Compare commits

..

16 Commits

Author SHA1 Message Date
Matti Nannt
cb6d9afcdc fix: remove hardcoded 'public' schema from migrations and add IF EXISTS guards
- Remove hardcoded 'public' schema references from three migrations
- Add IF EXISTS guards to DROP statements to prevent failures
- Fixes migration failures for self-hosted deployments using custom PostgreSQL schemas

Resolves #6747
2025-11-10 17:29:46 +01:00
Dhruwang Jariwala
4df28878db fix: preview animation fix (duplicate) (#6784)
Co-authored-by: Praveen Thanikachalam <100035228+prave01@users.noreply.github.com>
2025-11-06 20:16:26 +00:00
Johannes
b355d05b25 fix: Tweak Recontact UI (#6783)
Co-authored-by: Dhruwang <dhruwangjariwala18@gmail.com>
2025-11-06 14:53:29 +00:00
Matti Nannt
e757e9aec9 fix: serve logo from self-hosted instance instead of external S3 bucket (#6781) 2025-11-05 14:57:44 +00:00
Dhruwang Jariwala
cf4119baf6 fix: update issue in welcome card (#6779) 2025-11-05 13:42:12 +00:00
Johannes
6be2ae3071 chore: update wording & UI tweak for easier SDK setup (#6777) 2025-11-05 06:10:14 +00:00
Dhruwang Jariwala
600b793641 chore: recalibrate survey editor width to 2/3 editor and 1/3 preview (#6772) 2025-11-04 09:10:31 +00:00
Dhruwang Jariwala
cde03b6997 fix: duplicate survey issue (#6774) 2025-11-04 08:19:25 +00:00
Anshuman Pandey
00371bfb01 docs: minio intructions for docker setup (#6773)
Co-authored-by: Akhilesh Patidar <akhileshpatidar989368@gmail.com>
Co-authored-by: Akhilesh <126186908+Akhileshait@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-11-04 06:23:05 +00:00
Johannes
6be6782531 docs: improve API docs for better DX (#6760)
Co-authored-by: Dhruwang <dhruwangjariwala18@gmail.com>
2025-10-31 11:59:40 +00:00
Pyrrian
3ae4f8aa68 fix: nindent typo in securityContext helm chart (#6753) 2025-10-31 12:35:20 +01:00
Thomas Brugman
3d3c69a92b feat: Add Dutch language support. (#6737) 2025-10-31 12:35:08 +01:00
dependabot[bot]
b1b94eaa66 chore(deps): bump next-auth from 4.24.11 to 4.24.12 in /apps/web in the npm_and_yarn group across 1 directory (#6751)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: pandeymangg <anshuman.pandey9999@gmail.com>
2025-10-30 13:09:31 +00:00
Marc T.
67cc96449d fix: allow access of /animated-bgs/** from public url (#6748)
Co-authored-by: pandeymangg <anshuman.pandey9999@gmail.com>
2025-10-30 12:21:50 +00:00
Dhruwang Jariwala
bf41a53b86 fix: survey ui loading issue (#6755) 2025-10-30 07:32:44 +00:00
Anshuman Pandey
26292ecf39 fix: welcome card headline in survey title (#6749) 2025-10-29 07:57:27 +00:00
56 changed files with 6240 additions and 6643 deletions

View File

@@ -89,7 +89,7 @@ jobs:
- check-latest-release
with:
IS_PRERELEASE: ${{ github.event.release.prerelease }}
MAKE_LATEST: ${{ needs.check-latest-release.outputs.is_latest == 'true' }}
MAKE_LATEST: ${{ needs.check-latest-release.outputs.is_latest }}
docker-build-cloud:
name: Build & push Formbricks Cloud to ECR
@@ -101,7 +101,7 @@ jobs:
with:
image_tag: ${{ needs.docker-build-community.outputs.VERSION }}
IS_PRERELEASE: ${{ github.event.release.prerelease }}
MAKE_LATEST: ${{ needs.check-latest-release.outputs.is_latest == 'true' }}
MAKE_LATEST: ${{ needs.check-latest-release.outputs.is_latest }}
needs:
- check-latest-release
- docker-build-community
@@ -154,4 +154,4 @@ jobs:
release_tag: ${{ github.event.release.tag_name }}
commit_sha: ${{ github.sha }}
is_prerelease: ${{ github.event.release.prerelease }}
make_latest: ${{ needs.check-latest-release.outputs.is_latest == 'true' }}
make_latest: ${{ needs.check-latest-release.outputs.is_latest }}

View File

@@ -13,7 +13,6 @@ import {
TIntegrationNotionDatabase,
} from "@formbricks/types/integration/notion";
import { TSurvey, TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types";
import { getTextContent } from "@formbricks/types/surveys/validation";
import { createOrUpdateIntegrationAction } from "@/app/(app)/environments/[environmentId]/project/integrations/actions";
import {
ERRORS,
@@ -123,7 +122,7 @@ export const AddIntegrationModal = ({
const questions = selectedSurvey
? replaceHeadlineRecall(selectedSurvey, "default")?.questions.map((q) => ({
id: q.id,
name: getTextContent(getLocalizedValue(q.headline, "default")),
name: getLocalizedValue(q.headline, "default"),
type: q.type,
}))
: [];

View File

@@ -96,7 +96,6 @@ export const SurveyAnalysisCTA = ({
const duplicateSurveyAndRoute = async (surveyId: string) => {
setLoading(true);
const duplicatedSurveyResponse = await copySurveyToOtherEnvironmentAction({
environmentId: environment.id,
surveyId: surveyId,
targetEnvironmentId: environment.id,
});

View File

@@ -7,7 +7,7 @@
},
"locale": {
"source": "en-US",
"targets": ["de-DE", "fr-FR", "ja-JP", "pt-BR", "pt-PT", "ro-RO", "zh-Hans-CN", "zh-Hant-TW", "es-ES"]
"targets": ["de-DE", "fr-FR", "ja-JP", "pt-BR", "pt-PT", "ro-RO", "zh-Hans-CN", "zh-Hant-TW", "nl-NL"]
},
"version": 1.8
}

View File

@@ -398,7 +398,6 @@ checksums:
common/user_id: 37f5ba37f71cb50607af32a6a203b1d4
common/user_not_found: 5903581136ac6c1c1351a482a6d8fdf7
common/variable: c13db5775ba9791b1522cc55c9c7acce
common/variable_ids: 44bf93b70703b7699fa9f21bc6c8eed4
common/variables: ffd3eec5497af36d7b4e4185bad1313a
common/verified_email: d4a9e5e47d622c6ef2fede44233076c7
common/video: 8050c90e4289b105a0780f0fdda6ff66
@@ -722,14 +721,14 @@ checksums:
environments/project/api_keys/secret: f041e5eb96121c8b4f2b8af7e0f83a9b
environments/project/api_keys/unable_to_delete_api_key: 1fd76d9a22c5f5f8c241c4891fca8295
environments/project/app-connection/app_connection: 778d2305e1a9c8efe91c2c7b4af37ae4
environments/project/app-connection/app_connection_description: 01327bfae3da950d796890b6605afed2
environments/project/app-connection/app_connection_description: dde226414bd2265cbd0daf6635efcfdd
environments/project/app-connection/cache_update_delay_description: 1cb2c46fdb6762ccb348d21086063a4f
environments/project/app-connection/cache_update_delay_title: fef7f99f0228f9e30093574ac7770e7e
environments/project/app-connection/environment_id: 3dba898b081c18cd4cae131765ef411f
environments/project/app-connection/environment_id_description: 8b4a763d069b000cfa1a2025a13df80c
environments/project/app-connection/formbricks_sdk_connected: 29e8a40ad6a7fdb5af5ee9451a70a9aa
environments/project/app-connection/formbricks_sdk_not_connected: 557c534e665750978ba6edb0eacb428e
environments/project/app-connection/formbricks_sdk_not_connected_description: 666b2b25f06e76554cc2d60f925bcd4b
environments/project/app-connection/formbricks_sdk_not_connected_description: 4ddbacae084238bd0cefeded0fe9dbb9
environments/project/app-connection/how_to_setup: 3bad40037f280b47fe6418fcbeb4c717
environments/project/app-connection/how_to_setup_description: 2ae5cd9456a8acd3986e3d3678e70ed2
environments/project/app-connection/receiving_data: 9f2a48c0b0278861add70b526061264c

View File

@@ -169,12 +169,12 @@ export const AVAILABLE_LOCALES: TUserLocale[] = [
"de-DE",
"pt-BR",
"fr-FR",
"nl-NL",
"zh-Hant-TW",
"pt-PT",
"ro-RO",
"ja-JP",
"zh-Hans-CN",
"es-ES",
];
// Billing constants

View File

@@ -137,7 +137,7 @@ export const appLanguages = [
"ro-RO": "Engleză (SUA)",
"ja-JP": "英語(米国)",
"zh-Hans-CN": "英语(美国)",
"es-ES": "Inglés (EE.UU.)",
"nl-NL": "Engels (VS)",
},
},
{
@@ -152,7 +152,7 @@ export const appLanguages = [
"ro-RO": "Germană",
"ja-JP": "ドイツ語",
"zh-Hans-CN": "德语",
"es-ES": "Alemán",
"nl-NL": "Duits",
},
},
{
@@ -167,7 +167,7 @@ export const appLanguages = [
"ro-RO": "Portugheză (Brazilia)",
"ja-JP": "ポルトガル語(ブラジル)",
"zh-Hans-CN": "葡萄牙语(巴西)",
"es-ES": "Portugs (Brasil)",
"nl-NL": "Portugees (Brazil)",
},
},
{
@@ -182,7 +182,7 @@ export const appLanguages = [
"ro-RO": "Franceză",
"ja-JP": "フランス語",
"zh-Hans-CN": "法语",
"es-ES": "Frans",
"nl-NL": "Frans",
},
},
{
@@ -197,7 +197,7 @@ export const appLanguages = [
"ro-RO": "Chineză (Tradicională)",
"ja-JP": "中国語(繁体字)",
"zh-Hans-CN": "繁体中文",
"es-ES": "Chino (Tradicional)",
"nl-NL": "Chinees (Traditioneel)",
},
},
{
@@ -212,7 +212,7 @@ export const appLanguages = [
"ro-RO": "Portugheză (Portugalia)",
"ja-JP": "ポルトガル語(ポルトガル)",
"zh-Hans-CN": "葡萄牙语(葡萄牙)",
"es-ES": "Portugs (Portugal)",
"nl-NL": "Portugees (Portugal)",
},
},
{
@@ -227,7 +227,7 @@ export const appLanguages = [
"ro-RO": "Română",
"ja-JP": "ルーマニア語",
"zh-Hans-CN": "罗马尼亚语",
"es-ES": "Rumano",
"nl-NL": "Roemeens",
},
},
{
@@ -242,7 +242,7 @@ export const appLanguages = [
"ro-RO": "Japoneză",
"ja-JP": "日本語",
"zh-Hans-CN": "日语",
"es-ES": "Japonés",
"nl-NL": "Japans",
},
},
{
@@ -257,22 +257,22 @@ export const appLanguages = [
"ro-RO": "Chineză (Simplificată)",
"ja-JP": "中国語(簡体字)",
"zh-Hans-CN": "简体中文",
"es-ES": "Chino (Simplificado)",
"nl-NL": "Chinees (Vereenvoudigd)",
},
},
{
code: "es-ES",
code: "nl-NL",
label: {
"en-US": "Spanish",
"de-DE": "Spanisch",
"pt-BR": "Espanhol",
"fr-FR": "Espagnol",
"zh-Hant-TW": "西班牙語",
"pt-PT": "Espanhol",
"ro-RO": "Spaniol",
"ja-JP": "スペイン語",
"zh-Hans-CN": "西班牙语",
"es-ES": "Español",
"en-US": "Dutch",
"de-DE": "Niederländisch",
"pt-BR": "Holandês",
"fr-FR": "Néerlandais",
"zh-Hant-TW": "荷蘭語",
"pt-PT": "Holandês",
"ro-RO": "Olandeză",
"ja-JP": "オランダ語",
"zh-Hans-CN": "荷兰语",
"nl-NL": "Nederlands",
},
},
];

View File

@@ -1,5 +1,5 @@
import { formatDistance, intlFormat } from "date-fns";
import { de, enUS, es, fr, ja, pt, ptBR, ro, zhCN, zhTW } from "date-fns/locale";
import { de, enUS, fr, ja, nl, pt, ptBR, ro, zhCN, zhTW } from "date-fns/locale";
import { TUserLocale } from "@formbricks/types/user";
export const convertDateString = (dateString: string | null) => {
@@ -91,6 +91,8 @@ const getLocaleForTimeSince = (locale: TUserLocale) => {
return ptBR;
case "fr-FR":
return fr;
case "nl-NL":
return nl;
case "zh-Hant-TW":
return zhTW;
case "pt-PT":
@@ -101,8 +103,6 @@ const getLocaleForTimeSince = (locale: TUserLocale) => {
return ja;
case "zh-Hans-CN":
return zhCN;
case "es-ES":
return es;
}
};

View File

@@ -425,7 +425,6 @@
"user_id": "Benutzer-ID",
"user_not_found": "Benutzer nicht gefunden",
"variable": "Variable",
"variable_ids": "Variablen-IDs",
"variables": "Variablen",
"verified_email": "Verifizierte E-Mail",
"video": "Video",
@@ -775,14 +774,14 @@
},
"app-connection": {
"app_connection": "App-Verbindung",
"app_connection_description": "Verbinde deine App mit Formbricks.",
"app_connection_description": "Verbinde deine App oder Website mit Formbricks.",
"cache_update_delay_description": "Wenn du Aktualisierungen an Umfragen, Kontakten, Aktionen oder anderen Daten vornimmst, kann es bis zu 5 Minuten dauern, bis diese Änderungen in deiner lokalen App, die das Formbricks SDK verwendet, angezeigt werden. Diese Verzögerung ist auf eine Einschränkung unseres aktuellen Caching-Systems zurückzuführen. Wir arbeiten aktiv an einer Überarbeitung des Cache und werden in Formbricks 4.0 eine Lösung veröffentlichen.",
"cache_update_delay_title": "Änderungen werden aufgrund von Caching nach 5 Minuten angezeigt",
"environment_id": "Deine Umgebungs-ID",
"environment_id_description": "Diese ID identifiziert eindeutig diese Formbricks Umgebung.",
"formbricks_sdk_connected": "Formbricks SDK ist verbunden",
"formbricks_sdk_not_connected": "Formbricks SDK ist noch nicht verbunden.",
"formbricks_sdk_not_connected_description": "Verbinde deine Website oder App mit Formbricks",
"formbricks_sdk_not_connected_description": "Füge das Formbricks SDK zu deiner Website oder App hinzu, um sie mit Formbricks zu verbinden",
"how_to_setup": "Wie einrichten",
"how_to_setup_description": "Befolge diese Schritte, um das Formbricks Widget in deiner App einzurichten.",
"receiving_data": "Daten werden empfangen 💃🕺",

View File

@@ -425,7 +425,6 @@
"user_id": "User ID",
"user_not_found": "User not found",
"variable": "Variable",
"variable_ids": "Variable IDs",
"variables": "Variables",
"verified_email": "Verified Email",
"video": "Video",
@@ -775,14 +774,14 @@
},
"app-connection": {
"app_connection": "App Connection",
"app_connection_description": "Connect your app to Formbricks.",
"app_connection_description": "Connect your app or website to Formbricks.",
"cache_update_delay_description": "When you make updates to surveys, contacts, actions, or other data, it can take up to 5 minutes for those changes to appear in your local app running the Formbricks SDK. This delay is due to a limitation in our current caching system. Were actively reworking the cache and will release a fix in Formbricks 4.0.",
"cache_update_delay_title": "Changes will be reflected after 5 minutes due to caching",
"environment_id": "Your Environment ID",
"environment_id_description": "This id uniquely identifies this Formbricks environment.",
"formbricks_sdk_connected": "Formbricks SDK is connected",
"formbricks_sdk_not_connected": "Formbricks SDK is not yet connected.",
"formbricks_sdk_not_connected_description": "Connect your website or app with Formbricks",
"formbricks_sdk_not_connected_description": "Add the Formbricks SDK to your website or app to connect it with Formbricks",
"how_to_setup": "How to setup",
"how_to_setup_description": "Follow these steps to setup the Formbricks widget within your app.",
"receiving_data": "Receiving data \uD83D\uDC83\uD83D\uDD7A",

File diff suppressed because it is too large Load Diff

View File

@@ -425,7 +425,6 @@
"user_id": "Identifiant d'utilisateur",
"user_not_found": "Utilisateur non trouvé",
"variable": "Variable",
"variable_ids": "Identifiants variables",
"variables": "Variables",
"verified_email": "Email vérifié",
"video": "Vidéo",
@@ -775,14 +774,14 @@
},
"app-connection": {
"app_connection": "Connexion d'une application",
"app_connection_description": "Vous pouvez connecter une application à Formbricks.",
"app_connection_description": "Connectez votre application ou site web à Formbricks.",
"cache_update_delay_description": "Lorsque vous effectuez des mises à jour sur les sondages, contacts, actions ou autres données, cela peut prendre jusqu'à 5 minutes pour que ces modifications apparaissent dans votre application locale exécutant le SDK Formbricks. Ce délai est dû à une limitation de notre système de mise en cache actuel. Nous retravaillons activement le cache et publierons une correction dans Formbricks 4.0.",
"cache_update_delay_title": "Les modifications seront reflétées après 5 minutes en raison de la mise en cache",
"environment_id": "Identifiant de votre environnement",
"environment_id_description": "Cet identifiant unique est attribué à votre environnement Formbricks.",
"formbricks_sdk_connected": "Le SDK Formbricks est connecté",
"formbricks_sdk_not_connected": "Le SDK Formbricks n'est pas encore connecté.",
"formbricks_sdk_not_connected_description": "Connectez votre site Web ou votre application à Formbricks.",
"formbricks_sdk_not_connected_description": "Ajoutez le SDK Formbricks à votre site web ou application pour le connecter à Formbricks",
"how_to_setup": "Comment configurer",
"how_to_setup_description": "Suivez ces étapes pour configurer le widget Formbricks dans votre application.",
"receiving_data": "Réception des données 💃🕺",

View File

@@ -425,7 +425,6 @@
"user_id": "ユーザーID",
"user_not_found": "ユーザーが見つかりません",
"variable": "変数",
"variable_ids": "変数ID",
"variables": "変数",
"verified_email": "認証済みメールアドレス",
"video": "動画",
@@ -775,14 +774,14 @@
},
"app-connection": {
"app_connection": "アプリ接続",
"app_connection_description": "あなたのアプリをFormbricksに接続します。",
"app_connection_description": "アプリやウェブサイトをFormbricksに接続します。",
"cache_update_delay_description": "フォーム・連絡先・アクションなどを更新してから、Formbricks SDK を実行中のローカルアプリに反映されるまで最大5分かかる場合があります。これは現在のキャッシュ方式の制限によるものです。私たちはキャッシュを改修中で、Formbricks 4.0 で修正を提供予定です。",
"cache_update_delay_title": "キャッシュのため変更の反映に最大5分かかります",
"environment_id": "あなたのEnvironmentId",
"environment_id_description": "このIDはこのFormbricks環境を一意に識別します。",
"formbricks_sdk_connected": "Formbricks SDK は接続されています",
"formbricks_sdk_not_connected": "Formbricks SDK はまだ接続されていません。",
"formbricks_sdk_not_connected_description": "あなたのウェブサイトまたはアプリをFormbricks接続してください",
"formbricks_sdk_not_connected_description": "FormbricksSDKをウェブサイトやアプリに追加して、Formbricks接続してください",
"how_to_setup": "セットアップ方法",
"how_to_setup_description": "アプリ内でFormbricksウィジェットを設定する手順に従ってください。",
"receiving_data": "データ受信中 💃🕺",

2914
apps/web/locales/nl-NL.json Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -425,7 +425,6 @@
"user_id": "ID do usuário",
"user_not_found": "Usuário não encontrado",
"variable": "variável",
"variable_ids": "IDs de variáveis",
"variables": "Variáveis",
"verified_email": "Email Verificado",
"video": "vídeo",
@@ -775,14 +774,14 @@
},
"app-connection": {
"app_connection": "Conexão do App",
"app_connection_description": "Conecte seu app ao Formbricks.",
"app_connection_description": "Conecte seu app ou site ao Formbricks.",
"cache_update_delay_description": "Quando você faz atualizações em pesquisas, contatos, ações ou outros dados, pode levar até 5 minutos para que essas mudanças apareçam no seu app local rodando o SDK do Formbricks. Esse atraso é devido a uma limitação no nosso sistema de cache atual. Estamos ativamente retrabalhando o cache e planejamos lançar uma correção no Formbricks 4.0.",
"cache_update_delay_title": "As mudanças serão refletidas após 5 minutos devido ao cache",
"environment_id": "Seu Id do Ambiente",
"environment_id_description": "Este ID identifica exclusivamente este ambiente do Formbricks.",
"formbricks_sdk_connected": "O SDK do Formbricks está conectado",
"formbricks_sdk_not_connected": "O SDK do Formbricks ainda não está conectado.",
"formbricks_sdk_not_connected_description": "Conecte seu site ou app com o Formbricks",
"formbricks_sdk_not_connected_description": "Adicione o SDK do Formbricks ao seu site ou app para conectá-lo ao Formbricks",
"how_to_setup": "Como configurar",
"how_to_setup_description": "Siga esses passos para configurar o widget do Formbricks no seu app.",
"receiving_data": "Recebendo dados 💃🕺",

View File

@@ -425,7 +425,6 @@
"user_id": "ID do Utilizador",
"user_not_found": "Utilizador não encontrado",
"variable": "Variável",
"variable_ids": "IDs de variáveis",
"variables": "Variáveis",
"verified_email": "Email verificado",
"video": "Vídeo",
@@ -775,14 +774,14 @@
},
"app-connection": {
"app_connection": "Conexão de aplicação",
"app_connection_description": "Conecte a sua aplicação ao Formbricks",
"app_connection_description": "Ligue a sua aplicação ou website ao Formbricks.",
"cache_update_delay_description": "Quando fizer atualizações para inquéritos, contactos, ações ou outros dados, pode demorar até 5 minutos para que essas alterações apareçam na sua aplicação local a correr o SDK do Formbricks. Este atraso deve-se a uma limitação no nosso atual sistema de cache. Estamos a trabalhar ativamente na reformulação da cache e lançaremos uma correção no Formbricks 4.0.",
"cache_update_delay_title": "As alterações serão refletidas após 5 minutos devido ao armazenamento em cache.",
"environment_id": "O seu identificador",
"environment_id_description": "Este id identifica o seu espaço Formbricks.",
"formbricks_sdk_connected": "O SDK do Formbricks está conectado",
"formbricks_sdk_not_connected": "O Formbricks SDK ainda não está conectado",
"formbricks_sdk_not_connected_description": "Ligue o seu website ou aplicação ao Formbricks",
"formbricks_sdk_not_connected_description": "Adicione o SDK do Formbricks ao seu website ou aplicação para o ligar ao Formbricks",
"how_to_setup": "Como configurar",
"how_to_setup_description": "Siga estes passos para configurar o widget Formbricks na sua aplicação.",
"receiving_data": "A receber dados 💃🕺",

View File

@@ -425,7 +425,6 @@
"user_id": "ID Utilizator",
"user_not_found": "Utilizatorul nu a fost găsit",
"variable": "Variabilă",
"variable_ids": "ID-uri variabile",
"variables": "Variante",
"verified_email": "Email verificat",
"video": "Video",
@@ -775,14 +774,14 @@
},
"app-connection": {
"app_connection": "Conectare aplicație",
"app_connection_description": "Conectează aplicația ta la Formbricks.",
"app_connection_description": "Conectează-ți aplicația sau site-ul la Formbricks.",
"cache_update_delay_description": "Când faci actualizări la sondaje, contacte, acțiuni sau alte date, poate dura până la 5 minute pentru ca aceste modificări să apară în aplicația locală care rulează SDK Formbricks. Această întârziere se datorează unei limitări în sistemul nostru actual de caching. Revedem activ cache-ul și vom lansa o soluție în Formbricks 4.0.",
"cache_update_delay_title": "Modificările vor fi reflectate după 5 minute datorită memorării în cache",
"environment_id": "ID-ul mediului tău",
"environment_id_description": "Acest id identifică în mod unic acest mediu Formbricks.",
"formbricks_sdk_connected": "SDK Formbricks este conectat",
"formbricks_sdk_not_connected": "Formbricks SDK nu este încă conectat.",
"formbricks_sdk_not_connected_description": "Conectează-ți site-ul sau aplicația cu Formbricks",
"formbricks_sdk_not_connected_description": "Adaugă SDK-ul Formbricks pe site-ul sau în aplicația ta pentru a-l conecta la Formbricks.",
"how_to_setup": "Cum să configurezi",
"how_to_setup_description": "Urmează acești pași pentru a configura widget-ul Formbricks în aplicația ta.",
"receiving_data": "Recepționare date 💃🕺",

View File

@@ -425,7 +425,6 @@
"user_id": "用户 ID",
"user_not_found": "用户 不存在",
"variable": "变量",
"variable_ids": "变量 ID",
"variables": "变量",
"verified_email": "已验证 电子邮件",
"video": "视频",
@@ -775,14 +774,14 @@
},
"app-connection": {
"app_connection": "应用程序 连接",
"app_connection_description": "连接 您 的 应用 与 Formbricks。",
"app_connection_description": "将您的应用或网站连接到 Formbricks。",
"cache_update_delay_description": "当 你 对 调查 、 联系人 、 操作 或 其他 数据 进行 更新 时 可能 需要 最多 5 分钟 更改 才能 显示 在 你 本地 运行 Formbricks SDK 的 应用程序 中 。 这个 延迟 是 由于 我们 当前 缓存 系统 的 限制 。 我们 正在 积极 重新设计 缓存 并 将 在 Formbricks 4.0 中 发布 修复 。",
"cache_update_delay_title": "更改 将 在 5 分钟 后 由于 缓存 而 显示",
"environment_id": "你的 环境 ID",
"environment_id_description": "这个 id 独特地 标识 这个 Formbricks 环境。",
"formbricks_sdk_connected": "Formbricks SDK 已连接",
"formbricks_sdk_not_connected": "Formbricks SDK 尚未连接。",
"formbricks_sdk_not_connected_description": "连接 您 的 网站 或 应用 与 Formbricks",
"formbricks_sdk_not_connected_description": "将 Formbricks SDK 添加到您的网站或应用,以将其连接到 Formbricks",
"how_to_setup": "如何设置",
"how_to_setup_description": "遵循这些步骤在你的应用中设置 Formbricks 小部件。",
"receiving_data": "接收 数据 💃🕺",

View File

@@ -425,7 +425,6 @@
"user_id": "使用者 ID",
"user_not_found": "找不到使用者",
"variable": "變數",
"variable_ids": "變數 ID",
"variables": "變數",
"verified_email": "已驗證的電子郵件",
"video": "影片",
@@ -775,14 +774,14 @@
},
"app-connection": {
"app_connection": "應用程式連線",
"app_connection_description": "將您的應用程式連線至 Formbricks。",
"app_connection_description": "將您的應用程式或網站連接到 Formbricks。",
"cache_update_delay_description": "當您對調查、聯絡人、操作或其他資料進行更新時,可能需要長達 5 分鐘這些變更才能顯示在執行 Formbricks SDK 的本地應用程式中。此延遲是因我們目前快取系統的限制。我們正積極重新設計快取,並將在 Formbricks 4.0 中發佈修補程式。",
"cache_update_delay_title": "更改將於 5 分鐘後因快取而反映",
"environment_id": "您的 EnvironmentId",
"environment_id_description": "此 ID 可唯一識別此 Formbricks 環境。",
"formbricks_sdk_connected": "Formbricks SDK 已連線",
"formbricks_sdk_not_connected": "Formbricks SDK 尚未連線。",
"formbricks_sdk_not_connected_description": "將您的網站或應用程式 Formbricks 連線",
"formbricks_sdk_not_connected_description": "將 Formbricks SDK 添加到您的網站或應用程式,以連接到 Formbricks",
"how_to_setup": "如何設定",
"how_to_setup_description": "請按照這些步驟在您的應用程式中設定 Formbricks 小工具。",
"receiving_data": "正在接收資料 💃🕺",

View File

@@ -85,6 +85,6 @@ export const middleware = async (originalRequest: NextRequest) => {
export const config = {
matcher: [
"/((?!_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt|js|css|images|fonts|icons|public).*)",
"/((?!_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt|js|css|images|fonts|icons|public|animated-bgs).*)",
],
};

View File

@@ -62,7 +62,10 @@ const validateLanguages = (languages: Language[], t: TFunction) => {
return false;
}
// Check if the chosen alias matches an ISO identifier of a language that hasn't been added
// Prevent choosing an alias that clashes with the ISO code of some other
// language. Without this guard users could create ambiguous language entries
// (e.g. alias "nl" pointing to a non-Dutch language) which later breaks the
// dropdowns that rely on ISO identifiers.
for (const alias of languageAliases) {
if (iso639Languages.some((language) => language.alpha2 === alias && !languageCodes.includes(alias))) {
toast.error(t("environments.project.languages.conflict_between_selected_alias_and_another_language"), {

View File

@@ -43,8 +43,13 @@ export function LanguageSelect({ language, onLanguageChange, disabled, locale }:
setIsOpen(false);
};
// Most ISO entries don't ship with every locale translation, so fall back to
// English to keep the dropdown readable for locales such as Dutch that were
// added recently.
const getLabelForLocale = (item: TIso639Language) => item.label[locale] ?? item.label["en-US"];
const filteredItems = items.filter((item) =>
item.label[locale].toLowerCase().includes(searchTerm.toLowerCase())
getLabelForLocale(item).toLowerCase().includes(searchTerm.toLowerCase())
);
// Focus the input when the dropdown is opened
@@ -61,7 +66,9 @@ export function LanguageSelect({ language, onLanguageChange, disabled, locale }:
disabled={disabled}
onClick={toggleDropdown}
variant="ghost">
<span className="mr-2">{selectedOption?.label[locale] ?? t("common.select")}</span>
<span className="mr-2">
{selectedOption ? getLabelForLocale(selectedOption) : t("common.select")}
</span>
<ChevronDown className="h-4 w-4" />
</Button>
<div
@@ -84,7 +91,7 @@ export function LanguageSelect({ language, onLanguageChange, disabled, locale }:
onClick={() => {
handleOptionSelect(item);
}}>
{item.label[locale]}
{getLabelForLocale(item)}
</button>
))}
</div>

View File

@@ -3,7 +3,6 @@
import { HandshakeIcon, Undo2Icon } from "lucide-react";
import { useTranslation } from "react-i18next";
import { TSurveyEndings } from "@formbricks/types/surveys/types";
import { getTextContent } from "@formbricks/types/surveys/validation";
import { getLocalizedValue } from "@/lib/i18n/utils";
import {
Select,
@@ -42,7 +41,7 @@ export const EndingCardSelector = ({ endings, value, onChange }: EndingCardSelec
{/* Custom endings */}
{endingCards.map((ending) => (
<SelectItem key={ending.id} value={ending.id}>
{getTextContent(getLocalizedValue(ending.headline, "default"))}
{getLocalizedValue(ending.headline, "default")}
</SelectItem>
))}
</SelectGroup>

View File

@@ -51,7 +51,7 @@ export const AppConnectionPage = async ({ params }: { params: Promise<{ environm
<div className="space-y-4">
<WidgetStatusIndicator environment={environment} />
{!environment.appSetupCompleted ? (
<Alert variant="outbound">
<Alert variant="info">
<AlertTitle>{t("environments.project.app-connection.setup_alert_title")}</AlertTitle>
<AlertDescription>
{t("environments.project.app-connection.setup_alert_description")}

View File

@@ -40,9 +40,7 @@ export const AdvancedSettings = ({
updateQuestion={updateQuestion}
/>
{showOptionIds && (
<OptionIds type="question" question={question} selectedLanguageCode={selectedLanguageCode} />
)}
{showOptionIds && <OptionIds question={question} selectedLanguageCode={selectedLanguageCode} />}
</div>
);
};

View File

@@ -4,7 +4,6 @@ import { ArrowRightIcon } from "lucide-react";
import { ReactElement, useMemo } from "react";
import { useTranslation } from "react-i18next";
import { TSurvey, TSurveyLogic, TSurveyQuestion } from "@formbricks/types/surveys/types";
import { getTextContent } from "@formbricks/types/surveys/validation";
import { getLocalizedValue } from "@/lib/i18n/utils";
import { LogicEditorActions } from "@/modules/survey/editor/components/logic-editor-actions";
import { LogicEditorConditions } from "@/modules/survey/editor/components/logic-editor-conditions";
@@ -49,7 +48,7 @@ export function LogicEditor({
const ques = localSurvey.questions[i];
options.push({
icon: QUESTIONS_ICON_MAP[ques.type],
label: getTextContent(getLocalizedValue(ques.headline, "default")),
label: getLocalizedValue(ques.headline, "default"),
value: ques.id,
});
}
@@ -58,8 +57,7 @@ export function LogicEditor({
options.push({
label:
ending.type === "endScreen"
? getTextContent(getLocalizedValue(ending.headline, "default")) ||
t("environments.surveys.edit.end_screen_card")
? getLocalizedValue(ending.headline, "default") || t("environments.surveys.edit.end_screen_card")
: ending.label || t("environments.surveys.edit.redirect_thank_you_card"),
value: ending.id,
});

View File

@@ -1,27 +1,19 @@
import Image from "next/image";
import { useTranslation } from "react-i18next";
import { TSurveyQuestion, TSurveyQuestionTypeEnum, TSurveyVariable } from "@formbricks/types/surveys/types";
import { TSurveyQuestion, TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types";
import { getLocalizedValue } from "@/lib/i18n/utils";
import { IdBadge } from "@/modules/ui/components/id-badge";
import { Label } from "@/modules/ui/components/label";
interface OptionIdsQuestionProps {
type: "question";
interface OptionIdsProps {
question: TSurveyQuestion;
selectedLanguageCode: string;
}
interface OptionIdsVariablesProps {
type: "variables";
variables: TSurveyVariable[];
}
type OptionIdsProps = OptionIdsQuestionProps | OptionIdsVariablesProps;
export const OptionIds = (props: OptionIdsProps) => {
export const OptionIds = ({ question, selectedLanguageCode }: OptionIdsProps) => {
const { t } = useTranslation();
const renderChoiceIds = (question: TSurveyQuestion, selectedLanguageCode: string) => {
const renderChoiceIds = () => {
switch (question.type) {
case TSurveyQuestionTypeEnum.MultipleChoiceSingle:
case TSurveyQuestionTypeEnum.MultipleChoiceMulti:
@@ -67,31 +59,10 @@ export const OptionIds = (props: OptionIdsProps) => {
}
};
const renderVariableIds = (variables: TSurveyVariable[]) => {
return (
<div className="flex flex-col gap-2">
{variables.map((variable) => (
<div key={variable.id}>
<IdBadge id={variable.id} label={variable.name} />
</div>
))}
</div>
);
};
if (props.type === "variables") {
return (
<div className="space-y-3">
<Label className="text-sm font-medium text-gray-700">{t("common.variable_ids")}</Label>
<div className="w-full">{renderVariableIds(props.variables)}</div>
</div>
);
}
return (
<div className="space-y-3">
<Label className="text-sm font-medium text-gray-700">{t("common.option_ids")}</Label>
<div className="w-full">{renderChoiceIds(props.question, props.selectedLanguageCode)}</div>
<div className="w-full">{renderChoiceIds()}</div>
</div>
);
};

View File

@@ -65,7 +65,7 @@ export const SurveyEditorTabs = ({
let tabsToDisplay = isCxMode ? tabsComputed.filter((tab) => tab.id !== "settings") : tabsComputed;
return (
<div className="fixed z-30 flex h-12 w-full items-center justify-center border-b bg-white md:w-1/2">
<div className="fixed z-30 flex h-12 w-full items-center justify-center border-b bg-white md:w-2/3">
<nav className="flex h-full items-center space-x-4" aria-label="Tabs">
{tabsToDisplay.map((tab) => (
<button

View File

@@ -176,7 +176,7 @@ export const SurveyEditor = ({
/>
<div className="relative z-0 flex flex-1 overflow-hidden">
<main
className="relative z-0 w-1/2 flex-1 overflow-y-auto bg-slate-50 focus:outline-none"
className="relative z-0 w-full overflow-y-auto bg-slate-50 focus:outline-none md:w-2/3"
ref={surveyEditorRef}>
<SurveyEditorTabs
activeId={activeView}
@@ -260,7 +260,7 @@ export const SurveyEditor = ({
)}
</main>
<aside className="group hidden flex-1 flex-shrink-0 items-center justify-center overflow-hidden border-l border-slate-200 bg-slate-100 shadow-inner md:flex md:flex-col">
<aside className="group hidden w-1/3 flex-shrink-0 items-center justify-center overflow-hidden border-l border-slate-200 bg-slate-100 shadow-inner md:flex md:flex-col">
<PreviewSurvey
survey={localSurvey}
questionId={activeQuestionId}

View File

@@ -7,7 +7,6 @@ import { useTranslation } from "react-i18next";
import { TSurveyQuota } from "@formbricks/types/quota";
import { TSurvey, TSurveyQuestionId } from "@formbricks/types/surveys/types";
import { cn } from "@/lib/cn";
import { OptionIds } from "@/modules/survey/editor/components/option-ids";
import { SurveyVariablesCardItem } from "@/modules/survey/editor/components/survey-variables-card-item";
interface SurveyVariablesCardProps {
@@ -92,12 +91,6 @@ export const SurveyVariablesCard = ({
setLocalSurvey={setLocalSurvey}
quotas={quotas}
/>
{localSurvey.variables.length > 0 && (
<div className="mt-6">
<OptionIds type="variables" variables={localSurvey.variables} />
</div>
)}
</Collapsible.CollapsibleContent>
</Collapsible.Root>
</div>

View File

@@ -594,7 +594,7 @@ export const getMatchValueProps = (
const questionOptions = openTextQuestions.map((question) => {
return {
icon: getQuestionIconMapping(t)[question.type],
label: getTextContent(getLocalizedValue(question.headline, "default")),
label: getLocalizedValue(question.headline, "default"),
value: question.id,
meta: {
type: "question",
@@ -691,7 +691,7 @@ export const getMatchValueProps = (
const questionOptions = allowedQuestions.map((question) => {
return {
icon: getQuestionIconMapping(t)[question.type],
label: getTextContent(getLocalizedValue(question.headline, "default")),
label: getLocalizedValue(question.headline, "default"),
value: question.id,
meta: {
type: "question",
@@ -765,7 +765,7 @@ export const getMatchValueProps = (
const questionOptions = allowedQuestions.map((question) => {
return {
icon: getQuestionIconMapping(t)[question.type],
label: getTextContent(getLocalizedValue(question.headline, "default")),
label: getLocalizedValue(question.headline, "default"),
value: question.id,
meta: {
type: "question",
@@ -845,7 +845,7 @@ export const getMatchValueProps = (
const questionOptions = allowedQuestions.map((question) => {
return {
icon: getQuestionIconMapping(t)[question.type],
label: getTextContent(getLocalizedValue(question.headline, "default")),
label: getLocalizedValue(question.headline, "default"),
value: question.id,
meta: {
type: "question",

View File

@@ -18,7 +18,6 @@ import toast from "react-hot-toast";
import { useTranslation } from "react-i18next";
import { TSurveyFollowUpAction, TSurveyFollowUpTrigger } from "@formbricks/database/types/survey-follow-up";
import { TSurvey, TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types";
import { getTextContent } from "@formbricks/types/surveys/validation";
import { TUserLocale } from "@formbricks/types/user";
import { getLocalizedValue } from "@/lib/i18n/utils";
import { recallToHeadline } from "@/lib/utils/recall";
@@ -141,9 +140,9 @@ export const FollowUpModal = ({
return [
...openTextAndContactQuestions.map((question) => ({
label: getTextContent(
recallToHeadline(question.headline, localSurvey, false, selectedLanguageCode)[selectedLanguageCode]
),
label: recallToHeadline(question.headline, localSurvey, false, selectedLanguageCode)[
selectedLanguageCode
],
id: question.id,
type:
question.type === TSurveyQuestionTypeEnum.OpenText
@@ -518,9 +517,7 @@ export const FollowUpModal = ({
const getEndingLabel = (): string => {
if (ending.type === "endScreen") {
return (
getTextContent(
getLocalizedValue(ending.headline, selectedLanguageCode)
) || "Ending"
getLocalizedValue(ending.headline, selectedLanguageCode) || "Ending"
);
}

View File

@@ -32,6 +32,16 @@ vi.mock("@/lib/styling/constants", () => ({
},
}));
// Mock recall utility
vi.mock("@/lib/utils/recall", () => ({
recallToHeadline: vi.fn((headline) => headline),
}));
// Mock text content extraction
vi.mock("@formbricks/types/surveys/validation", () => ({
getTextContent: vi.fn((text) => text),
}));
describe("Metadata Utils", () => {
// Reset all mocks before each test
beforeEach(() => {
@@ -173,6 +183,75 @@ describe("Metadata Utils", () => {
WEBAPP_URL: "https://test.formbricks.com",
}));
});
test("handles welcome card headline with HTML content", async () => {
const { getTextContent } = await import("@formbricks/types/surveys/validation");
const mockSurvey = {
id: mockSurveyId,
environmentId: mockEnvironmentId,
name: "Test Survey",
metadata: {},
languages: [],
welcomeCard: {
enabled: true,
timeToFinish: false,
showResponseCount: false,
headline: {
default: "<p>Welcome <strong>Headline</strong></p>",
},
html: {
default: "Welcome Description",
},
} as TSurveyWelcomeCard,
} as TSurvey;
vi.mocked(getSurvey).mockResolvedValue(mockSurvey);
vi.mocked(getTextContent).mockReturnValue("Welcome Headline");
const result = await getBasicSurveyMetadata(mockSurveyId);
expect(getTextContent).toHaveBeenCalled();
expect(result.title).toBe("Welcome Headline");
});
test("handles welcome card headline with recall variables", async () => {
const { recallToHeadline } = await import("@/lib/utils/recall");
const mockSurvey = {
id: mockSurveyId,
environmentId: mockEnvironmentId,
name: "Test Survey",
metadata: {},
languages: [],
welcomeCard: {
enabled: true,
timeToFinish: false,
showResponseCount: false,
headline: {
default: "Welcome #recall:name/fallback:User#",
},
html: {
default: "Welcome Description",
},
} as TSurveyWelcomeCard,
} as TSurvey;
vi.mocked(getSurvey).mockResolvedValue(mockSurvey);
vi.mocked(recallToHeadline).mockReturnValue({
default: "Welcome @User",
});
const result = await getBasicSurveyMetadata(mockSurveyId);
expect(recallToHeadline).toHaveBeenCalledWith(
mockSurvey.welcomeCard.headline,
mockSurvey,
false,
"default"
);
expect(result.title).toBe("Welcome @User");
});
});
describe("getSurveyOpenGraphMetadata", () => {

View File

@@ -1,12 +1,13 @@
"use server";
import { z } from "zod";
import { ResourceNotFoundError } from "@formbricks/types/errors";
import { OperationNotAllowedError, ResourceNotFoundError } from "@formbricks/types/errors";
import { ZSurveyFilterCriteria } from "@formbricks/types/surveys/types";
import { authenticatedActionClient } from "@/lib/utils/action-client";
import { checkAuthorizationUpdated } from "@/lib/utils/action-client/action-client-middleware";
import { AuthenticatedActionClientCtx } from "@/lib/utils/action-client/types/context";
import {
getEnvironmentIdFromSurveyId,
getOrganizationIdFromEnvironmentId,
getOrganizationIdFromSurveyId,
getProjectIdFromEnvironmentId,
@@ -50,7 +51,6 @@ export const getSurveyAction = authenticatedActionClient
});
const ZCopySurveyToOtherEnvironmentAction = z.object({
environmentId: z.string().cuid2(),
surveyId: z.string().cuid2(),
targetEnvironmentId: z.string().cuid2(),
});
@@ -66,9 +66,10 @@ export const copySurveyToOtherEnvironmentAction = authenticatedActionClient
parsedInput,
}: {
ctx: AuthenticatedActionClientCtx;
parsedInput: Record<string, any>;
parsedInput: z.infer<typeof ZCopySurveyToOtherEnvironmentAction>;
}) => {
const sourceEnvironmentProjectId = await getProjectIdIfEnvironmentExists(parsedInput.environmentId);
const sourceEnvironmentId = await getEnvironmentIdFromSurveyId(parsedInput.surveyId);
const sourceEnvironmentProjectId = await getProjectIdIfEnvironmentExists(sourceEnvironmentId);
const targetEnvironmentProjectId = await getProjectIdIfEnvironmentExists(
parsedInput.targetEnvironmentId
);
@@ -76,13 +77,25 @@ export const copySurveyToOtherEnvironmentAction = authenticatedActionClient
if (!sourceEnvironmentProjectId || !targetEnvironmentProjectId) {
throw new ResourceNotFoundError(
"Environment",
sourceEnvironmentProjectId ? parsedInput.targetEnvironmentId : parsedInput.environmentId
sourceEnvironmentProjectId ? parsedInput.targetEnvironmentId : sourceEnvironmentId
);
}
const sourceEnvironmentOrganizationId = await getOrganizationIdFromEnvironmentId(sourceEnvironmentId);
const targetEnvironmentOrganizationId = await getOrganizationIdFromEnvironmentId(
parsedInput.targetEnvironmentId
);
if (sourceEnvironmentOrganizationId !== targetEnvironmentOrganizationId) {
throw new OperationNotAllowedError(
"Source and target environments must be in the same organization"
);
}
// authorization check for source environment
await checkAuthorizationUpdated({
userId: ctx.user.id,
organizationId: await getOrganizationIdFromEnvironmentId(parsedInput.environmentId),
organizationId: sourceEnvironmentOrganizationId,
access: [
{
type: "organization",
@@ -96,9 +109,10 @@ export const copySurveyToOtherEnvironmentAction = authenticatedActionClient
],
});
// authorization check for target environment
await checkAuthorizationUpdated({
userId: ctx.user.id,
organizationId: await getOrganizationIdFromEnvironmentId(parsedInput.environmentId),
organizationId: targetEnvironmentOrganizationId,
access: [
{
type: "organization",
@@ -112,12 +126,10 @@ export const copySurveyToOtherEnvironmentAction = authenticatedActionClient
],
});
ctx.auditLoggingCtx.organizationId = await getOrganizationIdFromEnvironmentId(
parsedInput.environmentId
);
ctx.auditLoggingCtx.organizationId = sourceEnvironmentOrganizationId;
ctx.auditLoggingCtx.surveyId = parsedInput.surveyId;
const result = await copySurveyToOtherEnvironment(
parsedInput.environmentId,
sourceEnvironmentId,
parsedInput.surveyId,
parsedInput.targetEnvironmentId,
ctx.user.id

View File

@@ -129,7 +129,6 @@ export const CopySurveyForm = ({ defaultProjects, survey, onCancel, setOpen }: C
return {
operation: copySurveyToOtherEnvironmentAction({
environmentId: survey.environmentId,
surveyId: survey.id,
targetEnvironmentId: environmentId,
}),

View File

@@ -113,7 +113,6 @@ export const SurveyDropDownMenu = ({
setLoading(true);
try {
const duplicatedSurveyResponse = await copySurveyToOtherEnvironmentAction({
environmentId,
surveyId,
targetEnvironmentId: environmentId,
});

View File

@@ -167,7 +167,7 @@ export const MediaBackground: React.FC<MediaBackgroundProps> = ({
<div
ref={ContentRef}
data-testid="mobile-preview-container"
className={`relative h-[90%] w-[45%] overflow-hidden rounded-[3rem] border-[6px] border-slate-400 ${getFilterStyle()}`}>
className={`relative h-[90%] w-full overflow-hidden rounded-[3rem] border-[6px] border-slate-400 lg:w-[75%] ${getFilterStyle()}`}>
{/* below element is use to create notch for the mobile device mockup */}
<div className="absolute left-1/2 right-1/2 top-2 z-20 h-4 w-1/3 -translate-x-1/2 transform rounded-full bg-slate-400"></div>
{surveyType === "link" && renderBackground()}

View File

@@ -1,12 +1,13 @@
"use client";
import { Environment, Project } from "@prisma/client";
import { Variants, motion } from "framer-motion";
import { motion } from "framer-motion";
import { ExpandIcon, MonitorIcon, ShrinkIcon, SmartphoneIcon } from "lucide-react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { TProjectStyling } from "@formbricks/types/project";
import { TSurvey, TSurveyQuestionId, TSurveyStyling } from "@formbricks/types/surveys/types";
import { cn } from "@/lib/cn";
import { ClientLogo } from "@/modules/ui/components/client-logo";
import { MediaBackground } from "@/modules/ui/components/media-background";
import { ResetProgressButton } from "@/modules/ui/components/reset-progress-button";
@@ -28,33 +29,6 @@ interface PreviewSurveyProps {
let surveyNameTemp: string;
const previewParentContainerVariant: Variants = {
expanded: {
position: "fixed",
height: "100%",
width: "100%",
backgroundColor: "rgba(0, 0, 0, 0.4)",
backdropFilter: "blur(15px)",
left: 0,
top: 0,
zIndex: 1040,
transition: {
ease: "easeIn",
duration: 0.001,
},
},
shrink: {
display: "none",
position: "fixed",
backgroundColor: "rgba(0, 0, 0, 0.0)",
backdropFilter: "blur(0px)",
transition: {
duration: 0,
},
zIndex: -1,
},
};
let setQuestionId = (_: string) => {};
export const PreviewSurvey = ({
@@ -72,44 +46,8 @@ export const PreviewSurvey = ({
const [appSetupCompleted, setAppSetupCompleted] = useState(false);
const [previewMode, setPreviewMode] = useState("desktop");
const [previewPosition, setPreviewPosition] = useState("relative");
const ContentRef = useRef<HTMLDivElement | null>(null);
const [shrink, setShrink] = useState(false);
const { projectOverwrites } = survey || {};
const previewScreenVariants: Variants = {
expanded: {
right: "5%",
bottom: "10%",
top: "12%",
width: "40%",
position: "fixed",
height: "80%",
zIndex: 1050,
boxShadow: "0px 4px 5px 4px rgba(169, 169, 169, 0.25)",
transition: {
ease: "easeInOut",
duration: shrink ? 0.3 : 0,
},
},
expanded_with_fixed_positioning: {
zIndex: 1050,
position: "fixed",
top: "5%",
right: "5%",
bottom: "10%",
width: "90%",
height: "90%",
transition: {
ease: "easeOut",
duration: 0.4,
},
},
shrink: {
display: "relative",
width: ["95%"],
height: ["95%"],
},
};
const { placement: surveyPlacement } = projectOverwrites || {};
const { darkOverlay: surveyDarkOverlay } = projectOverwrites || {};
@@ -226,22 +164,40 @@ export const PreviewSurvey = ({
};
return (
<div className="flex h-full w-full flex-col items-center justify-items-center py-4" id="survey-preview">
<div
className="flex h-full w-full flex-col items-center justify-items-center p-2 py-4"
id="survey-preview">
<motion.div
variants={previewParentContainerVariant}
animate={isFullScreenPreview ? "expanded" : "shrink"}
className={cn(
"z-50 flex h-full w-fit items-center justify-center",
isFullScreenPreview && "h-full w-full bg-zinc-500/50 backdrop-blur-md"
)}
style={{
position: isFullScreenPreview ? "fixed" : "absolute",
zIndex: 50,
left: isFullScreenPreview ? 0 : undefined,
top: isFullScreenPreview ? 0 : undefined,
}}
transition={{
ease: "easeInOut",
delay: 1.5,
}}
/>
<motion.div
layout
variants={previewScreenVariants}
animate={
isFullScreenPreview
? previewPosition === "relative"
? "expanded"
: "expanded_with_fixed_positioning"
: "shrink"
}
className="relative flex h-full w-[95%] items-center justify-center rounded-lg border border-slate-300">
style={{
left: isFullScreenPreview ? "2.5%" : undefined,
top: isFullScreenPreview ? 0 : undefined,
}}
transition={{
duration: 0.8,
ease: "easeInOut",
type: "spring",
}}
className={cn(
"z-50 flex h-[95%] w-full items-center justify-center overflow-hidden rounded-lg border border-slate-300",
isFullScreenPreview && "absolute z-50 h-[95%] w-[95%]"
)}>
{previewMode === "mobile" && (
<>
<p className="absolute left-0 top-0 m-2 rounded bg-slate-100 px-2 py-1 text-xs text-slate-400">
@@ -316,13 +272,9 @@ export const PreviewSurvey = ({
className="h-3 w-3 cursor-pointer rounded-full bg-emerald-500"
onClick={() => {
if (isFullScreenPreview) {
setShrink(true);
setPreviewPosition("relative");
setTimeout(() => setIsFullScreenPreview(false), 300);
setIsFullScreenPreview(false);
} else {
setShrink(false);
setIsFullScreenPreview(true);
setTimeout(() => setPreviewPosition("fixed"), 300);
}
}}
aria-label={isFullScreenPreview ? "Shrink Preview" : "Expand Preview"}></button>
@@ -339,18 +291,14 @@ export const PreviewSurvey = ({
<ShrinkIcon
className="mr-1 h-[22px] w-[22px] cursor-pointer rounded-md bg-white p-1 text-slate-500 hover:text-slate-700"
onClick={() => {
setShrink(true);
setPreviewPosition("relative");
setTimeout(() => setIsFullScreenPreview(false), 300);
setIsFullScreenPreview(false);
}}
/>
) : (
<ExpandIcon
className="mr-1 h-[22px] w-[22px] cursor-pointer rounded-md bg-white p-1 text-slate-500 hover:text-slate-700"
onClick={() => {
setShrink(false);
setIsFullScreenPreview(true);
setTimeout(() => setPreviewPosition("fixed"), 300);
}}
/>
)}

View File

@@ -102,7 +102,7 @@
"markdown-it": "14.1.0",
"mime-types": "3.0.1",
"next": "15.5.6",
"next-auth": "4.24.11",
"next-auth": "4.24.12",
"next-safe-action": "7.10.8",
"node-fetch": "3.3.2",
"nodemailer": "7.0.9",

View File

@@ -216,12 +216,12 @@ vi.mock("@/lib/constants", () => ({
"de-DE",
"pt-BR",
"fr-FR",
"nl-NL",
"zh-Hant-TW",
"pt-PT",
"ro-RO",
"ja-JP",
"zh-Hans-CN",
"es-ES",
],
DEFAULT_LOCALE: "en-US",
BREVO_API_KEY: "mock-brevo-api-key",

View File

@@ -1238,21 +1238,45 @@
"requestBody": {
"content": {
"application/json": {
"examples": {
"default": {
"value": {
"data": {
"hs8yd14l9h8u353tjmv6rzaw": "clicked",
"tcgls0063n8ri7dtrbnepcmz": "Who? Who? Who?"
},
"finished": false,
"meta": {
"action": "test action",
"source": "Postman API",
"url": "https://postman.com"
},
"surveyId": "{{surveyId}}",
"userId": "{{userId}}"
}
}
},
"schema": {
"example": {
"data": {
"hs8yd14l9h8u353tjmv6rzaw": "clicked",
"tcgls0063n8ri7dtrbnepcmz": "Who? Who? Who?"
"properties": {
"data": { "additionalProperties": true, "type": "object" },
"finished": { "type": "boolean" },
"language": {
"description": "Language of the response (survey should have this language enabled)",
"enum": ["en", "de", "pt", "etc.."],
"type": "string"
},
"finished": false,
"meta": {
"action": "test action",
"source": "Postman API",
"url": "https://postman.com"
"properties": {
"action": { "type": "string" },
"source": { "type": "string" },
"url": { "type": "string" }
},
"type": "object"
},
"surveyId": "{{surveyId}}",
"userId": "{{userId}} (optional)"
"surveyId": { "type": "string" },
"userId": { "type": "string" }
},
"required": ["surveyId"],
"type": "object"
}
}
@@ -2361,20 +2385,55 @@
},
"noCodeConfig": {
"description": "Configuration object required when type is 'noCode'. Defines the conditions for triggering the action. Not needed for 'code' type.",
"example": {
"elementSelector": {
"cssSelector": ".button-class",
"innerHtml": "Click me"
},
"type": "click",
"urlFilters": [
{
"rule": "contains",
"value": "https://www.google.com"
}
]
},
"nullable": true,
"properties": {
"elementSelector": {
"description": "Element selector (required for click type)",
"properties": {
"cssSelector": {
"description": "CSS selector for the element",
"type": "string"
},
"innerHtml": {
"description": "Inner HTML text to match",
"type": "string"
}
},
"type": "object"
},
"type": {
"description": "Type of no-code trigger",
"enum": ["click", "pageView", "exitIntent", "fiftyPercentScroll"],
"type": "string"
},
"urlFilters": {
"items": {
"properties": {
"rule": {
"description": "URL matching rule",
"enum": [
"exactMatch",
"contains",
"startsWith",
"endsWith",
"notMatch",
"notContains",
"matchesRegex"
],
"type": "string"
},
"value": {
"description": "URL pattern to match",
"type": "string"
}
},
"required": ["rule", "value"],
"type": "object"
},
"type": "array"
}
},
"required": ["type", "urlFilters"],
"type": "object"
},
"type": {
@@ -5021,17 +5080,52 @@
"requestBody": {
"content": {
"application/json": {
"examples": {
"default": {
"value": {
"createdAt": "2024-09-05T08:44:16.051Z",
"data": {
"hg508afs7lgx8nlni5dtit5u": ["Hello World"]
},
"finished": false,
"language": "en",
"surveyId": "clwj7hi7r0000vfhpfze6vjdg",
"updatedAt": "2024-09-05T08:44:16.051Z"
}
}
},
"schema": {
"example": {
"createdAt": "2024-09-05T08:44:16.051Z",
"data": {
"hg508afs7lgx8nlni5dtit5u": ["Hello World"]
"properties": {
"createdAt": {
"description": "Creation timestamp (optional; usually set by server)",
"format": "date-time",
"type": "string"
},
"finished": false,
"language": "default",
"surveyId": "clwj7hi7r0000vfhpfze6vjdg",
"updatedAt": "2024-09-05T08:44:16.051Z"
"data": {
"additionalProperties": true,
"description": "Answers keyed by questionId; value shape depends on question type",
"type": "object"
},
"finished": {
"description": "Whether the response is marked as finished",
"type": "boolean"
},
"language": {
"description": "Language of the response (survey should have this language enabled)",
"enum": ["en", "de", "pt", "etc.."],
"type": "string"
},
"surveyId": {
"description": "The ID of the survey this response belongs to",
"type": "string"
},
"updatedAt": {
"description": "Update timestamp (optional; usually set by server)",
"format": "date-time",
"type": "string"
}
},
"required": ["surveyId"],
"type": "object"
}
}
@@ -5768,6 +5862,32 @@
"allowedFileExtensions": {
"description": "Optional. List of allowed file extensions.",
"items": {
"enum": [
"heic",
"png",
"jpeg",
"jpg",
"webp",
"pdf",
"eml",
"doc",
"docx",
"xls",
"xlsx",
"ppt",
"pptx",
"txt",
"csv",
"mp4",
"mov",
"avi",
"mkv",
"webm",
"zip",
"rar",
"7z",
"tar"
],
"type": "string"
},
"type": "array"
@@ -6108,113 +6228,153 @@
"requestBody": {
"content": {
"application/json": {
"schema": {
"example": {
"autoClose": null,
"autoComplete": null,
"createdBy": null,
"delay": 0,
"displayLimit": null,
"displayOption": "displayOnce",
"displayPercentage": null,
"endings": [
{
"buttonLabel": {
"default": "Create your own Survey"
"examples": {
"default": {
"value": {
"autoClose": null,
"autoComplete": null,
"createdBy": null,
"delay": 0,
"displayLimit": null,
"displayOption": "displayOnce",
"displayPercentage": null,
"endings": [
{
"buttonLabel": { "default": "Create your own Survey" },
"buttonLink": "https://formbricks.com/signup",
"headline": { "default": "Thank you!" },
"id": "p73t62dgwq0cvmtt6ug0hmfc",
"subheader": { "default": "We appreciate your feedback." },
"type": "endScreen"
}
],
"environmentId": "{{environmentId}}",
"hiddenFields": { "enabled": false, "fieldIds": [] },
"isVerifyEmailEnabled": false,
"languages": [],
"name": "Example Survey",
"pin": null,
"productOverwrites": null,
"questions": [
{
"headline": { "default": "What would you like to know?" },
"id": "ovpy6va1hab7fl12n913zua0",
"inputType": "text",
"placeholder": { "default": "Type your answer here..." },
"required": true,
"subheader": { "default": "This is an example survey." },
"type": "openText"
},
"buttonLink": "https://formbricks.com/signup",
"headline": {
"default": "Thank you!"
{
"choices": [
{ "id": "xpoxuu3sifk1ee8he67ctf5i", "label": { "default": "Sun \u2600\ufe0f" } },
{ "id": "hnsovcdmxtcbly6tig1az3qc", "label": { "default": "Ocean \ud83c\udf0a" } },
{ "id": "kcnelzdxknvwo8fq20d3nrr5", "label": { "default": "Palms \ud83c\udf34" } }
],
"headline": { "default": "What's important on vacay?" },
"id": "awkn2llljy7a4oulp5t15yec",
"required": true,
"shuffleOption": "none",
"type": "multipleChoiceMulti"
}
],
"recontactDays": null,
"redirectUrl": null,
"segmentId": null,
"showLanguageSwitch": null,
"singleUse": { "enabled": false, "isEncrypted": true },
"status": "inProgress",
"styling": null,
"surveyClosedMessage": null,
"triggers": [],
"type": "link",
"welcomeCard": {
"enabled": true,
"fileUrl": "",
"headline": { "default": "Welcome!" },
"html": {
"default": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span style=\"white-space: pre-wrap;\">Thanks for providing your feedback - let's go!</span></p>"
},
"id": "p73t62dgwq0cvmtt6ug0hmfc",
"subheader": {
"default": "We appreciate your feedback."
},
"type": "endScreen"
"showResponseCount": false,
"timeToFinish": false
}
],
"environmentId": "{{environmentId}}",
"hiddenFields": {
"enabled": false,
"fieldIds": []
},
"isVerifyEmailEnabled": false,
"languages": [],
"name": "Example Survey",
"pin": null,
"productOverwrites": null,
"questions": [
{
"headline": {
"default": "What would you like to know?"
},
"id": "ovpy6va1hab7fl12n913zua0",
"inputType": "text",
"placeholder": {
"default": "Type your answer here..."
},
"required": true,
"subheader": {
"default": "This is an example survey."
},
"type": "openText"
},
{
"choices": [
{
"id": "xpoxuu3sifk1ee8he67ctf5i",
"label": {
"default": "Sun \u2600\ufe0f"
}
},
{
"id": "hnsovcdmxtcbly6tig1az3qc",
"label": {
"default": "Ocean \ud83c\udf0a"
}
},
{
"id": "kcnelzdxknvwo8fq20d3nrr5",
"label": {
"default": "Palms \ud83c\udf34"
}
}
],
"headline": {
"default": "What's important on vacay?"
},
"id": "awkn2llljy7a4oulp5t15yec",
"required": true,
"shuffleOption": "none",
"type": "multipleChoiceMulti"
}
],
"recontactDays": null,
"redirectUrl": null,
"segmentId": null,
"showLanguageSwitch": null,
"singleUse": {
"enabled": false,
"isEncrypted": true
},
"status": "inProgress",
"styling": null,
"surveyClosedMessage": null,
"triggers": [],
"type": "link",
"welcomeCard": {
"enabled": true,
"fileUrl": "",
"headline": {
"default": "Welcome!"
},
"html": {
"default": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span style=\"white-space: pre-wrap;\">Thanks for providing your feedback - let's go!</span></p>"
},
"showResponseCount": false,
"timeToFinish": false
}
}
},
"schema": {
"properties": {
"displayOption": {
"enum": ["displayOnce", "displayMultiple", "respondMultiple", "displaySome"],
"type": "string"
},
"environmentId": { "type": "string" },
"languages": {
"items": {
"properties": {
"default": { "type": "boolean" },
"enabled": { "type": "boolean" },
"language": {
"description": "Language of the survey (This language should exist in your environment)",
"enum": ["en", "de", "pt", "etc.."],
"type": "string"
}
},
"type": "object"
},
"type": "array"
},
"name": { "type": "string" },
"questions": {
"items": {
"properties": {
"date": {
"properties": {
"format": { "enum": ["M-d-y", "d-M-y", "y-M-d"], "type": "string" }
},
"type": "object"
},
"id": { "type": "string" },
"inputType": {
"enum": ["text", "email", "url", "number", "phone"],
"type": "string"
},
"rating": {
"properties": {
"range": { "enum": [3, 4, 5, 6, 7, 10], "type": "integer" },
"scale": { "enum": ["number", "smiley", "star"], "type": "string" }
},
"type": "object"
},
"shuffleOption": { "enum": ["none", "all", "exceptLast"], "type": "string" },
"type": {
"enum": [
"address",
"cta",
"consent",
"date",
"fileUpload",
"matrix",
"multipleChoiceMulti",
"multipleChoiceSingle",
"nps",
"openText",
"pictureSelection",
"rating",
"cal",
"ranking",
"contactInfo"
],
"type": "string"
}
},
"type": "object"
},
"type": "array"
},
"status": { "enum": ["draft", "inProgress", "paused", "completed"], "type": "string" },
"type": { "enum": ["link", "app"], "type": "string" }
},
"required": ["environmentId", "name", "type", "status"],
"type": "object"
}
}
@@ -7468,11 +7628,41 @@
"requestBody": {
"content": {
"application/json": {
"examples": {
"default": {
"value": {
"triggers": ["responseCreated", "responseUpdated", "responseFinished"],
"url": "https://eoy8o887lmsqmhz.m.pipedream.net"
}
}
},
"schema": {
"example": {
"triggers": ["responseCreated", "responseUpdated", "responseFinished"],
"url": "https://eoy8o887lmsqmhz.m.pipedream.net"
"properties": {
"name": {
"description": "Optional name for the webhook",
"type": "string"
},
"surveyIds": {
"description": "Optional list of survey IDs to filter webhook calls",
"items": {
"type": "string"
},
"type": "array"
},
"triggers": {
"description": "List of events that trigger this webhook",
"items": {
"enum": ["responseCreated", "responseUpdated", "responseFinished"],
"type": "string"
},
"type": "array"
},
"url": {
"description": "The webhook URL to call when triggers are fired",
"type": "string"
}
},
"required": ["url", "triggers"],
"type": "object"
}
}
@@ -7947,7 +8137,7 @@
},
"servers": [
{
"url": "http://{{baseurl}}",
"url": "https://{baseurl}",
"variables": {
"baseurl": {
"default": "localhost:3000"

View File

@@ -97,10 +97,44 @@ paths:
content:
application/json:
schema:
type: object
properties:
surveyId:
type: string
description: The ID of the survey this response belongs to
responses:
type: object
additionalProperties: true
description: Answers keyed by questionId; value shape depends on question type
finished:
type: boolean
description: Whether the response is marked as finished
language:
type: string
enum:
[
"en-US",
"de-DE",
"pt-BR",
"fr-FR",
"zh-Hant-TW",
"pt-PT",
"ro-RO",
"ja-JP",
"zh-Hans-CN",
]
description: Locale of the response
meta:
type: object
properties:
action: { type: string }
source: { type: string }
url: { type: string }
description: Optional metadata about the response
required: ["surveyId"]
example:
surveyId: survey123
responses: {}
type: object
responses:
"201":
content:
@@ -136,9 +170,15 @@ paths:
content:
application/json:
schema:
type: object
properties:
attributes:
type: object
additionalProperties: true
description: Key-value pairs of contact attributes
required: ["attributes"]
example:
attributes: {}
type: object
responses:
"200":
content:

View File

@@ -102,6 +102,7 @@ When PUBLIC_URL is configured, the following routes are automatically served fro
- `/fonts/*` - Font files
- `/icons/*` - Icon assets
- `/public/*` - Public static files
- `/animated-bgs/*` - Animated Background assets
#### Storage Routes

View File

@@ -117,6 +117,191 @@ Please take a look at our [migration guide](/self-hosting/advanced/migration) fo
docker compose up -d
```
## Optional: Adding MinIO for File Storage
MinIO provides S3-compatible object storage for file uploads in Formbricks. If you want to enable features like image uploads, file uploads in surveys, or custom logos, you can add MinIO to your Docker setup.
<Note>
For detailed information about file storage options and configuration, see our [File Uploads
Configuration](/self-hosting/configuration/file-uploads) guide.
</Note>
<Warning>
**For production deployments with HTTPS**, use the [one-click setup script](/self-hosting/setup/one-click)
which automatically configures MinIO with Traefik, SSL certificates, and a subdomain (required for MinIO in
production). The setups below are suitable for local development or testing only.
</Warning>
### Quick Start: Using docker-compose.dev.yml
The fastest way to test MinIO with Formbricks is to use the included `docker-compose.dev.yml` which already has MinIO pre-configured.
1. **Start MinIO and Services**
From the repository root:
```bash
docker compose -f docker-compose.dev.yml up -d
```
This starts PostgreSQL, Valkey (Redis), MinIO, and Mailhog.
2. **Access MinIO Console**
Open http://localhost:9001 in your browser.
Login credentials:
- Username: `devminio`
- Password: `devminio123`
3. **Create Bucket**
- Click "Buckets" in the left sidebar
- Click "Create Bucket"
- Name it: `formbricks`
4. **Configure Formbricks**
Update your `.env` file or environment variables with MinIO configuration:
```bash
# MinIO S3 Storage
S3_ACCESS_KEY="devminio"
S3_SECRET_KEY="devminio123"
S3_REGION="us-east-1"
S3_BUCKET_NAME="formbricks"
S3_ENDPOINT_URL="http://localhost:9000"
S3_FORCE_PATH_STYLE="1"
```
5. **Verify in MinIO Console**
After uploading files in Formbricks, view them at http://localhost:9001:
- Navigate to Buckets → formbricks → Browse
- Your uploaded files will appear here
<Note>
The `docker-compose.dev.yml` file includes MinIO with console access on port 9001, making it easy to
visually verify file uploads. This is the recommended approach for development and testing.
</Note>
### Manual MinIO Setup (Custom Configuration)
<Note>
<strong>Recommended:</strong> If you can, use <code>docker-compose.dev.yml</code> for the fastest path. Use
this manual approach only when you need to integrate MinIO into an existing <code>docker-compose.yml</code>{" "}
or customize settings.
</Note>
If you prefer to add MinIO to your own `docker-compose.yml`, follow these steps:
1. **Add the MinIO service**
Add this service alongside your existing `formbricks` and `postgres` services:
```yaml
services:
# ... your existing services (formbricks, postgres, redis/valkey, etc.)
minio:
image: minio/minio:latest
restart: always
command: server /data --console-address ":9001"
environment:
MINIO_ROOT_USER: "formbricks-root"
MINIO_ROOT_PASSWORD: "change-this-secure-password"
ports:
- "9000:9000" # S3 API
- "9001:9001" # Web console
volumes:
- minio-data:/data
```
<Note>
For production pinning, consider using a digest (e.g., <code>minio/minio@sha256:...</code>) and review
periodically with <code>docker inspect minio/minio:latest</code>.
</Note>
2. **Declare the MinIO volume**
Add (or extend) your `volumes` block:
```yaml
volumes:
postgres:
driver: local
redis:
driver: local
minio-data:
driver: local
```
3. **Start services**
```bash
docker compose up -d
```
4. **Open the MinIO Console & Create a Bucket**
- Visit **http://localhost:9001**
- Log in with:
- **Username:** `formbricks-root`
- **Password:** `change-this-secure-password`
- Go to **Buckets → Create Bucket**
- Name it: **`formbricks`**
5. **Configure Formbricks to use MinIO**
In your `.env` or `formbricks` service environment, set:
```bash
# MinIO S3 Storage
S3_ACCESS_KEY="formbricks-root"
S3_SECRET_KEY="change-this-secure-password"
S3_REGION="us-east-1"
S3_BUCKET_NAME="formbricks"
S3_ENDPOINT_URL="http://minio:9000"
S3_FORCE_PATH_STYLE="1"
```
<Note>
These credentials should match <code>MINIO_ROOT_USER</code> and <code>MINIO_ROOT_PASSWORD</code> above.
For local/dev this is fine. For production, create a dedicated MinIO user with restricted policies.
</Note>
6. **Verify uploads**
After uploading a file in Formbricks, check **http://localhost:9001**:
- **Buckets → formbricks → Browse**
You should see your uploaded files.
#### Tips & Common Gotchas
- **Connection refused**: Ensure the `minio` container is running and port **9000** is reachable from the Formbricks container (use the internal URL `http://minio:9000`).
- **Bucket not found**: Create the `formbricks` bucket in the console before uploading.
- **Auth failed**: Confirm `S3_ACCESS_KEY`/`S3_SECRET_KEY` match MinIO credentials.
- **Health check**: From the Formbricks container:
```bash
docker compose exec formbricks sh -c 'wget -O- http://minio:9000/minio/health/ready'
```
### Production Setup with Traefik
For production deployments, use the [one-click setup script](/self-hosting/setup/one-click) which automatically configures:
- MinIO service with Traefik reverse proxy
- Dedicated subdomain (e.g., `files.yourdomain.com`) - **required for production**
- Automatic SSL certificate generation via Let's Encrypt
- CORS configuration for your domain
- Rate limiting middleware
- Secure credential generation
The production setup from [formbricks.sh](https://github.com/formbricks/formbricks/blob/main/docker/formbricks.sh) includes advanced features not covered in this manual setup. For production use, we strongly recommend using the one-click installer.
## Debug
If you encounter any issues, you can check the logs of the container with this command:

View File

@@ -74,7 +74,7 @@ spec:
{{- end }}
{{- if .Values.deployment.securityContext }}
securityContext:
{{ toYaml .Values.deployment.securityContext | indent 8 }}
{{ toYaml .Values.deployment.securityContext | nindent 8 }}
{{- end }}
terminationGracePeriodSeconds: {{ .Values.deployment.terminationGracePeriodSeconds | default 30 }}
containers:

View File

@@ -79,7 +79,7 @@
},
"pnpm": {
"patchedDependencies": {
"next-auth@4.24.11": "patches/next-auth@4.24.11.patch"
"next-auth@4.24.12": "patches/next-auth@4.24.12.patch"
},
"overrides": {
"axios": ">=1.12.2",

View File

@@ -8,19 +8,19 @@
*/
-- Update any scheduled surveys to paused
UPDATE "public"."Survey" SET "status" = 'paused' WHERE "status" = 'scheduled';
UPDATE "Survey" SET "status" = 'paused' WHERE "status" = 'scheduled';
-- AlterEnum
BEGIN;
CREATE TYPE "public"."SurveyStatus_new" AS ENUM ('draft', 'inProgress', 'paused', 'completed');
ALTER TABLE "public"."Survey" ALTER COLUMN "status" DROP DEFAULT;
ALTER TABLE "public"."Survey" ALTER COLUMN "status" TYPE "public"."SurveyStatus_new" USING ("status"::text::"public"."SurveyStatus_new");
ALTER TYPE "public"."SurveyStatus" RENAME TO "SurveyStatus_old";
ALTER TYPE "public"."SurveyStatus_new" RENAME TO "SurveyStatus";
DROP TYPE "public"."SurveyStatus_old";
ALTER TABLE "public"."Survey" ALTER COLUMN "status" SET DEFAULT 'draft';
CREATE TYPE "SurveyStatus_new" AS ENUM ('draft', 'inProgress', 'paused', 'completed');
ALTER TABLE "Survey" ALTER COLUMN "status" DROP DEFAULT;
ALTER TABLE "Survey" ALTER COLUMN "status" TYPE "SurveyStatus_new" USING ("status"::text::"SurveyStatus_new");
ALTER TYPE "SurveyStatus" RENAME TO "SurveyStatus_old";
ALTER TYPE "SurveyStatus_new" RENAME TO "SurveyStatus";
DROP TYPE "SurveyStatus_old";
ALTER TABLE "Survey" ALTER COLUMN "status" SET DEFAULT 'draft';
COMMIT;
-- AlterTable
ALTER TABLE "public"."Survey" DROP COLUMN "closeOnDate",
ALTER TABLE "Survey" DROP COLUMN "closeOnDate",
DROP COLUMN "runOnDate";

View File

@@ -20,88 +20,88 @@
*/
-- AlterEnum
BEGIN;
CREATE TYPE "public"."SurveyType_new" AS ENUM ('link', 'app');
ALTER TABLE "public"."Survey" ALTER COLUMN "type" DROP DEFAULT;
ALTER TABLE "public"."Survey" ALTER COLUMN "type" TYPE "public"."SurveyType_new" USING ("type"::text::"public"."SurveyType_new");
ALTER TYPE "public"."SurveyType" RENAME TO "SurveyType_old";
ALTER TYPE "public"."SurveyType_new" RENAME TO "SurveyType";
DROP TYPE "public"."SurveyType_old";
ALTER TABLE "public"."Survey" ALTER COLUMN "type" SET DEFAULT 'app';
CREATE TYPE "SurveyType_new" AS ENUM ('link', 'app');
ALTER TABLE "Survey" ALTER COLUMN "type" DROP DEFAULT;
ALTER TABLE "Survey" ALTER COLUMN "type" TYPE "SurveyType_new" USING ("type"::text::"SurveyType_new");
ALTER TYPE "SurveyType" RENAME TO "SurveyType_old";
ALTER TYPE "SurveyType_new" RENAME TO "SurveyType";
DROP TYPE "SurveyType_old";
ALTER TABLE "Survey" ALTER COLUMN "type" SET DEFAULT 'app';
COMMIT;
-- DropForeignKey
ALTER TABLE "public"."Document" DROP CONSTRAINT "Document_environmentId_fkey";
ALTER TABLE IF EXISTS "Document" DROP CONSTRAINT IF EXISTS "Document_environmentId_fkey";
-- DropForeignKey
ALTER TABLE "public"."Document" DROP CONSTRAINT "Document_responseId_fkey";
ALTER TABLE IF EXISTS "Document" DROP CONSTRAINT IF EXISTS "Document_responseId_fkey";
-- DropForeignKey
ALTER TABLE "public"."Document" DROP CONSTRAINT "Document_surveyId_fkey";
ALTER TABLE IF EXISTS "Document" DROP CONSTRAINT IF EXISTS "Document_surveyId_fkey";
-- DropForeignKey
ALTER TABLE "public"."DocumentInsight" DROP CONSTRAINT "DocumentInsight_documentId_fkey";
ALTER TABLE IF EXISTS "DocumentInsight" DROP CONSTRAINT IF EXISTS "DocumentInsight_documentId_fkey";
-- DropForeignKey
ALTER TABLE "public"."DocumentInsight" DROP CONSTRAINT "DocumentInsight_insightId_fkey";
ALTER TABLE IF EXISTS "DocumentInsight" DROP CONSTRAINT IF EXISTS "DocumentInsight_insightId_fkey";
-- DropForeignKey
ALTER TABLE "public"."Insight" DROP CONSTRAINT "Insight_environmentId_fkey";
ALTER TABLE IF EXISTS "Insight" DROP CONSTRAINT IF EXISTS "Insight_environmentId_fkey";
-- DropIndex
DROP INDEX "public"."Display_responseId_key";
DROP INDEX IF EXISTS "Display_responseId_key";
-- AlterTable
ALTER TABLE "public"."Display" DROP COLUMN "responseId",
DROP COLUMN "status";
ALTER TABLE "Display" DROP COLUMN IF EXISTS "responseId",
DROP COLUMN IF EXISTS "status";
-- AlterTable
ALTER TABLE "public"."Environment" DROP COLUMN "widgetSetupCompleted";
ALTER TABLE "Environment" DROP COLUMN IF EXISTS "widgetSetupCompleted";
-- AlterTable
ALTER TABLE "public"."Invite" DROP COLUMN "deprecatedRole";
ALTER TABLE "Invite" DROP COLUMN IF EXISTS "deprecatedRole";
-- AlterTable
ALTER TABLE "public"."Membership" DROP COLUMN "deprecatedRole";
ALTER TABLE "Membership" DROP COLUMN IF EXISTS "deprecatedRole";
-- AlterTable
ALTER TABLE "public"."Project" DROP COLUMN "brandColor",
DROP COLUMN "highlightBorderColor";
ALTER TABLE "Project" DROP COLUMN IF EXISTS "brandColor",
DROP COLUMN IF EXISTS "highlightBorderColor";
-- AlterTable
ALTER TABLE "public"."Survey" DROP COLUMN "thankYouCard",
DROP COLUMN "verifyEmail",
ALTER TABLE "Survey" DROP COLUMN IF EXISTS "thankYouCard",
DROP COLUMN IF EXISTS "verifyEmail",
ALTER COLUMN "type" SET DEFAULT 'app';
-- AlterTable
ALTER TABLE "public"."User" DROP COLUMN "objective",
DROP COLUMN "role";
ALTER TABLE "User" DROP COLUMN IF EXISTS "objective",
DROP COLUMN IF EXISTS "role";
-- DropTable
DROP TABLE "public"."Document";
DROP TABLE IF EXISTS "Document";
-- DropTable
DROP TABLE "public"."DocumentInsight";
DROP TABLE IF EXISTS "DocumentInsight";
-- DropTable
DROP TABLE "public"."Insight";
DROP TABLE IF EXISTS "Insight";
-- DropEnum
DROP TYPE "public"."DisplayStatus";
DROP TYPE IF EXISTS "DisplayStatus";
-- DropEnum
DROP TYPE "public"."InsightCategory";
DROP TYPE IF EXISTS "InsightCategory";
-- DropEnum
DROP TYPE "public"."Intention";
DROP TYPE IF EXISTS "Intention";
-- DropEnum
DROP TYPE "public"."MembershipRole";
DROP TYPE IF EXISTS "MembershipRole";
-- DropEnum
DROP TYPE "public"."Objective";
DROP TYPE IF EXISTS "Objective";
-- DropEnum
DROP TYPE "public"."Role";
DROP TYPE IF EXISTS "Role";
-- DropEnum
DROP TYPE "public"."Sentiment";
DROP TYPE IF EXISTS "Sentiment";

View File

@@ -1,9 +1,9 @@
-- DropIndex
DROP INDEX IF EXISTS "public"."ApiKey_hashedKey_key";
DROP INDEX IF EXISTS "ApiKey_hashedKey_key";
-- AlterTable
ALTER TABLE "public"."ApiKey" ADD COLUMN IF NOT EXISTS "lookupHash" TEXT;
ALTER TABLE "ApiKey" ADD COLUMN IF NOT EXISTS "lookupHash" TEXT;
-- CreateIndex
CREATE UNIQUE INDEX IF NOT EXISTS "ApiKey_lookupHash_key" ON "public"."ApiKey"("lookupHash");
CREATE UNIQUE INDEX IF NOT EXISTS "ApiKey_lookupHash_key" ON "ApiKey"("lookupHash");

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,7 @@
},
"locale": {
"source": "en",
"targets": ["de", "it", "fr", "es", "ar", "pt", "ru", "uz", "ro", "ja", "zh-Hans", "hi"]
"targets": ["de", "it", "fr", "es", "ar", "pt", "ru", "uz", "ro", "ja", "zh-Hans", "hi", "nl"]
},
"version": 1.8
}

View File

@@ -0,0 +1,73 @@
{
"common": {
"and": "en",
"apply": "Toepassen",
"auto_close_wrapper": "Automatisch sluitende wrapper",
"back": "Terug",
"click_or_drag_to_upload_files": "Klik of sleep om bestanden te uploaden.",
"close_survey": "Enquête sluiten",
"company_logo": "Bedrijfslogo",
"delete_file": "Bestand verwijderen",
"file_upload": "Bestand uploaden",
"finish": "Voltooien",
"language_switch": "Taalschakelaar",
"less_than_x_minutes": "{count, plural, one {minder dan 1 minuut} other {minder dan {count} minuten}}",
"move_down": "Verplaats {item} naar beneden",
"move_up": "Verplaats {item} omhoog",
"next": "Volgende",
"open_in_new_tab": "Openen in nieuw tabblad",
"optional": "Optioneel",
"options": "Opties",
"people_responded": "{count, plural, one {1 persoon heeft gereageerd} other {{count} mensen hebben gereageerd}}",
"please_retry_now_or_try_again_later": "Probeer het nu opnieuw of probeer het later opnieuw.",
"powered_by": "Aangedreven door",
"privacy_policy": "Privacybeleid",
"protected_by_reCAPTCHA_and_the_Google": "Beschermd door reCAPTCHA en Google",
"question": "Vraag",
"question_video": "Vraagvideo",
"ranking_items": "Items rangschikken",
"respondents_will_not_see_this_card": "Respondenten zien deze kaart niet",
"retry": "Opnieuw proberen",
"select_a_date": "Selecteer een datum",
"select_for_ranking": "Selecteer {item} voor rangschikking",
"sending_responses": "Reacties verzenden...",
"takes": "Neemt",
"terms_of_service": "Servicevoorwaarden",
"the_servers_cannot_be_reached_at_the_moment": "De servers zijn momenteel niet bereikbaar.",
"they_will_be_redirected_immediately": "Ze worden onmiddellijk doorgestuurd",
"upload_files_by_clicking_or_dragging_them_here": "Upload bestanden door ze hierheen te klikken of te slepen",
"uploading": "Uploaden",
"x_minutes": "{count, plural, one {1 minuut} other {{count} minuten}}",
"x_plus_minutes": "{count}+ minuten",
"you_have_selected_x_date": "Je hebt {date} geselecteerd",
"you_have_successfully_uploaded_the_file": "Je hebt het bestand {fileName} succesvol geüpload",
"your_feedback_is_stuck": "Je feedback blijft hangen :("
},
"errors": {
"file_input": {
"duplicate_files": "De volgende bestanden zijn al geüpload: {duplicateNames}. Dubbele bestanden zijn niet toegestaan.",
"file_size_exceeded": "De volgende bestanden overschrijden de maximale grootte van {maxSizeInMB} MB en zijn verwijderd: {fileNames}",
"file_size_exceeded_alert": "Het bestand moet kleiner zijn dan {maxSizeInMB} MB",
"no_valid_file_types_selected": "Geen geldige bestandstypen geselecteerd. Selecteer een geldig bestandstype.",
"only_one_file_can_be_uploaded_at_a_time": "Er kan slechts één bestand tegelijk worden geüpload.",
"upload_failed": "Uploaden mislukt! Probeer het opnieuw.",
"you_can_only_upload_a_maximum_of_files": "Je kunt maximaal {FILE_LIMIT} bestanden uploaden."
},
"invalid_device_error": {
"message": "Schakel de spambeveiliging uit in de enquête-instellingen om dit apparaat te blijven gebruiken.",
"title": "Dit apparaat ondersteunt geen spambeveiliging."
},
"please_book_an_appointment": "Maak een afspraak",
"please_enter_a_valid_email_address": "Voer een geldig e-mailadres in",
"please_enter_a_valid_phone_number": "Voer een geldig telefoonnummer in",
"please_enter_a_valid_url": "Voer een geldige URL in",
"please_fill_out_this_field": "Vul dit veld in",
"please_rank_all_items_before_submitting": "Rangschik alle items voordat u ze verzendt",
"please_select_a_date": "Selecteer een datum",
"please_upload_a_file": "Upload een bestand",
"recaptcha_error": {
"message": "Uw reactie kan niet worden verzonden omdat deze is gemarkeerd als geautomatiseerde activiteit. Als u ademhaalt, probeer het dan opnieuw.",
"title": "We konden niet verifiëren dat je een mens bent."
}
}
}

View File

@@ -9,6 +9,7 @@ import frTranslations from "../../locales/fr.json";
import hiTranslations from "../../locales/hi.json";
import itTranslations from "../../locales/it.json";
import jaTranslations from "../../locales/ja.json";
import nlTranslations from "../../locales/nl.json";
import ptTranslations from "../../locales/pt.json";
import roTranslations from "../../locales/ro.json";
import ruTranslations from "../../locales/ru.json";
@@ -20,7 +21,7 @@ i18n
.use(initReactI18next)
.init({
fallbackLng: "en",
supportedLngs: ["en", "de", "it", "fr", "es", "ar", "pt", "ro", "ja", "ru", "uz", "zh-Hans", "hi"],
supportedLngs: ["en", "de", "it", "fr", "es", "ar", "pt", "ro", "ja", "ru", "uz", "zh-Hans", "hi", "nl"],
resources: {
en: { translation: enTranslations },
@@ -32,6 +33,7 @@ i18n
pt: { translation: ptTranslations },
ro: { translation: roTranslations },
ja: { translation: jaTranslations },
nl: { translation: nlTranslations },
ru: { translation: ruTranslations },
uz: { translation: uzTranslations },
"zh-Hans": { translation: zhHansTranslations },

View File

@@ -5,12 +5,12 @@ export const ZUserLocale = z.enum([
"de-DE",
"pt-BR",
"fr-FR",
"nl-NL",
"zh-Hant-TW",
"pt-PT",
"ro-RO",
"ja-JP",
"zh-Hans-CN",
"es-ES",
]);
export type TUserLocale = z.infer<typeof ZUserLocale>;

5351
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff