mirror of
https://github.com/formbricks/formbricks.git
synced 2026-05-14 19:38:53 -05:00
Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| da52ca0385 | |||
| aa0f916a8f | |||
| 210da98b69 | |||
| 2fc183d384 | |||
| 78fb111610 | |||
| 11c0cb4b61 | |||
| 95831f7c7f | |||
| a31e7bfaa5 | |||
| 6e35fc1769 | |||
| 48cded1646 | |||
| db752cee15 | |||
| b33aae0a73 | |||
| 72126ad736 | |||
| 4a2eeac90b |
@@ -111,27 +111,21 @@ jobs:
|
|||||||
const additions = ${{ steps.check-size.outputs.total_additions }};
|
const additions = ${{ steps.check-size.outputs.total_additions }};
|
||||||
const deletions = ${{ steps.check-size.outputs.total_deletions }};
|
const deletions = ${{ steps.check-size.outputs.total_deletions }};
|
||||||
|
|
||||||
const body = `## 🚨 PR Size Warning
|
const body = '## 🚨 PR Size Warning\n\n' +
|
||||||
|
'This PR has approximately **' + totalChanges + ' lines** of changes (' + additions + ' additions, ' + deletions + ' deletions across ' + countedFiles + ' files).\n\n' +
|
||||||
This PR has approximately **${totalChanges} lines** of changes (${additions} additions, ${deletions} deletions across ${countedFiles} files).
|
'Large PRs (>800 lines) are significantly harder to review and increase the chance of merge conflicts. Consider splitting this into smaller, self-contained PRs.\n\n' +
|
||||||
|
'### 💡 Suggestions:\n' +
|
||||||
Large PRs (>800 lines) are significantly harder to review and increase the chance of merge conflicts. Consider splitting this into smaller, self-contained PRs.
|
'- **Split by feature or module** - Break down into logical, independent pieces\n' +
|
||||||
|
'- **Create a sequence of PRs** - Each building on the previous one\n' +
|
||||||
### 💡 Suggestions:
|
'- **Branch off PR branches** - Don\'t wait for reviews to continue dependent work\n\n' +
|
||||||
- **Split by feature or module** - Break down into logical, independent pieces
|
'### 📊 What was counted:\n' +
|
||||||
- **Create a sequence of PRs** - Each building on the previous one
|
'- ✅ Source files, stylesheets, configuration files\n' +
|
||||||
- **Branch off PR branches** - Don't wait for reviews to continue dependent work
|
'- ❌ Excluded ' + excludedFiles + ' files (tests, locales, locks, generated files)\n\n' +
|
||||||
|
'### 📚 Guidelines:\n' +
|
||||||
### 📊 What was counted:
|
'- **Ideal:** 300-500 lines per PR\n' +
|
||||||
- ✅ Source files, stylesheets, configuration files
|
'- **Warning:** 500-800 lines\n' +
|
||||||
- ❌ Excluded ${excludedFiles} files (tests, locales, locks, generated files)
|
'- **Critical:** 800+ lines ⚠️\n\n' +
|
||||||
|
'If this large PR is unavoidable (e.g., migration, dependency update, major refactor), please explain in the PR description why it couldn\'t be split.';
|
||||||
### 📚 Guidelines:
|
|
||||||
- **Ideal:** 300-500 lines per PR
|
|
||||||
- **Warning:** 500-800 lines
|
|
||||||
- **Critical:** 800+ lines ⚠️
|
|
||||||
|
|
||||||
If this large PR is unavoidable (e.g., migration, dependency update, major refactor), please explain in the PR description why it couldn't be split.`;
|
|
||||||
|
|
||||||
// Check if we already commented
|
// Check if we already commented
|
||||||
const { data: comments } = await github.rest.issues.listComments({
|
const { data: comments } = await github.rest.issues.listComments({
|
||||||
|
|||||||
@@ -62,3 +62,4 @@ branch.json
|
|||||||
packages/ios/FormbricksSDK/FormbricksSDK.xcodeproj/project.xcworkspace/xcuserdata
|
packages/ios/FormbricksSDK/FormbricksSDK.xcodeproj/project.xcworkspace/xcuserdata
|
||||||
.cursorrules
|
.cursorrules
|
||||||
i18n.cache
|
i18n.cache
|
||||||
|
stats.html
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
#!/bin/sh
|
|
||||||
. "$(dirname "$0")/_/husky.sh"
|
|
||||||
|
|
||||||
# Load environment variables from .env files
|
# Load environment variables from .env files
|
||||||
if [ -f .env ]; then
|
if [ -f .env ]; then
|
||||||
set -a
|
set -a
|
||||||
|
|||||||
+1
-1
@@ -209,7 +209,7 @@ export const OrganizationBreadcrumb = ({
|
|||||||
)}
|
)}
|
||||||
{!isLoadingOrganizations && !loadError && (
|
{!isLoadingOrganizations && !loadError && (
|
||||||
<>
|
<>
|
||||||
<DropdownMenuGroup>
|
<DropdownMenuGroup className="max-h-[300px] overflow-y-auto">
|
||||||
{organizations.map((org) => (
|
{organizations.map((org) => (
|
||||||
<DropdownMenuCheckboxItem
|
<DropdownMenuCheckboxItem
|
||||||
key={org.id}
|
key={org.id}
|
||||||
|
|||||||
@@ -234,7 +234,7 @@ export const ProjectBreadcrumb = ({
|
|||||||
)}
|
)}
|
||||||
{!isLoadingProjects && !loadError && (
|
{!isLoadingProjects && !loadError && (
|
||||||
<>
|
<>
|
||||||
<DropdownMenuGroup>
|
<DropdownMenuGroup className="max-h-[300px] overflow-y-auto">
|
||||||
{projects.map((proj) => (
|
{projects.map((proj) => (
|
||||||
<DropdownMenuCheckboxItem
|
<DropdownMenuCheckboxItem
|
||||||
key={proj.id}
|
key={proj.id}
|
||||||
|
|||||||
+26
@@ -0,0 +1,26 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { ShieldCheckIcon } from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
export const SecurityListTip = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<div className="max-w-4xl">
|
||||||
|
<div className="flex items-center space-x-3 rounded-lg border border-blue-100 bg-blue-50 p-4 text-sm text-blue-900 shadow-sm">
|
||||||
|
<ShieldCheckIcon className="h-5 w-5 flex-shrink-0 text-blue-400" />
|
||||||
|
<p className="text-sm">
|
||||||
|
{t("environments.settings.general.security_list_tip")}{" "}
|
||||||
|
<Link
|
||||||
|
href="https://formbricks.com/security#stay-informed-with-formbricks-security-updates"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="underline hover:text-blue-700">
|
||||||
|
{t("environments.settings.general.security_list_tip_link")}
|
||||||
|
</Link>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
+2
@@ -12,6 +12,7 @@ import { PageHeader } from "@/modules/ui/components/page-header";
|
|||||||
import { SettingsCard } from "../../components/SettingsCard";
|
import { SettingsCard } from "../../components/SettingsCard";
|
||||||
import { DeleteOrganization } from "./components/DeleteOrganization";
|
import { DeleteOrganization } from "./components/DeleteOrganization";
|
||||||
import { EditOrganizationNameForm } from "./components/EditOrganizationNameForm";
|
import { EditOrganizationNameForm } from "./components/EditOrganizationNameForm";
|
||||||
|
import { SecurityListTip } from "./components/SecurityListTip";
|
||||||
|
|
||||||
const Page = async (props: { params: Promise<{ environmentId: string }> }) => {
|
const Page = async (props: { params: Promise<{ environmentId: string }> }) => {
|
||||||
const params = await props.params;
|
const params = await props.params;
|
||||||
@@ -48,6 +49,7 @@ const Page = async (props: { params: Promise<{ environmentId: string }> }) => {
|
|||||||
</Alert>
|
</Alert>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{!IS_FORMBRICKS_CLOUD && <SecurityListTip />}
|
||||||
<SettingsCard
|
<SettingsCard
|
||||||
title={t("environments.settings.general.organization_name")}
|
title={t("environments.settings.general.organization_name")}
|
||||||
description={t("environments.settings.general.organization_name_description")}>
|
description={t("environments.settings.general.organization_name_description")}>
|
||||||
|
|||||||
@@ -61,6 +61,10 @@ checksums:
|
|||||||
auth/signup/password_validation_uppercase_and_lowercase: ae98b485024dbff1022f6048e22443cd
|
auth/signup/password_validation_uppercase_and_lowercase: ae98b485024dbff1022f6048e22443cd
|
||||||
auth/signup/please_verify_captcha: 12938ca7ca13e3f933737dd5436fa1c0
|
auth/signup/please_verify_captcha: 12938ca7ca13e3f933737dd5436fa1c0
|
||||||
auth/signup/privacy_policy: 7459744a63ef8af4e517a09024bd7c08
|
auth/signup/privacy_policy: 7459744a63ef8af4e517a09024bd7c08
|
||||||
|
auth/signup/product_updates_description: f20eedb2cf42d2235b1fe0294086695b
|
||||||
|
auth/signup/product_updates_title: 31e099ba18abb0a49f8a75fece1f1791
|
||||||
|
auth/signup/security_updates_description: 4643df07f13cec619e7fd91c8f14d93b
|
||||||
|
auth/signup/security_updates_title: de5127f5847cdd412906607e1402f48d
|
||||||
auth/signup/terms_of_service: 5add91f519e39025708e54a7eb7a9fc5
|
auth/signup/terms_of_service: 5add91f519e39025708e54a7eb7a9fc5
|
||||||
auth/signup/title: 96addc349f834eaa5d14c786d5478b1c
|
auth/signup/title: 96addc349f834eaa5d14c786d5478b1c
|
||||||
auth/signup_without_verification_success/user_successfully_created: ff849ebedc5dacb36493d7894f16edc7
|
auth/signup_without_verification_success/user_successfully_created: ff849ebedc5dacb36493d7894f16edc7
|
||||||
@@ -954,6 +958,8 @@ checksums:
|
|||||||
environments/settings/general/remove_logo: f60f1803e6fc8017b1eae7c30089107f
|
environments/settings/general/remove_logo: f60f1803e6fc8017b1eae7c30089107f
|
||||||
environments/settings/general/replace_logo: e3c8bec7574a670607e88771164e272f
|
environments/settings/general/replace_logo: e3c8bec7574a670607e88771164e272f
|
||||||
environments/settings/general/resend_invitation_email: 6305d1ffa015c377ef59fe9c2661cf02
|
environments/settings/general/resend_invitation_email: 6305d1ffa015c377ef59fe9c2661cf02
|
||||||
|
environments/settings/general/security_list_tip: 0bbed89fa5265da7e07767087f87c736
|
||||||
|
environments/settings/general/security_list_tip_link: ccdb1a21610ebf5a626d813b155be4ba
|
||||||
environments/settings/general/share_invite_link: b40b7ffbcf02d7464be52fb562df5e3a
|
environments/settings/general/share_invite_link: b40b7ffbcf02d7464be52fb562df5e3a
|
||||||
environments/settings/general/share_this_link_to_let_your_organization_member_join_your_organization: 6eb43d5b1c855572b7ab35f527ba953c
|
environments/settings/general/share_this_link_to_let_your_organization_member_join_your_organization: 6eb43d5b1c855572b7ab35f527ba953c
|
||||||
environments/settings/general/test_email_sent_successfully: aa68214f5e0707c9615e01343640ab32
|
environments/settings/general/test_email_sent_successfully: aa68214f5e0707c9615e01343640ab32
|
||||||
|
|||||||
+6
-145
@@ -130,217 +130,78 @@ export const appLanguages = [
|
|||||||
code: "en-US",
|
code: "en-US",
|
||||||
label: {
|
label: {
|
||||||
"en-US": "English (US)",
|
"en-US": "English (US)",
|
||||||
"de-DE": "Englisch (US)",
|
|
||||||
"pt-BR": "Inglês (EUA)",
|
|
||||||
"fr-FR": "Anglais (États-Unis)",
|
|
||||||
"zh-Hant-TW": "英文 (美國)",
|
|
||||||
"pt-PT": "Inglês (EUA)",
|
|
||||||
"ro-RO": "Engleză (SUA)",
|
|
||||||
"ja-JP": "英語(米国)",
|
|
||||||
"zh-Hans-CN": "英语(美国)",
|
|
||||||
"nl-NL": "Engels (VS)",
|
|
||||||
"es-ES": "Inglés (EE.UU.)",
|
|
||||||
"sv-SE": "Engelska (USA)",
|
|
||||||
"ru-RU": "Английский (США)",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
code: "de-DE",
|
code: "de-DE",
|
||||||
label: {
|
label: {
|
||||||
"en-US": "German",
|
"en-US": "German",
|
||||||
"de-DE": "Deutsch",
|
|
||||||
"pt-BR": "Alemão",
|
|
||||||
"fr-FR": "Allemand",
|
|
||||||
"zh-Hant-TW": "德語",
|
|
||||||
"pt-PT": "Alemão",
|
|
||||||
"ro-RO": "Germană",
|
|
||||||
"ja-JP": "ドイツ語",
|
|
||||||
"zh-Hans-CN": "德语",
|
|
||||||
"nl-NL": "Duits",
|
|
||||||
"es-ES": "Alemán",
|
|
||||||
"sv-SE": "Tyska",
|
|
||||||
"ru-RU": "Немецкий",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
code: "pt-BR",
|
code: "pt-BR",
|
||||||
label: {
|
label: {
|
||||||
"en-US": "Portuguese (Brazil)",
|
"en-US": "Portuguese (Brazil)",
|
||||||
"de-DE": "Portugiesisch (Brasilien)",
|
|
||||||
"pt-BR": "Português (Brasil)",
|
|
||||||
"fr-FR": "Portugais (Brésil)",
|
|
||||||
"zh-Hant-TW": "葡萄牙語 (巴西)",
|
|
||||||
"pt-PT": "Português (Brasil)",
|
|
||||||
"ro-RO": "Portugheză (Brazilia)",
|
|
||||||
"ja-JP": "ポルトガル語(ブラジル)",
|
|
||||||
"zh-Hans-CN": "葡萄牙语(巴西)",
|
|
||||||
"nl-NL": "Portugees (Brazilië)",
|
|
||||||
"es-ES": "Portugués (Brasil)",
|
|
||||||
"sv-SE": "Portugisiska (Brasilien)",
|
|
||||||
"ru-RU": "Португальский (Бразилия)",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
code: "fr-FR",
|
code: "fr-FR",
|
||||||
label: {
|
label: {
|
||||||
"en-US": "French",
|
"en-US": "French",
|
||||||
"de-DE": "Französisch",
|
|
||||||
"pt-BR": "Francês",
|
|
||||||
"fr-FR": "Français",
|
|
||||||
"zh-Hant-TW": "法語",
|
|
||||||
"pt-PT": "Francês",
|
|
||||||
"ro-RO": "Franceză",
|
|
||||||
"ja-JP": "フランス語",
|
|
||||||
"zh-Hans-CN": "法语",
|
|
||||||
"nl-NL": "Frans",
|
|
||||||
"es-ES": "Francés",
|
|
||||||
"sv-SE": "Franska",
|
|
||||||
"ru-RU": "Французский",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
code: "zh-Hant-TW",
|
code: "zh-Hant-TW",
|
||||||
label: {
|
label: {
|
||||||
"en-US": "Chinese (Traditional)",
|
"en-US": "Chinese (Traditional)",
|
||||||
"de-DE": "Chinesisch (Traditionell)",
|
|
||||||
"pt-BR": "Chinês (Tradicional)",
|
|
||||||
"fr-FR": "Chinois (Traditionnel)",
|
|
||||||
"zh-Hant-TW": "繁體中文",
|
|
||||||
"pt-PT": "Chinês (Tradicional)",
|
|
||||||
"ro-RO": "Chineza (Tradițională)",
|
|
||||||
"ja-JP": "中国語(繁体字)",
|
|
||||||
"zh-Hans-CN": "繁体中文",
|
|
||||||
"nl-NL": "Chinees (Traditioneel)",
|
|
||||||
"es-ES": "Chino (Tradicional)",
|
|
||||||
"sv-SE": "Kinesiska (traditionell)",
|
|
||||||
"ru-RU": "Китайский (традиционный)",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
code: "pt-PT",
|
code: "pt-PT",
|
||||||
label: {
|
label: {
|
||||||
"en-US": "Portuguese (Portugal)",
|
"en-US": "Portuguese (Portugal)",
|
||||||
"de-DE": "Portugiesisch (Portugal)",
|
|
||||||
"pt-BR": "Português (Portugal)",
|
|
||||||
"fr-FR": "Portugais (Portugal)",
|
|
||||||
"zh-Hant-TW": "葡萄牙語 (葡萄牙)",
|
|
||||||
"pt-PT": "Português (Portugal)",
|
|
||||||
"ro-RO": "Portugheză (Portugalia)",
|
|
||||||
"ja-JP": "ポルトガル語(ポルトガル)",
|
|
||||||
"zh-Hans-CN": "葡萄牙语(葡萄牙)",
|
|
||||||
"nl-NL": "Portugees (Portugal)",
|
|
||||||
"es-ES": "Portugués (Portugal)",
|
|
||||||
"sv-SE": "Portugisiska (Portugal)",
|
|
||||||
"ru-RU": "Португальский (Португалия)",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
code: "ro-RO",
|
code: "ro-RO",
|
||||||
label: {
|
label: {
|
||||||
"en-US": "Romanian",
|
"en-US": "Romanian",
|
||||||
"de-DE": "Rumänisch",
|
|
||||||
"pt-BR": "Romeno",
|
|
||||||
"fr-FR": "Roumain",
|
|
||||||
"zh-Hant-TW": "羅馬尼亞語",
|
|
||||||
"pt-PT": "Romeno",
|
|
||||||
"ro-RO": "Română",
|
|
||||||
"ja-JP": "ルーマニア語",
|
|
||||||
"zh-Hans-CN": "罗马尼亚语",
|
|
||||||
"nl-NL": "Roemeens",
|
|
||||||
"es-ES": "Rumano",
|
|
||||||
"sv-SE": "Rumänska",
|
|
||||||
"ru-RU": "Румынский",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
code: "ja-JP",
|
code: "ja-JP",
|
||||||
label: {
|
label: {
|
||||||
"en-US": "Japanese",
|
"en-US": "Japanese",
|
||||||
"de-DE": "Japanisch",
|
|
||||||
"pt-BR": "Japonês",
|
|
||||||
"fr-FR": "Japonais",
|
|
||||||
"zh-Hant-TW": "日語",
|
|
||||||
"pt-PT": "Japonês",
|
|
||||||
"ro-RO": "Japoneză",
|
|
||||||
"ja-JP": "日本語",
|
|
||||||
"zh-Hans-CN": "日语",
|
|
||||||
"nl-NL": "Japans",
|
|
||||||
"es-ES": "Japonés",
|
|
||||||
"sv-SE": "Japanska",
|
|
||||||
"ru-RU": "Японский",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
code: "zh-Hans-CN",
|
code: "zh-Hans-CN",
|
||||||
label: {
|
label: {
|
||||||
"en-US": "Chinese (Simplified)",
|
"en-US": "Chinese (Simplified)",
|
||||||
"de-DE": "Chinesisch (Vereinfacht)",
|
|
||||||
"pt-BR": "Chinês (Simplificado)",
|
|
||||||
"fr-FR": "Chinois (Simplifié)",
|
|
||||||
"zh-Hant-TW": "簡體中文",
|
|
||||||
"pt-PT": "Chinês (Simplificado)",
|
|
||||||
"ro-RO": "Chineza (Simplificată)",
|
|
||||||
"ja-JP": "中国語(簡体字)",
|
|
||||||
"zh-Hans-CN": "简体中文",
|
|
||||||
"nl-NL": "Chinees (Vereenvoudigd)",
|
|
||||||
"es-ES": "Chino (Simplificado)",
|
|
||||||
"sv-SE": "Kinesiska (förenklad)",
|
|
||||||
"ru-RU": "Китайский (упрощенный)",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
code: "nl-NL",
|
code: "nl-NL",
|
||||||
label: {
|
label: {
|
||||||
"en-US": "Dutch",
|
"en-US": "Dutch",
|
||||||
"de-DE": "Niederländisch",
|
|
||||||
"pt-BR": "Holandês",
|
|
||||||
"fr-FR": "Néerlandais",
|
|
||||||
"zh-Hant-TW": "荷蘭語",
|
|
||||||
"pt-PT": "Holandês",
|
|
||||||
"ro-RO": "Olandeza",
|
|
||||||
"ja-JP": "オランダ語",
|
|
||||||
"zh-Hans-CN": "荷兰语",
|
|
||||||
"nl-NL": "Nederlands",
|
|
||||||
"es-ES": "Neerlandés",
|
|
||||||
"sv-SE": "Nederländska",
|
|
||||||
"ru-RU": "Голландский",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
code: "es-ES",
|
code: "es-ES",
|
||||||
label: {
|
label: {
|
||||||
"en-US": "Spanish",
|
"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": "西班牙语",
|
|
||||||
"nl-NL": "Spaans",
|
|
||||||
"es-ES": "Español",
|
|
||||||
"sv-SE": "Spanska",
|
|
||||||
"ru-RU": "Испанский",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
code: "sv-SE",
|
code: "sv-SE",
|
||||||
label: {
|
label: {
|
||||||
"en-US": "Swedish",
|
"en-US": "Swedish",
|
||||||
"de-DE": "Schwedisch",
|
},
|
||||||
"pt-BR": "Sueco",
|
},
|
||||||
"fr-FR": "Suédois",
|
{
|
||||||
"zh-Hant-TW": "瑞典語",
|
code: "ru-RU",
|
||||||
"pt-PT": "Sueco",
|
label: {
|
||||||
"ro-RO": "Suedeză",
|
"en-US": "Russian",
|
||||||
"ja-JP": "スウェーデン語",
|
|
||||||
"zh-Hans-CN": "瑞典语",
|
|
||||||
"nl-NL": "Zweeds",
|
|
||||||
"es-ES": "Sueco",
|
|
||||||
"sv-SE": "Svenska",
|
|
||||||
"ru-RU": "Шведский",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
export { iso639Languages };
|
|
||||||
|
|||||||
@@ -308,6 +308,10 @@ describe("Tests for updateSurvey", () => {
|
|||||||
const updatedSurvey = await updateSurvey(updateSurveyInput);
|
const updatedSurvey = await updateSurvey(updateSurveyInput);
|
||||||
expect(updatedSurvey).toEqual(mockTransformedSurveyOutput);
|
expect(updatedSurvey).toEqual(mockTransformedSurveyOutput);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Note: Language handling tests (for languages.length > 0 fix) are covered in
|
||||||
|
// apps/web/modules/survey/editor/lib/survey.test.ts where we have better control
|
||||||
|
// over the test mocks. The key fix ensures languages.length > 0 (not > 1) is used.
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Sad Path", () => {
|
describe("Sad Path", () => {
|
||||||
|
|||||||
@@ -329,7 +329,7 @@ export const updateSurveyInternal = async (
|
|||||||
? currentSurvey.languages.map((l) => l.language.id)
|
? currentSurvey.languages.map((l) => l.language.id)
|
||||||
: [];
|
: [];
|
||||||
const updatedLanguageIds =
|
const updatedLanguageIds =
|
||||||
languages.length > 1 ? updatedSurvey.languages.map((l) => l.language.id) : [];
|
languages.length > 0 ? updatedSurvey.languages.map((l) => l.language.id) : [];
|
||||||
const enabledLanguageIds = languages.map((language) => {
|
const enabledLanguageIds = languages.map((language) => {
|
||||||
if (language.enabled) return language.language.id;
|
if (language.enabled) return language.language.id;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -90,11 +90,10 @@ describe("locale", () => {
|
|||||||
// Verify sv-SE is in AVAILABLE_LOCALES
|
// Verify sv-SE is in AVAILABLE_LOCALES
|
||||||
expect(AVAILABLE_LOCALES).toContain("sv-SE");
|
expect(AVAILABLE_LOCALES).toContain("sv-SE");
|
||||||
|
|
||||||
// Verify Swedish has a language entry with proper labels
|
// Verify Swedish has a language entry with proper label
|
||||||
const swedishLanguage = appLanguages.find((lang) => lang.code === "sv-SE");
|
const swedishLanguage = appLanguages.find((lang) => lang.code === "sv-SE");
|
||||||
expect(swedishLanguage).toBeDefined();
|
expect(swedishLanguage).toBeDefined();
|
||||||
expect(swedishLanguage?.label["en-US"]).toBe("Swedish");
|
expect(swedishLanguage?.label["en-US"]).toBe("Swedish");
|
||||||
expect(swedishLanguage?.label["sv-SE"]).toBe("Svenska");
|
|
||||||
|
|
||||||
// Verify the locale can be matched from Accept-Language header
|
// Verify the locale can be matched from Accept-Language header
|
||||||
vi.mocked(nextHeaders.headers).mockReturnValue({
|
vi.mocked(nextHeaders.headers).mockReturnValue({
|
||||||
|
|||||||
@@ -75,6 +75,10 @@
|
|||||||
"password_validation_uppercase_and_lowercase": "Mix aus Groß- und Kleinbuchstaben",
|
"password_validation_uppercase_and_lowercase": "Mix aus Groß- und Kleinbuchstaben",
|
||||||
"please_verify_captcha": "Bitte bestätige reCAPTCHA",
|
"please_verify_captcha": "Bitte bestätige reCAPTCHA",
|
||||||
"privacy_policy": "Datenschutzerklärung",
|
"privacy_policy": "Datenschutzerklärung",
|
||||||
|
"product_updates_description": "Monatliche Produktneuigkeiten und Feature-Updates, es gilt die Datenschutzerklärung.",
|
||||||
|
"product_updates_title": "Produkt-Updates",
|
||||||
|
"security_updates_description": "Nur sicherheitsrelevante Informationen, es gilt die Datenschutzerklärung.",
|
||||||
|
"security_updates_title": "Sicherheits-Updates",
|
||||||
"terms_of_service": "Nutzungsbedingungen",
|
"terms_of_service": "Nutzungsbedingungen",
|
||||||
"title": "Erstelle dein Formbricks-Konto"
|
"title": "Erstelle dein Formbricks-Konto"
|
||||||
},
|
},
|
||||||
@@ -1015,6 +1019,8 @@
|
|||||||
"remove_logo": "Logo entfernen",
|
"remove_logo": "Logo entfernen",
|
||||||
"replace_logo": "Logo ersetzen",
|
"replace_logo": "Logo ersetzen",
|
||||||
"resend_invitation_email": "Einladungsemail erneut senden",
|
"resend_invitation_email": "Einladungsemail erneut senden",
|
||||||
|
"security_list_tip": "Haben Sie sich für unsere Sicherheitsliste angemeldet? Bleiben Sie informiert, um Ihre Instanz sicher zu halten!",
|
||||||
|
"security_list_tip_link": "Hier registrieren.",
|
||||||
"share_invite_link": "Einladungslink teilen",
|
"share_invite_link": "Einladungslink teilen",
|
||||||
"share_this_link_to_let_your_organization_member_join_your_organization": "Teile diesen Link, damit dein Organisationsmitglied deiner Organisation beitreten kann:",
|
"share_this_link_to_let_your_organization_member_join_your_organization": "Teile diesen Link, damit dein Organisationsmitglied deiner Organisation beitreten kann:",
|
||||||
"test_email_sent_successfully": "Test-E-Mail erfolgreich gesendet",
|
"test_email_sent_successfully": "Test-E-Mail erfolgreich gesendet",
|
||||||
@@ -1176,6 +1182,9 @@
|
|||||||
"assign": "Zuweisen =",
|
"assign": "Zuweisen =",
|
||||||
"audience": "Publikum",
|
"audience": "Publikum",
|
||||||
"auto_close_on_inactivity": "Automatisches Schließen bei Inaktivität",
|
"auto_close_on_inactivity": "Automatisches Schließen bei Inaktivität",
|
||||||
|
"auto_save_disabled": "Automatisches Speichern deaktiviert",
|
||||||
|
"auto_save_disabled_tooltip": "Ihre Umfrage wird nur im Entwurfsmodus automatisch gespeichert. So wird sichergestellt, dass öffentliche Umfragen nicht unbeabsichtigt aktualisiert werden.",
|
||||||
|
"auto_save_on": "Automatisches Speichern an",
|
||||||
"automatically_close_survey_after": "Umfrage automatisch schließen nach",
|
"automatically_close_survey_after": "Umfrage automatisch schließen nach",
|
||||||
"automatically_close_the_survey_after_a_certain_number_of_responses": "Schließe die Umfrage automatisch nach einer bestimmten Anzahl von Antworten.",
|
"automatically_close_the_survey_after_a_certain_number_of_responses": "Schließe die Umfrage automatisch nach einer bestimmten Anzahl von Antworten.",
|
||||||
"automatically_close_the_survey_if_the_user_does_not_respond_after_certain_number_of_seconds": "Schließe die Umfrage automatisch, wenn der Benutzer nach einer bestimmten Anzahl von Sekunden nicht antwortet.",
|
"automatically_close_the_survey_if_the_user_does_not_respond_after_certain_number_of_seconds": "Schließe die Umfrage automatisch, wenn der Benutzer nach einer bestimmten Anzahl von Sekunden nicht antwortet.",
|
||||||
@@ -1457,6 +1466,7 @@
|
|||||||
"please_specify": "Bitte angeben",
|
"please_specify": "Bitte angeben",
|
||||||
"prevent_double_submission": "Doppeltes Anbschicken verhindern",
|
"prevent_double_submission": "Doppeltes Anbschicken verhindern",
|
||||||
"prevent_double_submission_description": "Nur eine Antwort pro E-Mail-Adresse zulassen (beta)",
|
"prevent_double_submission_description": "Nur eine Antwort pro E-Mail-Adresse zulassen (beta)",
|
||||||
|
"progress_saved": "Fortschritt gespeichert",
|
||||||
"protect_survey_with_pin": "Umfrage mit einer PIN schützen",
|
"protect_survey_with_pin": "Umfrage mit einer PIN schützen",
|
||||||
"protect_survey_with_pin_description": "Nur Benutzer, die die PIN haben, können auf die Umfrage zugreifen.",
|
"protect_survey_with_pin_description": "Nur Benutzer, die die PIN haben, können auf die Umfrage zugreifen.",
|
||||||
"publish": "Veröffentlichen",
|
"publish": "Veröffentlichen",
|
||||||
|
|||||||
@@ -75,6 +75,10 @@
|
|||||||
"password_validation_uppercase_and_lowercase": "Mix of uppercase and lowercase",
|
"password_validation_uppercase_and_lowercase": "Mix of uppercase and lowercase",
|
||||||
"please_verify_captcha": "Please verify reCAPTCHA",
|
"please_verify_captcha": "Please verify reCAPTCHA",
|
||||||
"privacy_policy": "Privacy Policy",
|
"privacy_policy": "Privacy Policy",
|
||||||
|
"product_updates_description": "Monthly product news and feature updates, Privacy Policy applies.",
|
||||||
|
"product_updates_title": "Product updates",
|
||||||
|
"security_updates_description": "Security relevant information only, Privacy Policy applies.",
|
||||||
|
"security_updates_title": "Security updates",
|
||||||
"terms_of_service": "Terms of Service",
|
"terms_of_service": "Terms of Service",
|
||||||
"title": "Create your Formbricks account"
|
"title": "Create your Formbricks account"
|
||||||
},
|
},
|
||||||
@@ -1015,6 +1019,8 @@
|
|||||||
"remove_logo": "Remove logo",
|
"remove_logo": "Remove logo",
|
||||||
"replace_logo": "Replace logo",
|
"replace_logo": "Replace logo",
|
||||||
"resend_invitation_email": "Resend Invitation Email",
|
"resend_invitation_email": "Resend Invitation Email",
|
||||||
|
"security_list_tip": "Are you signed up for our Security List? Stay informed to keep your instance secure!",
|
||||||
|
"security_list_tip_link": "Sign up here.",
|
||||||
"share_invite_link": "Share Invite Link",
|
"share_invite_link": "Share Invite Link",
|
||||||
"share_this_link_to_let_your_organization_member_join_your_organization": "Share this link to let your organization member join your organization:",
|
"share_this_link_to_let_your_organization_member_join_your_organization": "Share this link to let your organization member join your organization:",
|
||||||
"test_email_sent_successfully": "Test email sent successfully",
|
"test_email_sent_successfully": "Test email sent successfully",
|
||||||
@@ -1176,6 +1182,9 @@
|
|||||||
"assign": "Assign =",
|
"assign": "Assign =",
|
||||||
"audience": "Audience",
|
"audience": "Audience",
|
||||||
"auto_close_on_inactivity": "Auto close on inactivity",
|
"auto_close_on_inactivity": "Auto close on inactivity",
|
||||||
|
"auto_save_disabled": "Auto-save disabled",
|
||||||
|
"auto_save_disabled_tooltip": "Your survey is only auto-saved when in draft. This assures public surveys are not unintentionally updated.",
|
||||||
|
"auto_save_on": "Auto-save on",
|
||||||
"automatically_close_survey_after": "Automatically close survey after",
|
"automatically_close_survey_after": "Automatically close survey after",
|
||||||
"automatically_close_the_survey_after_a_certain_number_of_responses": "Automatically close the survey after a certain number of responses.",
|
"automatically_close_the_survey_after_a_certain_number_of_responses": "Automatically close the survey after a certain number of responses.",
|
||||||
"automatically_close_the_survey_if_the_user_does_not_respond_after_certain_number_of_seconds": "Automatically close the survey if the user does not respond after certain number of seconds.",
|
"automatically_close_the_survey_if_the_user_does_not_respond_after_certain_number_of_seconds": "Automatically close the survey if the user does not respond after certain number of seconds.",
|
||||||
@@ -1457,6 +1466,7 @@
|
|||||||
"please_specify": "Please specify",
|
"please_specify": "Please specify",
|
||||||
"prevent_double_submission": "Prevent double submission",
|
"prevent_double_submission": "Prevent double submission",
|
||||||
"prevent_double_submission_description": "Only allow 1 response per email address",
|
"prevent_double_submission_description": "Only allow 1 response per email address",
|
||||||
|
"progress_saved": "Progress saved",
|
||||||
"protect_survey_with_pin": "Protect survey with a PIN",
|
"protect_survey_with_pin": "Protect survey with a PIN",
|
||||||
"protect_survey_with_pin_description": "Only users who have the PIN can access the survey.",
|
"protect_survey_with_pin_description": "Only users who have the PIN can access the survey.",
|
||||||
"publish": "Publish",
|
"publish": "Publish",
|
||||||
|
|||||||
@@ -75,6 +75,10 @@
|
|||||||
"password_validation_uppercase_and_lowercase": "Mezcla de mayúsculas y minúsculas",
|
"password_validation_uppercase_and_lowercase": "Mezcla de mayúsculas y minúsculas",
|
||||||
"please_verify_captcha": "Por favor, verifica el reCAPTCHA",
|
"please_verify_captcha": "Por favor, verifica el reCAPTCHA",
|
||||||
"privacy_policy": "Política de privacidad",
|
"privacy_policy": "Política de privacidad",
|
||||||
|
"product_updates_description": "Noticias mensuales del producto y actualizaciones de funciones, se aplica la política de privacidad.",
|
||||||
|
"product_updates_title": "Actualizaciones del producto",
|
||||||
|
"security_updates_description": "Solo información relevante sobre seguridad, se aplica la política de privacidad.",
|
||||||
|
"security_updates_title": "Actualizaciones de seguridad",
|
||||||
"terms_of_service": "Términos de servicio",
|
"terms_of_service": "Términos de servicio",
|
||||||
"title": "Crea tu cuenta de Formbricks"
|
"title": "Crea tu cuenta de Formbricks"
|
||||||
},
|
},
|
||||||
@@ -1015,6 +1019,8 @@
|
|||||||
"remove_logo": "Eliminar logotipo",
|
"remove_logo": "Eliminar logotipo",
|
||||||
"replace_logo": "Reemplazar logotipo",
|
"replace_logo": "Reemplazar logotipo",
|
||||||
"resend_invitation_email": "Reenviar correo electrónico de invitación",
|
"resend_invitation_email": "Reenviar correo electrónico de invitación",
|
||||||
|
"security_list_tip": "¿Estás suscrito a nuestra lista de seguridad? ¡Mantente informado para mantener tu instancia segura!",
|
||||||
|
"security_list_tip_link": "Regístrate aquí.",
|
||||||
"share_invite_link": "Compartir enlace de invitación",
|
"share_invite_link": "Compartir enlace de invitación",
|
||||||
"share_this_link_to_let_your_organization_member_join_your_organization": "Comparte este enlace para permitir que los miembros de tu organización se unan a tu organización:",
|
"share_this_link_to_let_your_organization_member_join_your_organization": "Comparte este enlace para permitir que los miembros de tu organización se unan a tu organización:",
|
||||||
"test_email_sent_successfully": "Correo electrónico de prueba enviado correctamente",
|
"test_email_sent_successfully": "Correo electrónico de prueba enviado correctamente",
|
||||||
@@ -1176,6 +1182,9 @@
|
|||||||
"assign": "Asignar =",
|
"assign": "Asignar =",
|
||||||
"audience": "Audiencia",
|
"audience": "Audiencia",
|
||||||
"auto_close_on_inactivity": "Cierre automático por inactividad",
|
"auto_close_on_inactivity": "Cierre automático por inactividad",
|
||||||
|
"auto_save_disabled": "Guardado automático desactivado",
|
||||||
|
"auto_save_disabled_tooltip": "Su encuesta solo se guarda automáticamente cuando está en borrador. Esto asegura que las encuestas públicas no se actualicen involuntariamente.",
|
||||||
|
"auto_save_on": "Guardado automático activado",
|
||||||
"automatically_close_survey_after": "Cerrar automáticamente la encuesta después de",
|
"automatically_close_survey_after": "Cerrar automáticamente la encuesta después de",
|
||||||
"automatically_close_the_survey_after_a_certain_number_of_responses": "Cerrar automáticamente la encuesta después de un cierto número de respuestas.",
|
"automatically_close_the_survey_after_a_certain_number_of_responses": "Cerrar automáticamente la encuesta después de un cierto número de respuestas.",
|
||||||
"automatically_close_the_survey_if_the_user_does_not_respond_after_certain_number_of_seconds": "Cerrar automáticamente la encuesta si el usuario no responde después de cierto número de segundos.",
|
"automatically_close_the_survey_if_the_user_does_not_respond_after_certain_number_of_seconds": "Cerrar automáticamente la encuesta si el usuario no responde después de cierto número de segundos.",
|
||||||
@@ -1457,6 +1466,7 @@
|
|||||||
"please_specify": "Por favor, especifica",
|
"please_specify": "Por favor, especifica",
|
||||||
"prevent_double_submission": "Evitar envío duplicado",
|
"prevent_double_submission": "Evitar envío duplicado",
|
||||||
"prevent_double_submission_description": "Permitir solo 1 respuesta por dirección de correo electrónico",
|
"prevent_double_submission_description": "Permitir solo 1 respuesta por dirección de correo electrónico",
|
||||||
|
"progress_saved": "Progreso guardado",
|
||||||
"protect_survey_with_pin": "Proteger encuesta con un PIN",
|
"protect_survey_with_pin": "Proteger encuesta con un PIN",
|
||||||
"protect_survey_with_pin_description": "Solo los usuarios que tengan el PIN pueden acceder a la encuesta.",
|
"protect_survey_with_pin_description": "Solo los usuarios que tengan el PIN pueden acceder a la encuesta.",
|
||||||
"publish": "Publicar",
|
"publish": "Publicar",
|
||||||
|
|||||||
@@ -75,6 +75,10 @@
|
|||||||
"password_validation_uppercase_and_lowercase": "Mélange de majuscules et de minuscules",
|
"password_validation_uppercase_and_lowercase": "Mélange de majuscules et de minuscules",
|
||||||
"please_verify_captcha": "Veuillez vérifier reCAPTCHA",
|
"please_verify_captcha": "Veuillez vérifier reCAPTCHA",
|
||||||
"privacy_policy": "Politique de confidentialité",
|
"privacy_policy": "Politique de confidentialité",
|
||||||
|
"product_updates_description": "Actualités mensuelles du produit et mises à jour des fonctionnalités, la politique de confidentialité s'applique.",
|
||||||
|
"product_updates_title": "Mises à jour du produit",
|
||||||
|
"security_updates_description": "Informations relatives à la sécurité uniquement, la politique de confidentialité s'applique.",
|
||||||
|
"security_updates_title": "Mises à jour de sécurité",
|
||||||
"terms_of_service": "Conditions d'utilisation",
|
"terms_of_service": "Conditions d'utilisation",
|
||||||
"title": "Créez votre compte Formbricks"
|
"title": "Créez votre compte Formbricks"
|
||||||
},
|
},
|
||||||
@@ -1015,6 +1019,8 @@
|
|||||||
"remove_logo": "Supprimer le logo",
|
"remove_logo": "Supprimer le logo",
|
||||||
"replace_logo": "Remplacer le logo",
|
"replace_logo": "Remplacer le logo",
|
||||||
"resend_invitation_email": "Renvoyer l'e-mail d'invitation",
|
"resend_invitation_email": "Renvoyer l'e-mail d'invitation",
|
||||||
|
"security_list_tip": "Êtes-vous inscrit à notre liste de sécurité ? Restez informé pour maintenir votre instance sécurisée !",
|
||||||
|
"security_list_tip_link": "Inscrivez-vous ici.",
|
||||||
"share_invite_link": "Partager le lien d'invitation",
|
"share_invite_link": "Partager le lien d'invitation",
|
||||||
"share_this_link_to_let_your_organization_member_join_your_organization": "Partagez ce lien pour permettre à un membre de votre organisation de rejoindre votre organisation :",
|
"share_this_link_to_let_your_organization_member_join_your_organization": "Partagez ce lien pour permettre à un membre de votre organisation de rejoindre votre organisation :",
|
||||||
"test_email_sent_successfully": "E-mail de test envoyé avec succès",
|
"test_email_sent_successfully": "E-mail de test envoyé avec succès",
|
||||||
@@ -1176,6 +1182,9 @@
|
|||||||
"assign": "Attribuer =",
|
"assign": "Attribuer =",
|
||||||
"audience": "Public",
|
"audience": "Public",
|
||||||
"auto_close_on_inactivity": "Fermeture automatique en cas d'inactivité",
|
"auto_close_on_inactivity": "Fermeture automatique en cas d'inactivité",
|
||||||
|
"auto_save_disabled": "Sauvegarde automatique désactivée",
|
||||||
|
"auto_save_disabled_tooltip": "Votre sondage n'est sauvegardé automatiquement que lorsqu'il est en brouillon. Cela garantit que les sondages publics ne sont pas mis à jour involontairement.",
|
||||||
|
"auto_save_on": "Sauvegarde automatique activée",
|
||||||
"automatically_close_survey_after": "Fermer automatiquement l'enquête après",
|
"automatically_close_survey_after": "Fermer automatiquement l'enquête après",
|
||||||
"automatically_close_the_survey_after_a_certain_number_of_responses": "Fermer automatiquement l'enquête après un certain nombre de réponses.",
|
"automatically_close_the_survey_after_a_certain_number_of_responses": "Fermer automatiquement l'enquête après un certain nombre de réponses.",
|
||||||
"automatically_close_the_survey_if_the_user_does_not_respond_after_certain_number_of_seconds": "Fermer automatiquement l'enquête si l'utilisateur ne répond pas après un certain nombre de secondes.",
|
"automatically_close_the_survey_if_the_user_does_not_respond_after_certain_number_of_seconds": "Fermer automatiquement l'enquête si l'utilisateur ne répond pas après un certain nombre de secondes.",
|
||||||
@@ -1457,6 +1466,7 @@
|
|||||||
"please_specify": "Veuillez préciser",
|
"please_specify": "Veuillez préciser",
|
||||||
"prevent_double_submission": "Empêcher la double soumission",
|
"prevent_double_submission": "Empêcher la double soumission",
|
||||||
"prevent_double_submission_description": "Autoriser uniquement 1 réponse par adresse e-mail",
|
"prevent_double_submission_description": "Autoriser uniquement 1 réponse par adresse e-mail",
|
||||||
|
"progress_saved": "Progression enregistrée",
|
||||||
"protect_survey_with_pin": "Protéger l'enquête par un code PIN",
|
"protect_survey_with_pin": "Protéger l'enquête par un code PIN",
|
||||||
"protect_survey_with_pin_description": "Seules les personnes ayant le code PIN peuvent accéder à l'enquête.",
|
"protect_survey_with_pin_description": "Seules les personnes ayant le code PIN peuvent accéder à l'enquête.",
|
||||||
"publish": "Publier",
|
"publish": "Publier",
|
||||||
|
|||||||
@@ -75,6 +75,10 @@
|
|||||||
"password_validation_uppercase_and_lowercase": "大文字と小文字を混ぜる",
|
"password_validation_uppercase_and_lowercase": "大文字と小文字を混ぜる",
|
||||||
"please_verify_captcha": "reCAPTCHAを認証してください",
|
"please_verify_captcha": "reCAPTCHAを認証してください",
|
||||||
"privacy_policy": "プライバシーポリシー",
|
"privacy_policy": "プライバシーポリシー",
|
||||||
|
"product_updates_description": "毎月の製品ニュースと機能アップデート、プライバシーポリシーが適用されます。",
|
||||||
|
"product_updates_title": "製品アップデート",
|
||||||
|
"security_updates_description": "セキュリティ関連情報のみ、プライバシーポリシーが適用されます。",
|
||||||
|
"security_updates_title": "セキュリティアップデート",
|
||||||
"terms_of_service": "利用規約",
|
"terms_of_service": "利用規約",
|
||||||
"title": "Formbricksアカウントを作成"
|
"title": "Formbricksアカウントを作成"
|
||||||
},
|
},
|
||||||
@@ -1015,6 +1019,8 @@
|
|||||||
"remove_logo": "ロゴを削除",
|
"remove_logo": "ロゴを削除",
|
||||||
"replace_logo": "ロゴを交換",
|
"replace_logo": "ロゴを交換",
|
||||||
"resend_invitation_email": "招待メールを再送信",
|
"resend_invitation_email": "招待メールを再送信",
|
||||||
|
"security_list_tip": "セキュリティリストに登録していますか?インスタンスを安全に保つために最新情報を入手しましょう!",
|
||||||
|
"security_list_tip_link": "こちらからサインアップしてください。",
|
||||||
"share_invite_link": "招待リンクを共有",
|
"share_invite_link": "招待リンクを共有",
|
||||||
"share_this_link_to_let_your_organization_member_join_your_organization": "このリンクを共有して、組織メンバーを招待できます:",
|
"share_this_link_to_let_your_organization_member_join_your_organization": "このリンクを共有して、組織メンバーを招待できます:",
|
||||||
"test_email_sent_successfully": "テストメールを正常に送信しました",
|
"test_email_sent_successfully": "テストメールを正常に送信しました",
|
||||||
@@ -1176,6 +1182,9 @@
|
|||||||
"assign": "割り当て =",
|
"assign": "割り当て =",
|
||||||
"audience": "オーディエンス",
|
"audience": "オーディエンス",
|
||||||
"auto_close_on_inactivity": "非アクティブ時に自動閉鎖",
|
"auto_close_on_inactivity": "非アクティブ時に自動閉鎖",
|
||||||
|
"auto_save_disabled": "自動保存が無効",
|
||||||
|
"auto_save_disabled_tooltip": "アンケートは下書き状態の時のみ自動保存されます。これにより、公開中のアンケートが意図せず更新されることを防ぎます。",
|
||||||
|
"auto_save_on": "自動保存オン",
|
||||||
"automatically_close_survey_after": "フォームを自動的に閉じる",
|
"automatically_close_survey_after": "フォームを自動的に閉じる",
|
||||||
"automatically_close_the_survey_after_a_certain_number_of_responses": "一定の回答数に達した後にフォームを自動的に閉じます。",
|
"automatically_close_the_survey_after_a_certain_number_of_responses": "一定の回答数に達した後にフォームを自動的に閉じます。",
|
||||||
"automatically_close_the_survey_if_the_user_does_not_respond_after_certain_number_of_seconds": "ユーザーが一定秒数応答しない場合、フォームを自動的に閉じます。",
|
"automatically_close_the_survey_if_the_user_does_not_respond_after_certain_number_of_seconds": "ユーザーが一定秒数応答しない場合、フォームを自動的に閉じます。",
|
||||||
@@ -1457,6 +1466,7 @@
|
|||||||
"please_specify": "具体的に指定してください",
|
"please_specify": "具体的に指定してください",
|
||||||
"prevent_double_submission": "二重送信を防ぐ",
|
"prevent_double_submission": "二重送信を防ぐ",
|
||||||
"prevent_double_submission_description": "メールアドレスごとに1つの回答のみを許可する",
|
"prevent_double_submission_description": "メールアドレスごとに1つの回答のみを許可する",
|
||||||
|
"progress_saved": "進捗を保存しました",
|
||||||
"protect_survey_with_pin": "PINでフォームを保護",
|
"protect_survey_with_pin": "PINでフォームを保護",
|
||||||
"protect_survey_with_pin_description": "PINを持つユーザーのみがフォームにアクセスできます。",
|
"protect_survey_with_pin_description": "PINを持つユーザーのみがフォームにアクセスできます。",
|
||||||
"publish": "公開",
|
"publish": "公開",
|
||||||
|
|||||||
@@ -75,6 +75,10 @@
|
|||||||
"password_validation_uppercase_and_lowercase": "Mix van hoofdletters en kleine letters",
|
"password_validation_uppercase_and_lowercase": "Mix van hoofdletters en kleine letters",
|
||||||
"please_verify_captcha": "Controleer reCAPTCHA",
|
"please_verify_captcha": "Controleer reCAPTCHA",
|
||||||
"privacy_policy": "Privacybeleid",
|
"privacy_policy": "Privacybeleid",
|
||||||
|
"product_updates_description": "Maandelijks productnieuws en feature-updates, privacybeleid is van toepassing.",
|
||||||
|
"product_updates_title": "Product-updates",
|
||||||
|
"security_updates_description": "Alleen beveiligingsrelevante informatie, privacybeleid is van toepassing.",
|
||||||
|
"security_updates_title": "Beveiligingsupdates",
|
||||||
"terms_of_service": "Servicevoorwaarden",
|
"terms_of_service": "Servicevoorwaarden",
|
||||||
"title": "Maak uw Formbricks-account aan"
|
"title": "Maak uw Formbricks-account aan"
|
||||||
},
|
},
|
||||||
@@ -1015,6 +1019,8 @@
|
|||||||
"remove_logo": "Logo verwijderen",
|
"remove_logo": "Logo verwijderen",
|
||||||
"replace_logo": "Logo vervangen",
|
"replace_logo": "Logo vervangen",
|
||||||
"resend_invitation_email": "Uitnodigings-e-mail opnieuw verzenden",
|
"resend_invitation_email": "Uitnodigings-e-mail opnieuw verzenden",
|
||||||
|
"security_list_tip": "Ben je aangemeld voor onze beveiligingslijst? Blijf op de hoogte om je instantie veilig te houden!",
|
||||||
|
"security_list_tip_link": "Meld je hier aan.",
|
||||||
"share_invite_link": "Deel de uitnodigingslink",
|
"share_invite_link": "Deel de uitnodigingslink",
|
||||||
"share_this_link_to_let_your_organization_member_join_your_organization": "Deel deze link om uw organisatielid lid te laten worden van uw organisatie:",
|
"share_this_link_to_let_your_organization_member_join_your_organization": "Deel deze link om uw organisatielid lid te laten worden van uw organisatie:",
|
||||||
"test_email_sent_successfully": "Test-e-mail succesvol verzonden",
|
"test_email_sent_successfully": "Test-e-mail succesvol verzonden",
|
||||||
@@ -1176,6 +1182,9 @@
|
|||||||
"assign": "Toewijzen =",
|
"assign": "Toewijzen =",
|
||||||
"audience": "Publiek",
|
"audience": "Publiek",
|
||||||
"auto_close_on_inactivity": "Automatisch sluiten bij inactiviteit",
|
"auto_close_on_inactivity": "Automatisch sluiten bij inactiviteit",
|
||||||
|
"auto_save_disabled": "Automatisch opslaan uitgeschakeld",
|
||||||
|
"auto_save_disabled_tooltip": "Uw enquête wordt alleen automatisch opgeslagen wanneer deze een concept is. Dit zorgt ervoor dat openbare enquêtes niet onbedoeld worden bijgewerkt.",
|
||||||
|
"auto_save_on": "Automatisch opslaan aan",
|
||||||
"automatically_close_survey_after": "Sluit de enquête daarna automatisch af",
|
"automatically_close_survey_after": "Sluit de enquête daarna automatisch af",
|
||||||
"automatically_close_the_survey_after_a_certain_number_of_responses": "Sluit de enquête automatisch af na een bepaald aantal reacties.",
|
"automatically_close_the_survey_after_a_certain_number_of_responses": "Sluit de enquête automatisch af na een bepaald aantal reacties.",
|
||||||
"automatically_close_the_survey_if_the_user_does_not_respond_after_certain_number_of_seconds": "Sluit de enquête automatisch af als de gebruiker na een bepaald aantal seconden niet reageert.",
|
"automatically_close_the_survey_if_the_user_does_not_respond_after_certain_number_of_seconds": "Sluit de enquête automatisch af als de gebruiker na een bepaald aantal seconden niet reageert.",
|
||||||
@@ -1457,6 +1466,7 @@
|
|||||||
"please_specify": "Gelieve te specificeren",
|
"please_specify": "Gelieve te specificeren",
|
||||||
"prevent_double_submission": "Voorkom dubbele indiening",
|
"prevent_double_submission": "Voorkom dubbele indiening",
|
||||||
"prevent_double_submission_description": "Er is slechts 1 reactie per e-mailadres toegestaan",
|
"prevent_double_submission_description": "Er is slechts 1 reactie per e-mailadres toegestaan",
|
||||||
|
"progress_saved": "Voortgang opgeslagen",
|
||||||
"protect_survey_with_pin": "Beveilig onderzoek met een pincode",
|
"protect_survey_with_pin": "Beveilig onderzoek met een pincode",
|
||||||
"protect_survey_with_pin_description": "Alleen gebruikers die de pincode hebben, hebben toegang tot de enquête.",
|
"protect_survey_with_pin_description": "Alleen gebruikers die de pincode hebben, hebben toegang tot de enquête.",
|
||||||
"publish": "Publiceren",
|
"publish": "Publiceren",
|
||||||
|
|||||||
@@ -75,6 +75,10 @@
|
|||||||
"password_validation_uppercase_and_lowercase": "mistura de maiúsculas e minúsculas",
|
"password_validation_uppercase_and_lowercase": "mistura de maiúsculas e minúsculas",
|
||||||
"please_verify_captcha": "Por favor, verifique o reCAPTCHA",
|
"please_verify_captcha": "Por favor, verifique o reCAPTCHA",
|
||||||
"privacy_policy": "Política de Privacidade",
|
"privacy_policy": "Política de Privacidade",
|
||||||
|
"product_updates_description": "Novidades mensais do produto e atualizações de recursos, a Política de Privacidade se aplica.",
|
||||||
|
"product_updates_title": "Atualizações do produto",
|
||||||
|
"security_updates_description": "Apenas informações relevantes sobre segurança, a Política de Privacidade se aplica.",
|
||||||
|
"security_updates_title": "Atualizações de segurança",
|
||||||
"terms_of_service": "Termos de Serviço",
|
"terms_of_service": "Termos de Serviço",
|
||||||
"title": "Crie sua conta no Formbricks"
|
"title": "Crie sua conta no Formbricks"
|
||||||
},
|
},
|
||||||
@@ -1015,6 +1019,8 @@
|
|||||||
"remove_logo": "Remover logo",
|
"remove_logo": "Remover logo",
|
||||||
"replace_logo": "Substituir logo",
|
"replace_logo": "Substituir logo",
|
||||||
"resend_invitation_email": "Reenviar E-mail de Convite",
|
"resend_invitation_email": "Reenviar E-mail de Convite",
|
||||||
|
"security_list_tip": "Você está inscrito na nossa Lista de Segurança? Mantenha-se informado para manter sua instância segura!",
|
||||||
|
"security_list_tip_link": "Cadastre-se aqui.",
|
||||||
"share_invite_link": "Compartilhar Link de Convite",
|
"share_invite_link": "Compartilhar Link de Convite",
|
||||||
"share_this_link_to_let_your_organization_member_join_your_organization": "Compartilhe esse link para que o membro da sua organização possa entrar na sua organização:",
|
"share_this_link_to_let_your_organization_member_join_your_organization": "Compartilhe esse link para que o membro da sua organização possa entrar na sua organização:",
|
||||||
"test_email_sent_successfully": "E-mail de teste enviado com sucesso",
|
"test_email_sent_successfully": "E-mail de teste enviado com sucesso",
|
||||||
@@ -1176,6 +1182,9 @@
|
|||||||
"assign": "atribuir =",
|
"assign": "atribuir =",
|
||||||
"audience": "Público",
|
"audience": "Público",
|
||||||
"auto_close_on_inactivity": "Fechar automaticamente por inatividade",
|
"auto_close_on_inactivity": "Fechar automaticamente por inatividade",
|
||||||
|
"auto_save_disabled": "Salvamento automático desativado",
|
||||||
|
"auto_save_disabled_tooltip": "Sua pesquisa só é salva automaticamente quando está em rascunho. Isso garante que pesquisas públicas não sejam atualizadas involuntariamente.",
|
||||||
|
"auto_save_on": "Salvamento automático ativado",
|
||||||
"automatically_close_survey_after": "Fechar pesquisa automaticamente após",
|
"automatically_close_survey_after": "Fechar pesquisa automaticamente após",
|
||||||
"automatically_close_the_survey_after_a_certain_number_of_responses": "Fechar automaticamente a pesquisa depois de um certo número de respostas.",
|
"automatically_close_the_survey_after_a_certain_number_of_responses": "Fechar automaticamente a pesquisa depois de um certo número de respostas.",
|
||||||
"automatically_close_the_survey_if_the_user_does_not_respond_after_certain_number_of_seconds": "Feche automaticamente a pesquisa se o usuário não responder depois de alguns segundos.",
|
"automatically_close_the_survey_if_the_user_does_not_respond_after_certain_number_of_seconds": "Feche automaticamente a pesquisa se o usuário não responder depois de alguns segundos.",
|
||||||
@@ -1457,6 +1466,7 @@
|
|||||||
"please_specify": "Por favor, especifique",
|
"please_specify": "Por favor, especifique",
|
||||||
"prevent_double_submission": "Evitar envio duplicado",
|
"prevent_double_submission": "Evitar envio duplicado",
|
||||||
"prevent_double_submission_description": "Permitir apenas 1 resposta por endereço de email",
|
"prevent_double_submission_description": "Permitir apenas 1 resposta por endereço de email",
|
||||||
|
"progress_saved": "Progresso salvo",
|
||||||
"protect_survey_with_pin": "Proteger pesquisa com um PIN",
|
"protect_survey_with_pin": "Proteger pesquisa com um PIN",
|
||||||
"protect_survey_with_pin_description": "Somente usuários que têm o PIN podem acessar a pesquisa.",
|
"protect_survey_with_pin_description": "Somente usuários que têm o PIN podem acessar a pesquisa.",
|
||||||
"publish": "Publicar",
|
"publish": "Publicar",
|
||||||
|
|||||||
@@ -75,6 +75,10 @@
|
|||||||
"password_validation_uppercase_and_lowercase": "Mistura de maiúsculas e minúsculas",
|
"password_validation_uppercase_and_lowercase": "Mistura de maiúsculas e minúsculas",
|
||||||
"please_verify_captcha": "Por favor, verifique o reCAPTCHA",
|
"please_verify_captcha": "Por favor, verifique o reCAPTCHA",
|
||||||
"privacy_policy": "Política de Privacidade",
|
"privacy_policy": "Política de Privacidade",
|
||||||
|
"product_updates_description": "Notícias mensais sobre o produto e atualizações de funcionalidades, aplica-se a Política de Privacidade.",
|
||||||
|
"product_updates_title": "Atualizações do produto",
|
||||||
|
"security_updates_description": "Apenas informações relevantes sobre segurança, aplica-se a Política de Privacidade.",
|
||||||
|
"security_updates_title": "Atualizações de segurança",
|
||||||
"terms_of_service": "Termos de Serviço",
|
"terms_of_service": "Termos de Serviço",
|
||||||
"title": "Crie a sua conta Formbricks"
|
"title": "Crie a sua conta Formbricks"
|
||||||
},
|
},
|
||||||
@@ -1015,6 +1019,8 @@
|
|||||||
"remove_logo": "Remover logótipo",
|
"remove_logo": "Remover logótipo",
|
||||||
"replace_logo": "Substituir logotipo",
|
"replace_logo": "Substituir logotipo",
|
||||||
"resend_invitation_email": "Reenviar Email de Convite",
|
"resend_invitation_email": "Reenviar Email de Convite",
|
||||||
|
"security_list_tip": "Está inscrito na nossa Lista de Segurança? Mantenha-se informado para manter a sua instância segura!",
|
||||||
|
"security_list_tip_link": "Inscreva-se aqui.",
|
||||||
"share_invite_link": "Partilhar Link de Convite",
|
"share_invite_link": "Partilhar Link de Convite",
|
||||||
"share_this_link_to_let_your_organization_member_join_your_organization": "Partilhe este link para permitir que o membro da sua organização se junte à sua organização:",
|
"share_this_link_to_let_your_organization_member_join_your_organization": "Partilhe este link para permitir que o membro da sua organização se junte à sua organização:",
|
||||||
"test_email_sent_successfully": "Email de teste enviado com sucesso",
|
"test_email_sent_successfully": "Email de teste enviado com sucesso",
|
||||||
@@ -1176,6 +1182,9 @@
|
|||||||
"assign": "Atribuir =",
|
"assign": "Atribuir =",
|
||||||
"audience": "Público",
|
"audience": "Público",
|
||||||
"auto_close_on_inactivity": "Fechar automaticamente por inatividade",
|
"auto_close_on_inactivity": "Fechar automaticamente por inatividade",
|
||||||
|
"auto_save_disabled": "Guardar automático desativado",
|
||||||
|
"auto_save_disabled_tooltip": "O seu inquérito só é guardado automaticamente quando está em rascunho. Isto garante que os inquéritos públicos não sejam atualizados involuntariamente.",
|
||||||
|
"auto_save_on": "Guardar automático ativado",
|
||||||
"automatically_close_survey_after": "Fechar automaticamente o inquérito após",
|
"automatically_close_survey_after": "Fechar automaticamente o inquérito após",
|
||||||
"automatically_close_the_survey_after_a_certain_number_of_responses": "Fechar automaticamente o inquérito após um certo número de respostas",
|
"automatically_close_the_survey_after_a_certain_number_of_responses": "Fechar automaticamente o inquérito após um certo número de respostas",
|
||||||
"automatically_close_the_survey_if_the_user_does_not_respond_after_certain_number_of_seconds": "Fechar automaticamente o inquérito se o utilizador não responder após um certo número de segundos.",
|
"automatically_close_the_survey_if_the_user_does_not_respond_after_certain_number_of_seconds": "Fechar automaticamente o inquérito se o utilizador não responder após um certo número de segundos.",
|
||||||
@@ -1457,6 +1466,7 @@
|
|||||||
"please_specify": "Por favor, especifique",
|
"please_specify": "Por favor, especifique",
|
||||||
"prevent_double_submission": "Impedir submissão dupla",
|
"prevent_double_submission": "Impedir submissão dupla",
|
||||||
"prevent_double_submission_description": "Permitir apenas 1 resposta por endereço de email",
|
"prevent_double_submission_description": "Permitir apenas 1 resposta por endereço de email",
|
||||||
|
"progress_saved": "Progresso guardado",
|
||||||
"protect_survey_with_pin": "Proteger inquérito com um PIN",
|
"protect_survey_with_pin": "Proteger inquérito com um PIN",
|
||||||
"protect_survey_with_pin_description": "Apenas utilizadores com o PIN podem aceder ao inquérito.",
|
"protect_survey_with_pin_description": "Apenas utilizadores com o PIN podem aceder ao inquérito.",
|
||||||
"publish": "Publicar",
|
"publish": "Publicar",
|
||||||
|
|||||||
@@ -75,6 +75,10 @@
|
|||||||
"password_validation_uppercase_and_lowercase": "Amestec de majuscule și minuscule",
|
"password_validation_uppercase_and_lowercase": "Amestec de majuscule și minuscule",
|
||||||
"please_verify_captcha": "Vă rugăm să verificați CAPTCHA",
|
"please_verify_captcha": "Vă rugăm să verificați CAPTCHA",
|
||||||
"privacy_policy": "Politica de confidențialitate",
|
"privacy_policy": "Politica de confidențialitate",
|
||||||
|
"product_updates_description": "Noutăți lunare despre produse și actualizări de funcționalități; se aplică Politica de confidențialitate.",
|
||||||
|
"product_updates_title": "Actualizări de produs",
|
||||||
|
"security_updates_description": "Doar informații relevante pentru securitate; se aplică Politica de confidențialitate.",
|
||||||
|
"security_updates_title": "Actualizări de securitate",
|
||||||
"terms_of_service": "Termeni de utilizare a serviciului",
|
"terms_of_service": "Termeni de utilizare a serviciului",
|
||||||
"title": "Creați-vă contul Formbricks"
|
"title": "Creați-vă contul Formbricks"
|
||||||
},
|
},
|
||||||
@@ -1015,6 +1019,8 @@
|
|||||||
"remove_logo": "Înlătură siglă",
|
"remove_logo": "Înlătură siglă",
|
||||||
"replace_logo": "Înlocuiește sigla",
|
"replace_logo": "Înlocuiește sigla",
|
||||||
"resend_invitation_email": "Retrimite emailul de invitație",
|
"resend_invitation_email": "Retrimite emailul de invitație",
|
||||||
|
"security_list_tip": "Ești abonat la lista noastră de securitate? Rămâi informat pentru a-ți menține instanța în siguranță!",
|
||||||
|
"security_list_tip_link": "Înscrie-te aici.",
|
||||||
"share_invite_link": "Distribuie link-ul de invitație",
|
"share_invite_link": "Distribuie link-ul de invitație",
|
||||||
"share_this_link_to_let_your_organization_member_join_your_organization": "Distribuie acest link pentru a permite membrului organizației să se alăture organizației tale:",
|
"share_this_link_to_let_your_organization_member_join_your_organization": "Distribuie acest link pentru a permite membrului organizației să se alăture organizației tale:",
|
||||||
"test_email_sent_successfully": "Email de test trimis cu succes",
|
"test_email_sent_successfully": "Email de test trimis cu succes",
|
||||||
@@ -1176,6 +1182,9 @@
|
|||||||
"assign": "Atribuire =",
|
"assign": "Atribuire =",
|
||||||
"audience": "Public",
|
"audience": "Public",
|
||||||
"auto_close_on_inactivity": "Închidere automată la inactivitate",
|
"auto_close_on_inactivity": "Închidere automată la inactivitate",
|
||||||
|
"auto_save_disabled": "Salvare automată dezactivată",
|
||||||
|
"auto_save_disabled_tooltip": "Chestionarul dvs. este salvat automat doar când este în ciornă. Acest lucru asigură că sondajele publice nu sunt actualizate neintenționat.",
|
||||||
|
"auto_save_on": "Salvare automată activată",
|
||||||
"automatically_close_survey_after": "Închideți automat sondajul după",
|
"automatically_close_survey_after": "Închideți automat sondajul după",
|
||||||
"automatically_close_the_survey_after_a_certain_number_of_responses": "Închideți automat sondajul după un număr anumit de răspunsuri.",
|
"automatically_close_the_survey_after_a_certain_number_of_responses": "Închideți automat sondajul după un număr anumit de răspunsuri.",
|
||||||
"automatically_close_the_survey_if_the_user_does_not_respond_after_certain_number_of_seconds": "Închideți automat sondajul dacă utilizatorul nu răspunde după un anumit număr de secunde.",
|
"automatically_close_the_survey_if_the_user_does_not_respond_after_certain_number_of_seconds": "Închideți automat sondajul dacă utilizatorul nu răspunde după un anumit număr de secunde.",
|
||||||
@@ -1457,6 +1466,7 @@
|
|||||||
"please_specify": "Vă rugăm să specificați",
|
"please_specify": "Vă rugăm să specificați",
|
||||||
"prevent_double_submission": "Prevenire trimitere dublă",
|
"prevent_double_submission": "Prevenire trimitere dublă",
|
||||||
"prevent_double_submission_description": "Permite doar 1 răspuns per adresă de email.",
|
"prevent_double_submission_description": "Permite doar 1 răspuns per adresă de email.",
|
||||||
|
"progress_saved": "Progres salvat",
|
||||||
"protect_survey_with_pin": "Protejați sondajul cu un PIN",
|
"protect_survey_with_pin": "Protejați sondajul cu un PIN",
|
||||||
"protect_survey_with_pin_description": "Doar utilizatorii care cunosc PIN-ul pot accesa sondajul.",
|
"protect_survey_with_pin_description": "Doar utilizatorii care cunosc PIN-ul pot accesa sondajul.",
|
||||||
"publish": "Publică",
|
"publish": "Publică",
|
||||||
|
|||||||
@@ -75,6 +75,10 @@
|
|||||||
"password_validation_uppercase_and_lowercase": "Сочетание заглавных и строчных букв",
|
"password_validation_uppercase_and_lowercase": "Сочетание заглавных и строчных букв",
|
||||||
"please_verify_captcha": "Пожалуйста, подтвердите reCAPTCHA",
|
"please_verify_captcha": "Пожалуйста, подтвердите reCAPTCHA",
|
||||||
"privacy_policy": "Политика конфиденциальности",
|
"privacy_policy": "Политика конфиденциальности",
|
||||||
|
"product_updates_description": "Ежемесячные новости о продукте и обновления функций. Применяется Политика конфиденциальности.",
|
||||||
|
"product_updates_title": "Обновления продукта",
|
||||||
|
"security_updates_description": "Только важная информация по безопасности. Применяется Политика конфиденциальности.",
|
||||||
|
"security_updates_title": "Обновления безопасности",
|
||||||
"terms_of_service": "Условия использования",
|
"terms_of_service": "Условия использования",
|
||||||
"title": "Создайте аккаунт Formbricks"
|
"title": "Создайте аккаунт Formbricks"
|
||||||
},
|
},
|
||||||
@@ -1015,6 +1019,8 @@
|
|||||||
"remove_logo": "Удалить логотип",
|
"remove_logo": "Удалить логотип",
|
||||||
"replace_logo": "Заменить логотип",
|
"replace_logo": "Заменить логотип",
|
||||||
"resend_invitation_email": "Отправить приглашение повторно",
|
"resend_invitation_email": "Отправить приглашение повторно",
|
||||||
|
"security_list_tip": "Вы подписаны на нашу рассылку по безопасности? Будьте в курсе, чтобы обезопасить свой экземпляр!",
|
||||||
|
"security_list_tip_link": "Зарегистрируйтесь здесь.",
|
||||||
"share_invite_link": "Поделиться ссылкой-приглашением",
|
"share_invite_link": "Поделиться ссылкой-приглашением",
|
||||||
"share_this_link_to_let_your_organization_member_join_your_organization": "Поделитесь этой ссылкой, чтобы участник вашей организации мог присоединиться к ней:",
|
"share_this_link_to_let_your_organization_member_join_your_organization": "Поделитесь этой ссылкой, чтобы участник вашей организации мог присоединиться к ней:",
|
||||||
"test_email_sent_successfully": "Тестовое письмо успешно отправлено",
|
"test_email_sent_successfully": "Тестовое письмо успешно отправлено",
|
||||||
@@ -1176,6 +1182,9 @@
|
|||||||
"assign": "Назначить =",
|
"assign": "Назначить =",
|
||||||
"audience": "Аудитория",
|
"audience": "Аудитория",
|
||||||
"auto_close_on_inactivity": "Автоматически закрывать при бездействии",
|
"auto_close_on_inactivity": "Автоматически закрывать при бездействии",
|
||||||
|
"auto_save_disabled": "Автосохранение отключено",
|
||||||
|
"auto_save_disabled_tooltip": "Ваш опрос автоматически сохраняется только в режиме черновика. Это гарантирует, что публичные опросы не будут случайно обновлены.",
|
||||||
|
"auto_save_on": "Автосохранение включено",
|
||||||
"automatically_close_survey_after": "Автоматически закрыть опрос через",
|
"automatically_close_survey_after": "Автоматически закрыть опрос через",
|
||||||
"automatically_close_the_survey_after_a_certain_number_of_responses": "Автоматически закрывать опрос после определённого количества ответов.",
|
"automatically_close_the_survey_after_a_certain_number_of_responses": "Автоматически закрывать опрос после определённого количества ответов.",
|
||||||
"automatically_close_the_survey_if_the_user_does_not_respond_after_certain_number_of_seconds": "Автоматически закрывать опрос, если пользователь не ответил за определённое количество секунд.",
|
"automatically_close_the_survey_if_the_user_does_not_respond_after_certain_number_of_seconds": "Автоматически закрывать опрос, если пользователь не ответил за определённое количество секунд.",
|
||||||
@@ -1457,6 +1466,7 @@
|
|||||||
"please_specify": "Пожалуйста, уточните",
|
"please_specify": "Пожалуйста, уточните",
|
||||||
"prevent_double_submission": "Предотвратить повторную отправку",
|
"prevent_double_submission": "Предотвратить повторную отправку",
|
||||||
"prevent_double_submission_description": "Разрешить только 1 ответ на один адрес электронной почты",
|
"prevent_double_submission_description": "Разрешить только 1 ответ на один адрес электронной почты",
|
||||||
|
"progress_saved": "Прогресс сохранён",
|
||||||
"protect_survey_with_pin": "Защитить опрос с помощью PIN-кода",
|
"protect_survey_with_pin": "Защитить опрос с помощью PIN-кода",
|
||||||
"protect_survey_with_pin_description": "Только пользователи, у которых есть PIN-код, могут получить доступ к опросу.",
|
"protect_survey_with_pin_description": "Только пользователи, у которых есть PIN-код, могут получить доступ к опросу.",
|
||||||
"publish": "Опубликовать",
|
"publish": "Опубликовать",
|
||||||
|
|||||||
@@ -75,6 +75,10 @@
|
|||||||
"password_validation_uppercase_and_lowercase": "Blandning av stora och små bokstäver",
|
"password_validation_uppercase_and_lowercase": "Blandning av stora och små bokstäver",
|
||||||
"please_verify_captcha": "Vänligen verifiera reCAPTCHA",
|
"please_verify_captcha": "Vänligen verifiera reCAPTCHA",
|
||||||
"privacy_policy": "Integritetspolicy",
|
"privacy_policy": "Integritetspolicy",
|
||||||
|
"product_updates_description": "Månatliga produktnyheter och funktionsuppdateringar. Integritetspolicyn gäller.",
|
||||||
|
"product_updates_title": "Produktuppdateringar",
|
||||||
|
"security_updates_description": "Endast säkerhetsrelaterad information. Integritetspolicyn gäller.",
|
||||||
|
"security_updates_title": "Säkerhetsuppdateringar",
|
||||||
"terms_of_service": "Användarvillkor",
|
"terms_of_service": "Användarvillkor",
|
||||||
"title": "Skapa ditt Formbricks-konto"
|
"title": "Skapa ditt Formbricks-konto"
|
||||||
},
|
},
|
||||||
@@ -1015,6 +1019,8 @@
|
|||||||
"remove_logo": "Ta bort logotyp",
|
"remove_logo": "Ta bort logotyp",
|
||||||
"replace_logo": "Ersätt logotyp",
|
"replace_logo": "Ersätt logotyp",
|
||||||
"resend_invitation_email": "Skicka inbjudningsmejl igen",
|
"resend_invitation_email": "Skicka inbjudningsmejl igen",
|
||||||
|
"security_list_tip": "Är du med på vår säkerhetslista? Håll dig informerad för att skydda din instans!",
|
||||||
|
"security_list_tip_link": "Registrera dig här.",
|
||||||
"share_invite_link": "Dela inbjudningslänk",
|
"share_invite_link": "Dela inbjudningslänk",
|
||||||
"share_this_link_to_let_your_organization_member_join_your_organization": "Dela denna länk för att låta din organisationsmedlem gå med i din organisation:",
|
"share_this_link_to_let_your_organization_member_join_your_organization": "Dela denna länk för att låta din organisationsmedlem gå med i din organisation:",
|
||||||
"test_email_sent_successfully": "Test-e-post skickat",
|
"test_email_sent_successfully": "Test-e-post skickat",
|
||||||
@@ -1176,6 +1182,9 @@
|
|||||||
"assign": "Tilldela =",
|
"assign": "Tilldela =",
|
||||||
"audience": "Målgrupp",
|
"audience": "Målgrupp",
|
||||||
"auto_close_on_inactivity": "Stäng automatiskt vid inaktivitet",
|
"auto_close_on_inactivity": "Stäng automatiskt vid inaktivitet",
|
||||||
|
"auto_save_disabled": "Automatisk sparning inaktiverad",
|
||||||
|
"auto_save_disabled_tooltip": "Din enkät sparas endast automatiskt när den är ett utkast. Detta säkerställer att publika enkäter inte uppdateras oavsiktligt.",
|
||||||
|
"auto_save_on": "Automatisk sparning på",
|
||||||
"automatically_close_survey_after": "Stäng enkäten automatiskt efter",
|
"automatically_close_survey_after": "Stäng enkäten automatiskt efter",
|
||||||
"automatically_close_the_survey_after_a_certain_number_of_responses": "Stäng enkäten automatiskt efter ett visst antal svar.",
|
"automatically_close_the_survey_after_a_certain_number_of_responses": "Stäng enkäten automatiskt efter ett visst antal svar.",
|
||||||
"automatically_close_the_survey_if_the_user_does_not_respond_after_certain_number_of_seconds": "Stäng enkäten automatiskt om användaren inte svarar efter ett visst antal sekunder.",
|
"automatically_close_the_survey_if_the_user_does_not_respond_after_certain_number_of_seconds": "Stäng enkäten automatiskt om användaren inte svarar efter ett visst antal sekunder.",
|
||||||
@@ -1457,6 +1466,7 @@
|
|||||||
"please_specify": "Vänligen specificera",
|
"please_specify": "Vänligen specificera",
|
||||||
"prevent_double_submission": "Förhindra dubbelinskickning",
|
"prevent_double_submission": "Förhindra dubbelinskickning",
|
||||||
"prevent_double_submission_description": "Tillåt endast 1 svar per e-postadress",
|
"prevent_double_submission_description": "Tillåt endast 1 svar per e-postadress",
|
||||||
|
"progress_saved": "Framsteg sparade",
|
||||||
"protect_survey_with_pin": "Skydda enkäten med en PIN",
|
"protect_survey_with_pin": "Skydda enkäten med en PIN",
|
||||||
"protect_survey_with_pin_description": "Endast användare som har PIN-koden kan komma åt enkäten.",
|
"protect_survey_with_pin_description": "Endast användare som har PIN-koden kan komma åt enkäten.",
|
||||||
"publish": "Publicera",
|
"publish": "Publicera",
|
||||||
|
|||||||
@@ -75,6 +75,10 @@
|
|||||||
"password_validation_uppercase_and_lowercase": "大小写混合",
|
"password_validation_uppercase_and_lowercase": "大小写混合",
|
||||||
"please_verify_captcha": "请 验证 reCAPTCHA",
|
"please_verify_captcha": "请 验证 reCAPTCHA",
|
||||||
"privacy_policy": "隐私政策",
|
"privacy_policy": "隐私政策",
|
||||||
|
"product_updates_description": "每月产品新闻和功能更新,适用隐私政策。",
|
||||||
|
"product_updates_title": "产品更新",
|
||||||
|
"security_updates_description": "仅限安全相关信息,适用隐私政策。",
|
||||||
|
"security_updates_title": "安全更新",
|
||||||
"terms_of_service": "服务条款",
|
"terms_of_service": "服务条款",
|
||||||
"title": "创建你的 Formbricks 账户"
|
"title": "创建你的 Formbricks 账户"
|
||||||
},
|
},
|
||||||
@@ -1015,6 +1019,8 @@
|
|||||||
"remove_logo": "移除 logo",
|
"remove_logo": "移除 logo",
|
||||||
"replace_logo": "替换 logo",
|
"replace_logo": "替换 logo",
|
||||||
"resend_invitation_email": "重新发送邀请邮件",
|
"resend_invitation_email": "重新发送邀请邮件",
|
||||||
|
"security_list_tip": "您已订阅我们的安全列表了吗?保持关注,保障您的实例安全!",
|
||||||
|
"security_list_tip_link": "点击此处注册。",
|
||||||
"share_invite_link": "分享邀请链接",
|
"share_invite_link": "分享邀请链接",
|
||||||
"share_this_link_to_let_your_organization_member_join_your_organization": "分享 这个 链接 以 让 你的 组织 成员 加入 你的 组织:",
|
"share_this_link_to_let_your_organization_member_join_your_organization": "分享 这个 链接 以 让 你的 组织 成员 加入 你的 组织:",
|
||||||
"test_email_sent_successfully": "测试 邮件 发送 成功",
|
"test_email_sent_successfully": "测试 邮件 发送 成功",
|
||||||
@@ -1176,6 +1182,9 @@
|
|||||||
"assign": "指派 =",
|
"assign": "指派 =",
|
||||||
"audience": "受众",
|
"audience": "受众",
|
||||||
"auto_close_on_inactivity": "自动关闭 在 无活动时",
|
"auto_close_on_inactivity": "自动关闭 在 无活动时",
|
||||||
|
"auto_save_disabled": "自动保存已禁用",
|
||||||
|
"auto_save_disabled_tooltip": "您的调查仅在草稿状态时自动保存。这确保公开的调查不会被意外更新。",
|
||||||
|
"auto_save_on": "自动保存已启用",
|
||||||
"automatically_close_survey_after": "自动 关闭 调查 后",
|
"automatically_close_survey_after": "自动 关闭 调查 后",
|
||||||
"automatically_close_the_survey_after_a_certain_number_of_responses": "自动 关闭 调查 在 达到 一定数量 的 回应 后",
|
"automatically_close_the_survey_after_a_certain_number_of_responses": "自动 关闭 调查 在 达到 一定数量 的 回应 后",
|
||||||
"automatically_close_the_survey_if_the_user_does_not_respond_after_certain_number_of_seconds": "用户未在一定秒数内应答时 自动关闭 问卷",
|
"automatically_close_the_survey_if_the_user_does_not_respond_after_certain_number_of_seconds": "用户未在一定秒数内应答时 自动关闭 问卷",
|
||||||
@@ -1457,6 +1466,7 @@
|
|||||||
"please_specify": "请 指定",
|
"please_specify": "请 指定",
|
||||||
"prevent_double_submission": "防止 重复 提交",
|
"prevent_double_submission": "防止 重复 提交",
|
||||||
"prevent_double_submission_description": "只允许每个 email 地址提供 1 个回复",
|
"prevent_double_submission_description": "只允许每个 email 地址提供 1 个回复",
|
||||||
|
"progress_saved": "进度已保存",
|
||||||
"protect_survey_with_pin": "使用 PIN 保护 调查",
|
"protect_survey_with_pin": "使用 PIN 保护 调查",
|
||||||
"protect_survey_with_pin_description": "只有 拥有 PIN 的 用户 可以 访问 调查。",
|
"protect_survey_with_pin_description": "只有 拥有 PIN 的 用户 可以 访问 调查。",
|
||||||
"publish": "发布",
|
"publish": "发布",
|
||||||
|
|||||||
@@ -75,6 +75,10 @@
|
|||||||
"password_validation_uppercase_and_lowercase": "混合使用大小寫字母",
|
"password_validation_uppercase_and_lowercase": "混合使用大小寫字母",
|
||||||
"please_verify_captcha": "請驗證 reCAPTCHA",
|
"please_verify_captcha": "請驗證 reCAPTCHA",
|
||||||
"privacy_policy": "隱私權政策",
|
"privacy_policy": "隱私權政策",
|
||||||
|
"product_updates_description": "每月產品新聞與功能更新,適用隱私權政策。",
|
||||||
|
"product_updates_title": "產品更新",
|
||||||
|
"security_updates_description": "僅限安全相關資訊,適用隱私權政策。",
|
||||||
|
"security_updates_title": "安全更新",
|
||||||
"terms_of_service": "服務條款",
|
"terms_of_service": "服務條款",
|
||||||
"title": "建立您的 Formbricks 帳戶"
|
"title": "建立您的 Formbricks 帳戶"
|
||||||
},
|
},
|
||||||
@@ -1015,6 +1019,8 @@
|
|||||||
"remove_logo": "移除標誌",
|
"remove_logo": "移除標誌",
|
||||||
"replace_logo": "取代標誌",
|
"replace_logo": "取代標誌",
|
||||||
"resend_invitation_email": "重新發送邀請電子郵件",
|
"resend_invitation_email": "重新發送邀請電子郵件",
|
||||||
|
"security_list_tip": "您已訂閱我們的安全名單了嗎?保持關注,確保您的實例安全!",
|
||||||
|
"security_list_tip_link": "請在此註冊。",
|
||||||
"share_invite_link": "分享邀請連結",
|
"share_invite_link": "分享邀請連結",
|
||||||
"share_this_link_to_let_your_organization_member_join_your_organization": "分享此連結以讓您的組織成員加入您的組織:",
|
"share_this_link_to_let_your_organization_member_join_your_organization": "分享此連結以讓您的組織成員加入您的組織:",
|
||||||
"test_email_sent_successfully": "測試電子郵件已成功發送",
|
"test_email_sent_successfully": "測試電子郵件已成功發送",
|
||||||
@@ -1176,6 +1182,9 @@
|
|||||||
"assign": "等於 =",
|
"assign": "等於 =",
|
||||||
"audience": "受眾",
|
"audience": "受眾",
|
||||||
"auto_close_on_inactivity": "非活動時自動關閉",
|
"auto_close_on_inactivity": "非活動時自動關閉",
|
||||||
|
"auto_save_disabled": "自動儲存已停用",
|
||||||
|
"auto_save_disabled_tooltip": "您的問卷僅在草稿狀態時自動儲存。這確保公開的問卷不會被意外更新。",
|
||||||
|
"auto_save_on": "自動儲存已啟用",
|
||||||
"automatically_close_survey_after": "在指定時間自動關閉問卷",
|
"automatically_close_survey_after": "在指定時間自動關閉問卷",
|
||||||
"automatically_close_the_survey_after_a_certain_number_of_responses": "在收到一定數量的回覆後自動關閉問卷。",
|
"automatically_close_the_survey_after_a_certain_number_of_responses": "在收到一定數量的回覆後自動關閉問卷。",
|
||||||
"automatically_close_the_survey_if_the_user_does_not_respond_after_certain_number_of_seconds": "如果用戶在特定秒數後未回應,則自動關閉問卷。",
|
"automatically_close_the_survey_if_the_user_does_not_respond_after_certain_number_of_seconds": "如果用戶在特定秒數後未回應,則自動關閉問卷。",
|
||||||
@@ -1457,6 +1466,7 @@
|
|||||||
"please_specify": "請指定",
|
"please_specify": "請指定",
|
||||||
"prevent_double_submission": "防止重複提交",
|
"prevent_double_submission": "防止重複提交",
|
||||||
"prevent_double_submission_description": "每個電子郵件地址僅允許 1 個回應",
|
"prevent_double_submission_description": "每個電子郵件地址僅允許 1 個回應",
|
||||||
|
"progress_saved": "進度已儲存",
|
||||||
"protect_survey_with_pin": "使用 PIN 碼保護問卷",
|
"protect_survey_with_pin": "使用 PIN 碼保護問卷",
|
||||||
"protect_survey_with_pin_description": "只有擁有 PIN 碼的使用者才能存取問卷。",
|
"protect_survey_with_pin_description": "只有擁有 PIN 碼的使用者才能存取問卷。",
|
||||||
"publish": "發布",
|
"publish": "發布",
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import { applyIPRateLimit } from "@/modules/core/rate-limit/helpers";
|
|||||||
import { rateLimitConfigs } from "@/modules/core/rate-limit/rate-limit-configs";
|
import { rateLimitConfigs } from "@/modules/core/rate-limit/rate-limit-configs";
|
||||||
import { withAuditLogging } from "@/modules/ee/audit-logs/lib/handler";
|
import { withAuditLogging } from "@/modules/ee/audit-logs/lib/handler";
|
||||||
import { getIsMultiOrgEnabled } from "@/modules/ee/license-check/lib/utils";
|
import { getIsMultiOrgEnabled } from "@/modules/ee/license-check/lib/utils";
|
||||||
|
import { subscribeUserToMailingList } from "@/modules/ee/mailing/lib/mailing-subscription";
|
||||||
import { sendInviteAcceptedEmail, sendVerificationEmail } from "@/modules/email";
|
import { sendInviteAcceptedEmail, sendVerificationEmail } from "@/modules/email";
|
||||||
|
|
||||||
const ZCreatedUser = ZUser.pick({
|
const ZCreatedUser = ZUser.pick({
|
||||||
@@ -44,6 +45,9 @@ const ZCreateUserAction = z.object({
|
|||||||
(token) => !IS_TURNSTILE_CONFIGURED || (IS_TURNSTILE_CONFIGURED && token),
|
(token) => !IS_TURNSTILE_CONFIGURED || (IS_TURNSTILE_CONFIGURED && token),
|
||||||
"CAPTCHA verification required"
|
"CAPTCHA verification required"
|
||||||
),
|
),
|
||||||
|
isFormbricksCloud: z.boolean(),
|
||||||
|
subscribeToSecurityUpdates: z.boolean().optional(),
|
||||||
|
subscribeToProductUpdates: z.boolean().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
async function verifyTurnstileIfConfigured(turnstileToken: string | undefined): Promise<void> {
|
async function verifyTurnstileIfConfigured(turnstileToken: string | undefined): Promise<void> {
|
||||||
@@ -191,6 +195,13 @@ export const createUserAction = actionClient.schema(ZCreateUserAction).action(
|
|||||||
parsedInput.inviteToken,
|
parsedInput.inviteToken,
|
||||||
parsedInput.emailVerificationDisabled
|
parsedInput.emailVerificationDisabled
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await subscribeUserToMailingList({
|
||||||
|
email: user.email,
|
||||||
|
isFormbricksCloud: parsedInput.isFormbricksCloud,
|
||||||
|
subscribeToSecurityUpdates: parsedInput.subscribeToSecurityUpdates,
|
||||||
|
subscribeToProductUpdates: parsedInput.subscribeToProductUpdates,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user) {
|
if (user) {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { createUserAction } from "@/modules/auth/signup/actions";
|
|||||||
import { TermsPrivacyLinks } from "@/modules/auth/signup/components/terms-privacy-links";
|
import { TermsPrivacyLinks } from "@/modules/auth/signup/components/terms-privacy-links";
|
||||||
import { SSOOptions } from "@/modules/ee/sso/components/sso-options";
|
import { SSOOptions } from "@/modules/ee/sso/components/sso-options";
|
||||||
import { Button } from "@/modules/ui/components/button";
|
import { Button } from "@/modules/ui/components/button";
|
||||||
|
import { Checkbox } from "@/modules/ui/components/checkbox";
|
||||||
import { FormControl, FormError, FormField, FormItem } from "@/modules/ui/components/form";
|
import { FormControl, FormError, FormField, FormItem } from "@/modules/ui/components/form";
|
||||||
import { Input } from "@/modules/ui/components/input";
|
import { Input } from "@/modules/ui/components/input";
|
||||||
import { PasswordInput } from "@/modules/ui/components/password-input";
|
import { PasswordInput } from "@/modules/ui/components/password-input";
|
||||||
@@ -48,6 +49,7 @@ interface SignupFormProps {
|
|||||||
samlTenant: string;
|
samlTenant: string;
|
||||||
samlProduct: string;
|
samlProduct: string;
|
||||||
turnstileSiteKey?: string;
|
turnstileSiteKey?: string;
|
||||||
|
isFormbricksCloud: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SignupForm = ({
|
export const SignupForm = ({
|
||||||
@@ -69,6 +71,7 @@ export const SignupForm = ({
|
|||||||
samlTenant,
|
samlTenant,
|
||||||
samlProduct,
|
samlProduct,
|
||||||
turnstileSiteKey,
|
turnstileSiteKey,
|
||||||
|
isFormbricksCloud,
|
||||||
}: SignupFormProps) => {
|
}: SignupFormProps) => {
|
||||||
const [showLogin, setShowLogin] = useState(false);
|
const [showLogin, setShowLogin] = useState(false);
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
@@ -76,6 +79,8 @@ export const SignupForm = ({
|
|||||||
const inviteToken = searchParams?.get("inviteToken");
|
const inviteToken = searchParams?.get("inviteToken");
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [turnstileToken, setTurnstileToken] = useState<string>();
|
const [turnstileToken, setTurnstileToken] = useState<string>();
|
||||||
|
const [subscribeToSecurityUpdates, setSubscribeToSecurityUpdates] = useState(false);
|
||||||
|
const [subscribeToProductUpdates, setSubscribeToProductUpdates] = useState(false);
|
||||||
|
|
||||||
const turnstile = useTurnstile();
|
const turnstile = useTurnstile();
|
||||||
|
|
||||||
@@ -110,6 +115,9 @@ export const SignupForm = ({
|
|||||||
inviteToken: inviteToken ?? "",
|
inviteToken: inviteToken ?? "",
|
||||||
emailVerificationDisabled,
|
emailVerificationDisabled,
|
||||||
turnstileToken,
|
turnstileToken,
|
||||||
|
isFormbricksCloud,
|
||||||
|
subscribeToSecurityUpdates,
|
||||||
|
subscribeToProductUpdates,
|
||||||
});
|
});
|
||||||
|
|
||||||
const emailTokenActionResponse = await createEmailTokenAction({ email: data.email });
|
const emailTokenActionResponse = await createEmailTokenAction({ email: data.email });
|
||||||
@@ -239,6 +247,43 @@ export const SignupForm = ({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{showLogin &&
|
||||||
|
(isFormbricksCloud ? (
|
||||||
|
<label
|
||||||
|
htmlFor="product-updates"
|
||||||
|
className="my-4 flex cursor-pointer space-x-2 rounded-md border border-slate-200 bg-slate-100 p-2 text-left">
|
||||||
|
<Checkbox
|
||||||
|
id="product-updates"
|
||||||
|
checked={subscribeToProductUpdates}
|
||||||
|
onCheckedChange={(checked) => setSubscribeToProductUpdates(checked === true)}
|
||||||
|
className="mt-0.5 h-4 w-4"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<span className="text-sm font-medium text-slate-700">
|
||||||
|
{t("auth.signup.product_updates_title")}
|
||||||
|
</span>
|
||||||
|
<p className="text-xs text-slate-500">{t("auth.signup.product_updates_description")}</p>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
) : (
|
||||||
|
<label
|
||||||
|
htmlFor="security-updates"
|
||||||
|
className="my-4 flex cursor-pointer space-x-2 rounded-md border border-slate-200 bg-slate-100 p-2 text-left">
|
||||||
|
<Checkbox
|
||||||
|
id="security-updates"
|
||||||
|
checked={subscribeToSecurityUpdates}
|
||||||
|
onCheckedChange={(checked) => setSubscribeToSecurityUpdates(checked === true)}
|
||||||
|
className="mt-0.5 h-4 w-4"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<span className="text-sm font-medium text-slate-700">
|
||||||
|
{t("auth.signup.security_updates_title")}
|
||||||
|
</span>
|
||||||
|
<p className="text-xs text-slate-500">{t("auth.signup.security_updates_description")}</p>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
|
||||||
{showLogin && (
|
{showLogin && (
|
||||||
<Button
|
<Button
|
||||||
data-testid="signup-submit"
|
data-testid="signup-submit"
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
EMAIL_VERIFICATION_DISABLED,
|
EMAIL_VERIFICATION_DISABLED,
|
||||||
GITHUB_OAUTH_ENABLED,
|
GITHUB_OAUTH_ENABLED,
|
||||||
GOOGLE_OAUTH_ENABLED,
|
GOOGLE_OAUTH_ENABLED,
|
||||||
|
IS_FORMBRICKS_CLOUD,
|
||||||
IS_TURNSTILE_CONFIGURED,
|
IS_TURNSTILE_CONFIGURED,
|
||||||
OIDC_DISPLAY_NAME,
|
OIDC_DISPLAY_NAME,
|
||||||
OIDC_OAUTH_ENABLED,
|
OIDC_OAUTH_ENABLED,
|
||||||
@@ -76,6 +77,7 @@ export const SignupPage = async ({ searchParams: searchParamsProps }) => {
|
|||||||
samlTenant={SAML_TENANT}
|
samlTenant={SAML_TENANT}
|
||||||
samlProduct={SAML_PRODUCT}
|
samlProduct={SAML_PRODUCT}
|
||||||
turnstileSiteKey={TURNSTILE_SITE_KEY}
|
turnstileSiteKey={TURNSTILE_SITE_KEY}
|
||||||
|
isFormbricksCloud={IS_FORMBRICKS_CLOUD}
|
||||||
/>
|
/>
|
||||||
</FormWrapper>
|
</FormWrapper>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ const CONFIG = {
|
|||||||
env.ENVIRONMENT === "staging"
|
env.ENVIRONMENT === "staging"
|
||||||
? "https://staging.ee.formbricks.com/api/licenses/check"
|
? "https://staging.ee.formbricks.com/api/licenses/check"
|
||||||
: "https://ee.formbricks.com/api/licenses/check",
|
: "https://ee.formbricks.com/api/licenses/check",
|
||||||
|
// ENDPOINT: "https://localhost:8080/api/licenses/check",
|
||||||
TIMEOUT_MS: 5000,
|
TIMEOUT_MS: 5000,
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|||||||
@@ -0,0 +1,205 @@
|
|||||||
|
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||||
|
import { logger } from "@formbricks/logger";
|
||||||
|
import { subscribeToMailingList, subscribeUserToMailingList } from "./mailing-subscription";
|
||||||
|
|
||||||
|
vi.mock("@formbricks/logger", () => ({
|
||||||
|
logger: {
|
||||||
|
error: vi.fn(),
|
||||||
|
info: vi.fn(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@/lib/utils/validate", () => ({
|
||||||
|
validateInputs: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
globalThis.fetch = vi.fn();
|
||||||
|
|
||||||
|
describe("subscribeToMailingList", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
vi.useFakeTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should successfully subscribe to security mailing list", async () => {
|
||||||
|
vi.mocked(globalThis.fetch).mockResolvedValueOnce(new Response(null, { status: 200 }));
|
||||||
|
|
||||||
|
const result = await subscribeToMailingList({
|
||||||
|
email: "test@example.com",
|
||||||
|
listId: "security",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual({ success: true });
|
||||||
|
expect(globalThis.fetch).toHaveBeenCalledWith(
|
||||||
|
"https://ee.formbricks.com/api/v1/public/mailing/security/subscriptions",
|
||||||
|
expect.objectContaining({
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ email: "test@example.com" }),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
expect(logger.info).toHaveBeenCalledWith(
|
||||||
|
{ listId: "security" },
|
||||||
|
"Successfully subscribed to security mailing list"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should successfully subscribe to product-updates mailing list", async () => {
|
||||||
|
vi.mocked(globalThis.fetch).mockResolvedValueOnce(new Response(null, { status: 200 }));
|
||||||
|
|
||||||
|
const result = await subscribeToMailingList({
|
||||||
|
email: "test@example.com",
|
||||||
|
listId: "product-updates",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual({ success: true });
|
||||||
|
expect(globalThis.fetch).toHaveBeenCalledWith(
|
||||||
|
"https://ee.formbricks.com/api/v1/public/mailing/product-updates/subscriptions",
|
||||||
|
expect.objectContaining({
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ email: "test@example.com" }),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should return error when API returns non-ok response", async () => {
|
||||||
|
vi.mocked(globalThis.fetch).mockResolvedValueOnce(
|
||||||
|
new Response("Bad Request", { status: 400, statusText: "Bad Request" })
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await subscribeToMailingList({
|
||||||
|
email: "test@example.com",
|
||||||
|
listId: "security",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual({ success: false, error: "Failed to subscribe: 400" });
|
||||||
|
expect(logger.error).toHaveBeenCalledWith(
|
||||||
|
{ status: 400, error: "Bad Request" },
|
||||||
|
"Failed to subscribe to security mailing list"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should return error when fetch throws an error", async () => {
|
||||||
|
vi.mocked(globalThis.fetch).mockRejectedValueOnce(new Error("Network error"));
|
||||||
|
|
||||||
|
const result = await subscribeToMailingList({
|
||||||
|
email: "test@example.com",
|
||||||
|
listId: "security",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual({ success: false, error: "Failed to subscribe to mailing list" });
|
||||||
|
expect(logger.error).toHaveBeenCalledWith(
|
||||||
|
expect.any(Error),
|
||||||
|
"Error subscribing to security mailing list"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should return timeout error when request times out", async () => {
|
||||||
|
const abortError = new Error("Aborted");
|
||||||
|
abortError.name = "AbortError";
|
||||||
|
vi.mocked(globalThis.fetch).mockRejectedValueOnce(abortError);
|
||||||
|
|
||||||
|
const result = await subscribeToMailingList({
|
||||||
|
email: "test@example.com",
|
||||||
|
listId: "security",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual({ success: false, error: "Request timed out" });
|
||||||
|
expect(logger.error).toHaveBeenCalledWith(
|
||||||
|
{ listId: "security" },
|
||||||
|
"Mailing subscription request timed out"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("subscribeUserToMailingList", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should subscribe to product-updates when isFormbricksCloud is true and subscribeToProductUpdates is true", async () => {
|
||||||
|
vi.mocked(globalThis.fetch).mockResolvedValueOnce(new Response(null, { status: 200 }));
|
||||||
|
|
||||||
|
await subscribeUserToMailingList({
|
||||||
|
email: "test@example.com",
|
||||||
|
isFormbricksCloud: true,
|
||||||
|
subscribeToProductUpdates: true,
|
||||||
|
subscribeToSecurityUpdates: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(globalThis.fetch).toHaveBeenCalledWith(
|
||||||
|
"https://ee.formbricks.com/api/v1/public/mailing/product-updates/subscriptions",
|
||||||
|
expect.any(Object)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should not subscribe when isFormbricksCloud is true but subscribeToProductUpdates is false", async () => {
|
||||||
|
await subscribeUserToMailingList({
|
||||||
|
email: "test@example.com",
|
||||||
|
isFormbricksCloud: true,
|
||||||
|
subscribeToProductUpdates: false,
|
||||||
|
subscribeToSecurityUpdates: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(globalThis.fetch).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should subscribe to security when isFormbricksCloud is false and subscribeToSecurityUpdates is true", async () => {
|
||||||
|
vi.mocked(globalThis.fetch).mockResolvedValueOnce(new Response(null, { status: 200 }));
|
||||||
|
|
||||||
|
await subscribeUserToMailingList({
|
||||||
|
email: "test@example.com",
|
||||||
|
isFormbricksCloud: false,
|
||||||
|
subscribeToSecurityUpdates: true,
|
||||||
|
subscribeToProductUpdates: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(globalThis.fetch).toHaveBeenCalledWith(
|
||||||
|
"https://ee.formbricks.com/api/v1/public/mailing/security/subscriptions",
|
||||||
|
expect.any(Object)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should not subscribe when isFormbricksCloud is false but subscribeToSecurityUpdates is false", async () => {
|
||||||
|
await subscribeUserToMailingList({
|
||||||
|
email: "test@example.com",
|
||||||
|
isFormbricksCloud: false,
|
||||||
|
subscribeToSecurityUpdates: false,
|
||||||
|
subscribeToProductUpdates: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(globalThis.fetch).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should not subscribe when both subscription flags are undefined", async () => {
|
||||||
|
await subscribeUserToMailingList({
|
||||||
|
email: "test@example.com",
|
||||||
|
isFormbricksCloud: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(globalThis.fetch).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should prioritize product-updates for cloud users even if security is also true", async () => {
|
||||||
|
vi.mocked(globalThis.fetch).mockResolvedValueOnce(new Response(null, { status: 200 }));
|
||||||
|
|
||||||
|
await subscribeUserToMailingList({
|
||||||
|
email: "test@example.com",
|
||||||
|
isFormbricksCloud: true,
|
||||||
|
subscribeToProductUpdates: true,
|
||||||
|
subscribeToSecurityUpdates: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Should only call product-updates endpoint for cloud users
|
||||||
|
expect(globalThis.fetch).toHaveBeenCalledTimes(1);
|
||||||
|
expect(globalThis.fetch).toHaveBeenCalledWith(
|
||||||
|
"https://ee.formbricks.com/api/v1/public/mailing/product-updates/subscriptions",
|
||||||
|
expect.any(Object)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
"use server";
|
||||||
|
|
||||||
|
import { logger } from "@formbricks/logger";
|
||||||
|
import { TUserEmail, ZUserEmail } from "@formbricks/types/user";
|
||||||
|
import { validateInputs } from "@/lib/utils/validate";
|
||||||
|
|
||||||
|
export type TMailingListId = "security" | "product-updates";
|
||||||
|
|
||||||
|
const MAILING_LIST_ENDPOINTS: Record<TMailingListId, string> = {
|
||||||
|
security: "https://ee.formbricks.com/api/v1/public/mailing/security/subscriptions",
|
||||||
|
"product-updates": "https://ee.formbricks.com/api/v1/public/mailing/product-updates/subscriptions",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const EE_SERVER_TIMEOUT_MS = 5000;
|
||||||
|
|
||||||
|
interface TSubscribeToMailingListParams {
|
||||||
|
email: TUserEmail;
|
||||||
|
listId: TMailingListId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribe a user to a mailing list via the EE server
|
||||||
|
* @param email - The user's email address
|
||||||
|
* @param listId - The mailing list ID ("security" or "product-updates")
|
||||||
|
*/
|
||||||
|
export const subscribeToMailingList = async ({
|
||||||
|
email,
|
||||||
|
listId,
|
||||||
|
}: TSubscribeToMailingListParams): Promise<{ success: boolean; error?: string }> => {
|
||||||
|
validateInputs([email, ZUserEmail.toLowerCase()]);
|
||||||
|
|
||||||
|
const endpoint = MAILING_LIST_ENDPOINTS[listId];
|
||||||
|
if (!endpoint) {
|
||||||
|
logger.error({ listId }, "Invalid mailing list ID");
|
||||||
|
return { success: false, error: "Invalid mailing list ID" };
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timeoutId = setTimeout(() => controller.abort(), EE_SERVER_TIMEOUT_MS);
|
||||||
|
|
||||||
|
const response = await fetch(endpoint, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ email }),
|
||||||
|
signal: controller.signal,
|
||||||
|
});
|
||||||
|
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorText = await response.text();
|
||||||
|
logger.error(
|
||||||
|
{ status: response.status, error: errorText },
|
||||||
|
`Failed to subscribe to ${listId} mailing list`
|
||||||
|
);
|
||||||
|
return { success: false, error: `Failed to subscribe: ${response.status}` };
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info({ listId }, `Successfully subscribed to ${listId} mailing list`);
|
||||||
|
return { success: true };
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error && error.name === "AbortError") {
|
||||||
|
logger.error({ listId }, "Mailing subscription request timed out");
|
||||||
|
return { success: false, error: "Request timed out" };
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.error(error, `Error subscribing to ${listId} mailing list`);
|
||||||
|
return { success: false, error: "Failed to subscribe to mailing list" };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const subscribeUserToMailingList = async ({
|
||||||
|
email,
|
||||||
|
isFormbricksCloud,
|
||||||
|
subscribeToSecurityUpdates,
|
||||||
|
subscribeToProductUpdates,
|
||||||
|
}: {
|
||||||
|
email: TUserEmail;
|
||||||
|
isFormbricksCloud: boolean;
|
||||||
|
subscribeToSecurityUpdates?: boolean;
|
||||||
|
subscribeToProductUpdates?: boolean;
|
||||||
|
}): Promise<void> => {
|
||||||
|
if (isFormbricksCloud && subscribeToProductUpdates) {
|
||||||
|
await subscribeToMailingList({ email, listId: "product-updates" });
|
||||||
|
} else if (!isFormbricksCloud && subscribeToSecurityUpdates) {
|
||||||
|
await subscribeToMailingList({ email, listId: "security" });
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -255,7 +255,7 @@ export const AddApiKeyModal = ({
|
|||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent className="min-w-[8rem]">
|
<DropdownMenuContent className="max-h-[300px] min-w-[8rem] overflow-y-auto">
|
||||||
{projectOptions.map((option) => (
|
{projectOptions.map((option) => (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
key={option.id}
|
key={option.id}
|
||||||
@@ -286,7 +286,7 @@ export const AddApiKeyModal = ({
|
|||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent className="min-w-[8rem] capitalize">
|
<DropdownMenuContent className="max-h-[300px] min-w-[8rem] overflow-y-auto capitalize">
|
||||||
{getEnvironmentOptionsForProject(permission.projectId).map((env) => (
|
{getEnvironmentOptionsForProject(permission.projectId).map((env) => (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
key={env.id}
|
key={env.id}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
EMAIL_VERIFICATION_DISABLED,
|
EMAIL_VERIFICATION_DISABLED,
|
||||||
GITHUB_OAUTH_ENABLED,
|
GITHUB_OAUTH_ENABLED,
|
||||||
GOOGLE_OAUTH_ENABLED,
|
GOOGLE_OAUTH_ENABLED,
|
||||||
|
IS_FORMBRICKS_CLOUD,
|
||||||
IS_TURNSTILE_CONFIGURED,
|
IS_TURNSTILE_CONFIGURED,
|
||||||
OIDC_DISPLAY_NAME,
|
OIDC_DISPLAY_NAME,
|
||||||
OIDC_OAUTH_ENABLED,
|
OIDC_OAUTH_ENABLED,
|
||||||
@@ -57,6 +58,7 @@ export const SignupPage = async () => {
|
|||||||
samlTenant={SAML_TENANT}
|
samlTenant={SAML_TENANT}
|
||||||
samlProduct={SAML_PRODUCT}
|
samlProduct={SAML_PRODUCT}
|
||||||
turnstileSiteKey={TURNSTILE_SITE_KEY}
|
turnstileSiteKey={TURNSTILE_SITE_KEY}
|
||||||
|
isFormbricksCloud={IS_FORMBRICKS_CLOUD}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useMemo, useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { cn } from "@/lib/cn";
|
||||||
|
import { TooltipRenderer } from "@/modules/ui/components/tooltip";
|
||||||
|
|
||||||
|
interface AutoSaveIndicatorProps {
|
||||||
|
isDraft: boolean;
|
||||||
|
lastSaved: Date | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AutoSaveIndicator = ({ isDraft, lastSaved }: AutoSaveIndicatorProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [showSaved, setShowSaved] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (lastSaved) {
|
||||||
|
setShowSaved(true);
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
setShowSaved(false);
|
||||||
|
}, 3000);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}, [lastSaved]);
|
||||||
|
|
||||||
|
const isSavedState = isDraft && showSaved;
|
||||||
|
|
||||||
|
const text = useMemo(() => {
|
||||||
|
if (!isDraft) {
|
||||||
|
return t("environments.surveys.edit.auto_save_disabled");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showSaved) {
|
||||||
|
return t("environments.surveys.edit.progress_saved");
|
||||||
|
}
|
||||||
|
|
||||||
|
return t("environments.surveys.edit.auto_save_on");
|
||||||
|
}, [isDraft, showSaved, t]);
|
||||||
|
|
||||||
|
const badge = (
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"inline-flex cursor-default items-center rounded-full border px-2.5 py-0.5 text-xs font-medium transition-colors duration-300",
|
||||||
|
isSavedState
|
||||||
|
? "border-green-600 bg-green-50 text-green-800"
|
||||||
|
: "border-slate-200 bg-slate-100 text-slate-600"
|
||||||
|
)}>
|
||||||
|
{text}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TooltipRenderer
|
||||||
|
shouldRender={!isDraft}
|
||||||
|
tooltipContent={t("environments.surveys.edit.auto_save_disabled_tooltip")}
|
||||||
|
className="max-w-64 text-center">
|
||||||
|
{badge}
|
||||||
|
</TooltipRenderer>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -26,6 +26,7 @@ import { Button } from "@/modules/ui/components/button";
|
|||||||
import { Input } from "@/modules/ui/components/input";
|
import { Input } from "@/modules/ui/components/input";
|
||||||
import { updateSurveyAction, updateSurveyDraftAction } from "../actions";
|
import { updateSurveyAction, updateSurveyDraftAction } from "../actions";
|
||||||
import { isSurveyValid } from "../lib/validation";
|
import { isSurveyValid } from "../lib/validation";
|
||||||
|
import { AutoSaveIndicator } from "./auto-save-indicator";
|
||||||
|
|
||||||
interface SurveyMenuBarProps {
|
interface SurveyMenuBarProps {
|
||||||
localSurvey: TSurvey;
|
localSurvey: TSurvey;
|
||||||
@@ -68,7 +69,14 @@ export const SurveyMenuBar = ({
|
|||||||
const [isConfirmDialogOpen, setConfirmDialogOpen] = useState(false);
|
const [isConfirmDialogOpen, setConfirmDialogOpen] = useState(false);
|
||||||
const [isSurveyPublishing, setIsSurveyPublishing] = useState(false);
|
const [isSurveyPublishing, setIsSurveyPublishing] = useState(false);
|
||||||
const [isSurveySaving, setIsSurveySaving] = useState(false);
|
const [isSurveySaving, setIsSurveySaving] = useState(false);
|
||||||
|
const [lastAutoSaved, setLastAutoSaved] = useState<Date | null>(null);
|
||||||
const isSuccessfullySavedRef = useRef(false);
|
const isSuccessfullySavedRef = useRef(false);
|
||||||
|
const isAutoSavingRef = useRef(false);
|
||||||
|
|
||||||
|
// Refs for interval-based auto-save (to access current values without re-creating interval)
|
||||||
|
const localSurveyRef = useRef(localSurvey);
|
||||||
|
const surveyRef = useRef(survey);
|
||||||
|
const isSurveySavingRef = useRef(isSurveySaving);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (audiencePrompt && activeId === "settings") {
|
if (audiencePrompt && activeId === "settings") {
|
||||||
@@ -80,6 +88,19 @@ export const SurveyMenuBar = ({
|
|||||||
setIsLinkSurvey(localSurvey.type === "link");
|
setIsLinkSurvey(localSurvey.type === "link");
|
||||||
}, [localSurvey.type]);
|
}, [localSurvey.type]);
|
||||||
|
|
||||||
|
// Keep refs updated for interval-based auto-save
|
||||||
|
useEffect(() => {
|
||||||
|
localSurveyRef.current = localSurvey;
|
||||||
|
}, [localSurvey]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
surveyRef.current = survey;
|
||||||
|
}, [survey]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
isSurveySavingRef.current = isSurveySaving;
|
||||||
|
}, [isSurveySaving]);
|
||||||
|
|
||||||
// Reset the successfully saved flag when survey prop updates (page refresh complete)
|
// Reset the successfully saved flag when survey prop updates (page refresh complete)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isSuccessfullySavedRef.current) {
|
if (isSuccessfullySavedRef.current) {
|
||||||
@@ -228,6 +249,52 @@ export const SurveyMenuBar = ({
|
|||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Interval-based auto-save for draft surveys (every 10 seconds)
|
||||||
|
useEffect(() => {
|
||||||
|
// Only set up interval for draft surveys
|
||||||
|
if (localSurvey.status !== "draft") return;
|
||||||
|
|
||||||
|
const intervalId = setInterval(async () => {
|
||||||
|
// Skip if tab is not visible (no computation, no API calls for background tabs)
|
||||||
|
if (document.hidden) return;
|
||||||
|
|
||||||
|
// Skip if already saving (manual or auto)
|
||||||
|
if (isAutoSavingRef.current || isSurveySavingRef.current) return;
|
||||||
|
|
||||||
|
// Check for changes using refs (avoids re-creating interval on every change)
|
||||||
|
const { updatedAt: localUpdatedAt, ...localSurveyRest } = localSurveyRef.current;
|
||||||
|
const { updatedAt: surveyUpdatedAt, ...surveyRest } = surveyRef.current;
|
||||||
|
|
||||||
|
// Skip if no changes
|
||||||
|
if (isEqual(localSurveyRest, surveyRest)) return;
|
||||||
|
|
||||||
|
isAutoSavingRef.current = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const currentSurvey = localSurveyRef.current;
|
||||||
|
const updatedSurveyResponse = await updateSurveyDraftAction({
|
||||||
|
...currentSurvey,
|
||||||
|
segment: currentSurvey.segment?.id === "temp" ? null : currentSurvey.segment,
|
||||||
|
} as unknown as TSurveyDraft);
|
||||||
|
|
||||||
|
if (updatedSurveyResponse?.data) {
|
||||||
|
// Update surveyRef (not localSurvey state) to prevent re-renders during auto-save.
|
||||||
|
// This keeps the UI stable while still tracking that changes have been saved.
|
||||||
|
// The comparison uses refs, so this prevents unnecessary re-saves.
|
||||||
|
surveyRef.current = { ...updatedSurveyResponse.data };
|
||||||
|
isSuccessfullySavedRef.current = true;
|
||||||
|
setLastAutoSaved(new Date());
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
} finally {
|
||||||
|
isAutoSavingRef.current = false;
|
||||||
|
}
|
||||||
|
}, 10000);
|
||||||
|
|
||||||
|
return () => clearInterval(intervalId);
|
||||||
|
}, [localSurvey.status]);
|
||||||
|
|
||||||
// Add new handler after handleSurveySave
|
// Add new handler after handleSurveySave
|
||||||
const handleSurveySaveDraft = async (): Promise<boolean> => {
|
const handleSurveySaveDraft = async (): Promise<boolean> => {
|
||||||
setIsSurveySaving(true);
|
setIsSurveySaving(true);
|
||||||
@@ -401,6 +468,7 @@ export const SurveyMenuBar = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-3 flex items-center gap-2 sm:mt-0 sm:ml-4">
|
<div className="mt-3 flex items-center gap-2 sm:mt-0 sm:ml-4">
|
||||||
|
<AutoSaveIndicator isDraft={localSurvey.status === "draft"} lastSaved={lastAutoSaved} />
|
||||||
{!isStorageConfigured && (
|
{!isStorageConfigured && (
|
||||||
<div>
|
<div>
|
||||||
<Alert variant="warning" size="small">
|
<Alert variant="warning" size="small">
|
||||||
@@ -427,6 +495,7 @@ export const SurveyMenuBar = ({
|
|||||||
)}
|
)}
|
||||||
{!isCxMode && (
|
{!isCxMode && (
|
||||||
<Button
|
<Button
|
||||||
|
data-save-button
|
||||||
disabled={disableSave}
|
disabled={disableSave}
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|||||||
@@ -168,7 +168,7 @@ describe("Survey Editor Library Tests", () => {
|
|||||||
vi.mocked(getOrganizationAIKeys).mockResolvedValue(mockOrganization as any);
|
vi.mocked(getOrganizationAIKeys).mockResolvedValue(mockOrganization as any);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should handle languages update", async () => {
|
test("should handle languages update with multiple languages", async () => {
|
||||||
const updatedSurvey: TSurvey = {
|
const updatedSurvey: TSurvey = {
|
||||||
...mockSurvey,
|
...mockSurvey,
|
||||||
languages: [
|
languages: [
|
||||||
@@ -219,6 +219,60 @@ describe("Survey Editor Library Tests", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("should handle languages update with single default language", async () => {
|
||||||
|
// This tests the fix for the bug where languages.length === 1 would incorrectly
|
||||||
|
// set updatedLanguageIds to [] causing the default language to be removed
|
||||||
|
const updatedSurvey: TSurvey = {
|
||||||
|
...mockSurvey,
|
||||||
|
languages: [
|
||||||
|
{
|
||||||
|
language: {
|
||||||
|
id: "en",
|
||||||
|
code: "en",
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
alias: null,
|
||||||
|
projectId: "project1",
|
||||||
|
},
|
||||||
|
default: true,
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
await updateSurvey(updatedSurvey);
|
||||||
|
|
||||||
|
// Verify that prisma.survey.update was called
|
||||||
|
expect(prisma.survey.update).toHaveBeenCalled();
|
||||||
|
|
||||||
|
const updateCall = vi.mocked(prisma.survey.update).mock.calls[0][0];
|
||||||
|
|
||||||
|
// The key test: when languages.length === 1, we should still process language updates
|
||||||
|
// and NOT delete the language. Before the fix, languages.length > 1 would fail this case.
|
||||||
|
expect(updateCall).toBeDefined();
|
||||||
|
expect(updateCall.where).toEqual({ id: "survey123" });
|
||||||
|
expect(updateCall.data).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should remove all languages when empty array is passed", async () => {
|
||||||
|
const updatedSurvey: TSurvey = {
|
||||||
|
...mockSurvey,
|
||||||
|
languages: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
await updateSurvey(updatedSurvey);
|
||||||
|
|
||||||
|
// Verify that prisma.survey.update was called
|
||||||
|
expect(prisma.survey.update).toHaveBeenCalled();
|
||||||
|
|
||||||
|
const updateCall = vi.mocked(prisma.survey.update).mock.calls[0][0];
|
||||||
|
|
||||||
|
// When languages is empty array, all existing languages should be removed
|
||||||
|
expect(updateCall).toBeDefined();
|
||||||
|
expect(updateCall.where).toEqual({ id: "survey123" });
|
||||||
|
expect(updateCall.data).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
test("should delete private segment for non-app type surveys", async () => {
|
test("should delete private segment for non-app type surveys", async () => {
|
||||||
const mockSegment: TSegment = {
|
const mockSegment: TSegment = {
|
||||||
id: "segment1",
|
id: "segment1",
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export const updateSurvey = async (updatedSurvey: TSurvey): Promise<TSurvey> =>
|
|||||||
? currentSurvey.languages.map((l) => l.language.id)
|
? currentSurvey.languages.map((l) => l.language.id)
|
||||||
: [];
|
: [];
|
||||||
const updatedLanguageIds =
|
const updatedLanguageIds =
|
||||||
languages.length > 1 ? updatedSurvey.languages.map((l) => l.language.id) : [];
|
languages.length > 0 ? updatedSurvey.languages.map((l) => l.language.id) : [];
|
||||||
const enabledLanguageIds = languages.map((language) => {
|
const enabledLanguageIds = languages.map((language) => {
|
||||||
if (language.enabled) return language.language.id;
|
if (language.enabled) return language.language.id;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -64,11 +64,13 @@
|
|||||||
|
|
||||||
*::-webkit-scrollbar-track {
|
*::-webkit-scrollbar-track {
|
||||||
background: #e2e8f0;
|
background: #e2e8f0;
|
||||||
|
border-radius: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
*::-webkit-scrollbar-thumb {
|
*::-webkit-scrollbar-thumb {
|
||||||
background-color: #cbd5e1;
|
background-color: #cbd5e1;
|
||||||
border: 1px solid #cbd5e1;
|
border: 1px solid #cbd5e1;
|
||||||
|
border-radius: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-scrollbar::-webkit-scrollbar {
|
.filter-scrollbar::-webkit-scrollbar {
|
||||||
|
|||||||
@@ -46,7 +46,7 @@
|
|||||||
"@lexical/rich-text": "0.36.2",
|
"@lexical/rich-text": "0.36.2",
|
||||||
"@lexical/table": "0.36.2",
|
"@lexical/table": "0.36.2",
|
||||||
"@opentelemetry/exporter-prometheus": "0.203.0",
|
"@opentelemetry/exporter-prometheus": "0.203.0",
|
||||||
"@opentelemetry/host-metrics": "0.36.0",
|
"@opentelemetry/host-metrics": "0.38.0",
|
||||||
"@opentelemetry/instrumentation": "0.203.0",
|
"@opentelemetry/instrumentation": "0.203.0",
|
||||||
"@opentelemetry/instrumentation-http": "0.203.0",
|
"@opentelemetry/instrumentation-http": "0.203.0",
|
||||||
"@opentelemetry/instrumentation-runtime-node": "0.17.1",
|
"@opentelemetry/instrumentation-runtime-node": "0.17.1",
|
||||||
|
|||||||
+22
-10
@@ -3,11 +3,11 @@ x-environment: &environment
|
|||||||
######################################################## REQUIRED ########################################################
|
######################################################## REQUIRED ########################################################
|
||||||
|
|
||||||
# The url of your Formbricks instance used in the admin panel
|
# The url of your Formbricks instance used in the admin panel
|
||||||
# Set this to your public-facing URL, e.g., https://example.com
|
# Set this to your public-facing URL, e.g., example http://localhost:3000 or https://example.com
|
||||||
WEBAPP_URL:
|
WEBAPP_URL:
|
||||||
|
|
||||||
# Required for next-auth. Should be the same as WEBAPP_URL
|
# Required for next-auth. Should be the same as WEBAPP_URL
|
||||||
NEXTAUTH_URL:
|
NEXTAUTH_URL:
|
||||||
|
|
||||||
# PostgreSQL DB for Formbricks to connect to
|
# PostgreSQL DB for Formbricks to connect to
|
||||||
DATABASE_URL: "postgresql://postgres:postgres@postgres:5432/formbricks?schema=public"
|
DATABASE_URL: "postgresql://postgres:postgres@postgres:5432/formbricks?schema=public"
|
||||||
@@ -15,15 +15,15 @@ x-environment: &environment
|
|||||||
# NextJS Auth
|
# NextJS Auth
|
||||||
# @see: https://next-auth.js.org/configuration/options#nextauth_secret
|
# @see: https://next-auth.js.org/configuration/options#nextauth_secret
|
||||||
# You can use: `openssl rand -hex 32` to generate one
|
# You can use: `openssl rand -hex 32` to generate one
|
||||||
NEXTAUTH_SECRET:
|
NEXTAUTH_SECRET:
|
||||||
|
|
||||||
# Encryption Key is used for 2FA & Single use URLs for Link Surveys
|
# Encryption Key is used for 2FA & Single use URLs for Link Surveys
|
||||||
# You can use: $(openssl rand -hex 32) to generate one
|
# You can use: $(openssl rand -hex 32) to generate one
|
||||||
ENCRYPTION_KEY:
|
ENCRYPTION_KEY:
|
||||||
|
|
||||||
# API Secret for running cron jobs.
|
# API Secret for running cron jobs.
|
||||||
# You can use: $(openssl rand -hex 32) to generate a secure one
|
# You can use: $(openssl rand -hex 32) to generate a secure one
|
||||||
CRON_SECRET:
|
CRON_SECRET:
|
||||||
|
|
||||||
# Redis URL for caching, rate limiting, and audit logging
|
# Redis URL for caching, rate limiting, and audit logging
|
||||||
# To use external Redis/Valkey: remove the redis service below and update this URL
|
# To use external Redis/Valkey: remove the redis service below and update this URL
|
||||||
@@ -201,9 +201,13 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- postgres:/var/lib/postgresql/data
|
- postgres:/var/lib/postgresql/data
|
||||||
environment:
|
environment:
|
||||||
# Postgres DB Super User Password
|
|
||||||
# Replace the below with your own secure password & Make sure the password matches the password field in DATABASE_URL above
|
|
||||||
- POSTGRES_PASSWORD=postgres
|
- POSTGRES_PASSWORD=postgres
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U postgres || exit 1"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 30
|
||||||
|
start_period: 30s
|
||||||
|
|
||||||
# Redis/Valkey service for caching, rate limiting, and audit logging
|
# Redis/Valkey service for caching, rate limiting, and audit logging
|
||||||
# Remove this service if you want to use an external Redis/Valkey instance
|
# Remove this service if you want to use an external Redis/Valkey instance
|
||||||
@@ -215,13 +219,21 @@ services:
|
|||||||
- redis:/data
|
- redis:/data
|
||||||
ports:
|
ports:
|
||||||
- "6379:6379"
|
- "6379:6379"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "valkey-cli", "ping"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 30
|
||||||
|
start_period: 10s
|
||||||
|
|
||||||
formbricks:
|
formbricks:
|
||||||
restart: always
|
restart: always
|
||||||
image: ghcr.io/formbricks/formbricks:latest
|
image: ghcr.io/formbricks/formbricks:latest
|
||||||
depends_on:
|
depends_on:
|
||||||
- postgres
|
postgres:
|
||||||
- redis
|
condition: service_healthy
|
||||||
|
redis:
|
||||||
|
condition: service_healthy
|
||||||
ports:
|
ports:
|
||||||
- 3000:3000
|
- 3000:3000
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
@@ -5868,6 +5868,7 @@
|
|||||||
"jpeg",
|
"jpeg",
|
||||||
"jpg",
|
"jpg",
|
||||||
"webp",
|
"webp",
|
||||||
|
"ico",
|
||||||
"pdf",
|
"pdf",
|
||||||
"eml",
|
"eml",
|
||||||
"doc",
|
"doc",
|
||||||
@@ -5883,6 +5884,7 @@
|
|||||||
"avi",
|
"avi",
|
||||||
"mkv",
|
"mkv",
|
||||||
"webm",
|
"webm",
|
||||||
|
"mp3",
|
||||||
"zip",
|
"zip",
|
||||||
"rar",
|
"rar",
|
||||||
"7z",
|
"7z",
|
||||||
|
|||||||
@@ -7,6 +7,11 @@ type: application
|
|||||||
# Helm chart Version
|
# Helm chart Version
|
||||||
version: 0.0.0-dev
|
version: 0.0.0-dev
|
||||||
|
|
||||||
|
# This is the version number of the application being deployed.
|
||||||
|
appVersion: "3.7.0"
|
||||||
|
|
||||||
|
icon: https://formbricks.com/favicon.ico
|
||||||
|
|
||||||
keywords:
|
keywords:
|
||||||
- formbricks
|
- formbricks
|
||||||
- postgresql
|
- postgresql
|
||||||
|
|||||||
@@ -84,13 +84,18 @@ Redis Access:
|
|||||||
---
|
---
|
||||||
|
|
||||||
Environment Variables:
|
Environment Variables:
|
||||||
The following environment variables have been automatically generated:
|
The following environment variables have been configured:
|
||||||
|
|
||||||
- `NEXTAUTH_SECRET`: A random 32-character string
|
- `WEBAPP_URL`: {{ .Values.formbricks.webappUrl }}
|
||||||
- `ENCRYPTION_KEY`: A random 32-character string
|
- `NEXTAUTH_URL`: {{ .Values.formbricks.webappUrl }}
|
||||||
- `CRON_SECRET`: A random 32-character string
|
{{- if .Values.formbricks.publicUrl }}
|
||||||
- 'EMAIL_VERIFICATION_DISABLED': 1 # By Default email verification is disabled, configure SMTP settings to enable(https://formbricks.com/docs/self-hosting/configuration/smtp)
|
- `PUBLIC_URL`: {{ .Values.formbricks.publicUrl }}
|
||||||
- 'PASSWORD_RESET_DISABLED': 1 # By Default password reset is disabled, configure SMTP settings to enable(https://formbricks.com/docs/self-hosting/configuration/smtp)
|
{{- end }}
|
||||||
|
- `NEXTAUTH_SECRET`: A random 32-character string (auto-generated)
|
||||||
|
- `ENCRYPTION_KEY`: A random 32-character string (auto-generated)
|
||||||
|
- `CRON_SECRET`: A random 32-character string (auto-generated)
|
||||||
|
- `EMAIL_VERIFICATION_DISABLED`: 1 # By Default email verification is disabled, configure SMTP settings to enable(https://formbricks.com/docs/self-hosting/configuration/smtp)
|
||||||
|
- `PASSWORD_RESET_DISABLED`: 1 # By Default password reset is disabled, configure SMTP settings to enable(https://formbricks.com/docs/self-hosting/configuration/smtp)
|
||||||
|
|
||||||
Retrieve them using:
|
Retrieve them using:
|
||||||
```sh
|
```sh
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
{{- $postgresAdminPassword := include "formbricks.postgresAdminPassword" . }}
|
{{- $postgresAdminPassword := include "formbricks.postgresAdminPassword" . }}
|
||||||
{{- $postgresUserPassword := include "formbricks.postgresUserPassword" . }}
|
{{- $postgresUserPassword := include "formbricks.postgresUserPassword" . }}
|
||||||
{{- $redisPassword := include "formbricks.redisPassword" . }}
|
{{- $redisPassword := include "formbricks.redisPassword" . }}
|
||||||
|
{{- $webappUrl := required "formbricks.webappUrl is required. Set it to your Formbricks instance URL (e.g., https://formbricks.example.com)" .Values.formbricks.webappUrl }}
|
||||||
---
|
---
|
||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
kind: Secret
|
kind: Secret
|
||||||
@@ -11,6 +12,12 @@ metadata:
|
|||||||
labels:
|
labels:
|
||||||
{{- include "formbricks.labels" . | nindent 4 }}
|
{{- include "formbricks.labels" . | nindent 4 }}
|
||||||
data:
|
data:
|
||||||
|
# Formbricks application URLs
|
||||||
|
WEBAPP_URL: {{ $webappUrl | b64enc }}
|
||||||
|
NEXTAUTH_URL: {{ $webappUrl | b64enc }}
|
||||||
|
{{- if .Values.formbricks.publicUrl }}
|
||||||
|
PUBLIC_URL: {{ .Values.formbricks.publicUrl | b64enc }}
|
||||||
|
{{- end }}
|
||||||
{{- if .Values.redis.enabled }}
|
{{- if .Values.redis.enabled }}
|
||||||
REDIS_URL: {{ printf "redis://:%s@formbricks-redis-master:6379" $redisPassword | b64enc }}
|
REDIS_URL: {{ printf "redis://:%s@formbricks-redis-master:6379" $redisPassword | b64enc }}
|
||||||
{{- else }}
|
{{- else }}
|
||||||
@@ -21,7 +28,7 @@ data:
|
|||||||
{{- else }}
|
{{- else }}
|
||||||
DATABASE_URL: {{ .Values.postgresql.externalDatabaseUrl | b64enc }}
|
DATABASE_URL: {{ .Values.postgresql.externalDatabaseUrl | b64enc }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
CRON_SECRET: {{ include "formbricks.cronSecret" . | b64enc }}
|
CRON_SECRET: {{ include "formbricks.cronSecret" . | b64enc }}
|
||||||
ENCRYPTION_KEY: {{ include "formbricks.encryptionKey" . | b64enc }}
|
ENCRYPTION_KEY: {{ include "formbricks.encryptionKey" . | b64enc }}
|
||||||
NEXTAUTH_SECRET: {{ include "formbricks.nextAuthSecret" . | b64enc }}
|
NEXTAUTH_SECRET: {{ include "formbricks.nextAuthSecret" . | b64enc }}
|
||||||
{{- if and (.Values.enterprise.licenseKey) (ne .Values.enterprise.licenseKey "") }}
|
{{- if and (.Values.enterprise.licenseKey) (ne .Values.enterprise.licenseKey "") }}
|
||||||
|
|||||||
@@ -7,15 +7,13 @@ metadata:
|
|||||||
namespace: {{ include "formbricks.namespace" . }}
|
namespace: {{ include "formbricks.namespace" . }}
|
||||||
labels:
|
labels:
|
||||||
{{- include "formbricks.labels" $ | nindent 4 }}
|
{{- include "formbricks.labels" $ | nindent 4 }}
|
||||||
{{- if .Values.serviceMonitor.additionalLabels }}
|
{{- if .Values.serviceMonitor.additionalLabels }}
|
||||||
{{ toYaml .Values.serviceMonitor.additionalLabels | indent 4 }}
|
{{- toYaml .Values.serviceMonitor.additionalLabels | nindent 4 }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
{{- if .Values.serviceMonitor.annotations }}
|
{{- if .Values.serviceMonitor.annotations }}
|
||||||
annotations:
|
annotations:
|
||||||
{{- end }}
|
{{- toYaml .Values.serviceMonitor.annotations | nindent 4 }}
|
||||||
{{- if or .Values.serviceMonitor.annotations }}
|
{{- end }}
|
||||||
{{ toYaml .Values.serviceMonitor.annotations | indent 4 }}
|
|
||||||
{{- end }}
|
|
||||||
spec:
|
spec:
|
||||||
selector:
|
selector:
|
||||||
matchLabels:
|
matchLabels:
|
||||||
|
|||||||
@@ -10,6 +10,17 @@ componentOverride: ""
|
|||||||
# Defaults to the chart name if not set.
|
# Defaults to the chart name if not set.
|
||||||
partOfOverride: ""
|
partOfOverride: ""
|
||||||
|
|
||||||
|
##########################################################
|
||||||
|
# Formbricks Configuration
|
||||||
|
##########################################################
|
||||||
|
formbricks:
|
||||||
|
# REQUIRED: Base URL of the site (e.g., https://formbricks.example.com)
|
||||||
|
# This is used for WEBAPP_URL and NEXTAUTH_URL
|
||||||
|
webappUrl: ""
|
||||||
|
|
||||||
|
# Optional: Public URL for surveys (defaults to webappUrl if not set)
|
||||||
|
publicUrl: ""
|
||||||
|
|
||||||
##########################################################
|
##########################################################
|
||||||
# Enterprise Configuration
|
# Enterprise Configuration
|
||||||
##########################################################
|
##########################################################
|
||||||
@@ -76,6 +87,7 @@ deployment:
|
|||||||
# Application container image
|
# Application container image
|
||||||
image:
|
image:
|
||||||
repository: "ghcr.io/formbricks/formbricks"
|
repository: "ghcr.io/formbricks/formbricks"
|
||||||
|
tag: "" # Defaults to appVersion if not set
|
||||||
digest: "" # If set, digest takes precedence over the tag
|
digest: "" # If set, digest takes precedence over the tag
|
||||||
pullPolicy: IfNotPresent
|
pullPolicy: IfNotPresent
|
||||||
|
|
||||||
|
|||||||
+3
-2
@@ -86,10 +86,11 @@
|
|||||||
"axios": ">=1.12.2",
|
"axios": ">=1.12.2",
|
||||||
"node-forge": ">=1.3.2",
|
"node-forge": ">=1.3.2",
|
||||||
"tar-fs": "2.1.4",
|
"tar-fs": "2.1.4",
|
||||||
"typeorm": ">=0.3.26"
|
"typeorm": ">=0.3.26",
|
||||||
|
"systeminformation": "5.27.14"
|
||||||
},
|
},
|
||||||
"comments": {
|
"comments": {
|
||||||
"overrides": "Security fixes for transitive dependencies. Remove when upstream packages update: axios (CVE-2025-58754) - awaiting @boxyhq/saml-jackson update | node-forge (Dependabot #230) - awaiting @boxyhq/saml-jackson update | tar-fs (Dependabot #205) - awaiting upstream dependency updates | typeorm (Dependabot #223) - awaiting @boxyhq/saml-jackson update"
|
"overrides": "Security fixes for transitive dependencies. Remove when upstream packages update: axios (CVE-2025-58754) - awaiting @boxyhq/saml-jackson update | node-forge (Dependabot #230) - awaiting @boxyhq/saml-jackson update | tar-fs (Dependabot #205) - awaiting upstream dependency updates | typeorm (Dependabot #223) - awaiting @boxyhq/saml-jackson update | systeminformation (Dependabot #241) - awaiting @opentelemetry/host-metrics update"
|
||||||
},
|
},
|
||||||
"patchedDependencies": {
|
"patchedDependencies": {
|
||||||
"next-auth@4.24.12": "patches/next-auth@4.24.12.patch"
|
"next-auth@4.24.12": "patches/next-auth@4.24.12.patch"
|
||||||
|
|||||||
@@ -38,7 +38,7 @@
|
|||||||
"db:push": "prisma db push --accept-data-loss",
|
"db:push": "prisma db push --accept-data-loss",
|
||||||
"db:seed": "dotenv -e ../../.env -- tsx src/seed.ts",
|
"db:seed": "dotenv -e ../../.env -- tsx src/seed.ts",
|
||||||
"db:seed:clear": "dotenv -e ../../.env -- tsx src/seed.ts --clear",
|
"db:seed:clear": "dotenv -e ../../.env -- tsx src/seed.ts --clear",
|
||||||
"db:setup": "pnpm db:migrate:dev && pnpm db:create-saml-database:dev && pnpm db:seed",
|
"db:setup": "pnpm db:migrate:dev && pnpm db:create-saml-database:dev",
|
||||||
"db:start": "pnpm db:setup",
|
"db:start": "pnpm db:setup",
|
||||||
"format": "prisma format",
|
"format": "prisma format",
|
||||||
"generate": "prisma generate",
|
"generate": "prisma generate",
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ export interface ConsentProps {
|
|||||||
onChange: (checked: boolean) => void;
|
onChange: (checked: boolean) => void;
|
||||||
/** Whether the field is required (shows asterisk indicator) */
|
/** Whether the field is required (shows asterisk indicator) */
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
|
/** Custom label for the required indicator */
|
||||||
|
requiredLabel?: string;
|
||||||
/** Error message to display */
|
/** Error message to display */
|
||||||
errorMessage?: string;
|
errorMessage?: string;
|
||||||
/** Text direction: 'ltr' (left-to-right), 'rtl' (right-to-left), or 'auto' (auto-detect from content) */
|
/** Text direction: 'ltr' (left-to-right), 'rtl' (right-to-left), or 'auto' (auto-detect from content) */
|
||||||
@@ -45,6 +47,7 @@ function Consent({
|
|||||||
value = false,
|
value = false,
|
||||||
onChange,
|
onChange,
|
||||||
required = false,
|
required = false,
|
||||||
|
requiredLabel,
|
||||||
errorMessage,
|
errorMessage,
|
||||||
dir = "auto",
|
dir = "auto",
|
||||||
disabled = false,
|
disabled = false,
|
||||||
@@ -63,6 +66,7 @@ function Consent({
|
|||||||
headline={headline}
|
headline={headline}
|
||||||
description={description}
|
description={description}
|
||||||
required={required}
|
required={required}
|
||||||
|
requiredLabel={requiredLabel}
|
||||||
htmlFor={inputId}
|
htmlFor={inputId}
|
||||||
imageUrl={imageUrl}
|
imageUrl={imageUrl}
|
||||||
videoUrl={videoUrl}
|
videoUrl={videoUrl}
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ export interface CTAProps {
|
|||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
/** Whether the field is required (shows asterisk indicator) */
|
/** Whether the field is required (shows asterisk indicator) */
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
|
/** Custom label for the required indicator */
|
||||||
|
requiredLabel?: string;
|
||||||
/** Error message to display */
|
/** Error message to display */
|
||||||
errorMessage?: string;
|
errorMessage?: string;
|
||||||
/** Text direction: 'ltr' (left-to-right), 'rtl' (right-to-left), or 'auto' (auto-detect from content) */
|
/** Text direction: 'ltr' (left-to-right), 'rtl' (right-to-left), or 'auto' (auto-detect from content) */
|
||||||
@@ -50,6 +52,7 @@ function CTA({
|
|||||||
buttonExternal = false,
|
buttonExternal = false,
|
||||||
onClick,
|
onClick,
|
||||||
required = false,
|
required = false,
|
||||||
|
requiredLabel,
|
||||||
errorMessage,
|
errorMessage,
|
||||||
dir = "auto",
|
dir = "auto",
|
||||||
disabled = false,
|
disabled = false,
|
||||||
@@ -73,6 +76,7 @@ function CTA({
|
|||||||
headline={headline}
|
headline={headline}
|
||||||
description={description}
|
description={description}
|
||||||
required={required}
|
required={required}
|
||||||
|
requiredLabel={requiredLabel}
|
||||||
htmlFor={inputId}
|
htmlFor={inputId}
|
||||||
imageUrl={imageUrl}
|
imageUrl={imageUrl}
|
||||||
videoUrl={videoUrl}
|
videoUrl={videoUrl}
|
||||||
@@ -82,7 +86,7 @@ function CTA({
|
|||||||
<div className="relative space-y-2">
|
<div className="relative space-y-2">
|
||||||
<ElementError errorMessage={errorMessage} dir={dir} />
|
<ElementError errorMessage={errorMessage} dir={dir} />
|
||||||
|
|
||||||
{buttonExternal && (
|
{buttonExternal ? (
|
||||||
<div className="flex w-full justify-start">
|
<div className="flex w-full justify-start">
|
||||||
<Button
|
<Button
|
||||||
id={inputId}
|
id={inputId}
|
||||||
@@ -95,7 +99,7 @@ function CTA({
|
|||||||
<SquareArrowOutUpRightIcon className="size-4" />
|
<SquareArrowOutUpRightIcon className="size-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ interface DateElementProps {
|
|||||||
onChange: (value: string) => void;
|
onChange: (value: string) => void;
|
||||||
/** Whether the field is required (shows asterisk indicator) */
|
/** Whether the field is required (shows asterisk indicator) */
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
|
/** Custom label for the required indicator */
|
||||||
|
requiredLabel?: string;
|
||||||
/** Minimum date allowed (ISO format: YYYY-MM-DD) */
|
/** Minimum date allowed (ISO format: YYYY-MM-DD) */
|
||||||
minDate?: string;
|
minDate?: string;
|
||||||
/** Maximum date allowed (ISO format: YYYY-MM-DD) */
|
/** Maximum date allowed (ISO format: YYYY-MM-DD) */
|
||||||
@@ -45,6 +47,7 @@ function DateElement({
|
|||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
required = false,
|
required = false,
|
||||||
|
requiredLabel,
|
||||||
minDate,
|
minDate,
|
||||||
maxDate,
|
maxDate,
|
||||||
dir = "auto",
|
dir = "auto",
|
||||||
@@ -152,6 +155,7 @@ function DateElement({
|
|||||||
headline={headline}
|
headline={headline}
|
||||||
description={description}
|
description={description}
|
||||||
required={required}
|
required={required}
|
||||||
|
requiredLabel={requiredLabel}
|
||||||
htmlFor={inputId}
|
htmlFor={inputId}
|
||||||
imageUrl={imageUrl}
|
imageUrl={imageUrl}
|
||||||
videoUrl={videoUrl}
|
videoUrl={videoUrl}
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ interface FileUploadProps {
|
|||||||
allowedFileExtensions?: string[];
|
allowedFileExtensions?: string[];
|
||||||
/** Whether the field is required (shows asterisk indicator) */
|
/** Whether the field is required (shows asterisk indicator) */
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
|
/** Custom label for the required indicator */
|
||||||
|
requiredLabel?: string;
|
||||||
/** Error message to display */
|
/** Error message to display */
|
||||||
errorMessage?: string;
|
errorMessage?: string;
|
||||||
/** Whether the component is in uploading state */
|
/** Whether the component is in uploading state */
|
||||||
@@ -219,6 +221,7 @@ function FileUpload({
|
|||||||
allowMultiple = false,
|
allowMultiple = false,
|
||||||
allowedFileExtensions,
|
allowedFileExtensions,
|
||||||
required = false,
|
required = false,
|
||||||
|
requiredLabel,
|
||||||
errorMessage,
|
errorMessage,
|
||||||
isUploading = false,
|
isUploading = false,
|
||||||
dir = "auto",
|
dir = "auto",
|
||||||
@@ -279,6 +282,7 @@ function FileUpload({
|
|||||||
headline={headline}
|
headline={headline}
|
||||||
description={description}
|
description={description}
|
||||||
required={required}
|
required={required}
|
||||||
|
requiredLabel={requiredLabel}
|
||||||
htmlFor={inputId}
|
htmlFor={inputId}
|
||||||
imageUrl={imageUrl}
|
imageUrl={imageUrl}
|
||||||
videoUrl={videoUrl}
|
videoUrl={videoUrl}
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ interface FormFieldProps {
|
|||||||
onChange: (value: Record<string, string>) => void;
|
onChange: (value: Record<string, string>) => void;
|
||||||
/** Whether the entire form is required (shows asterisk indicator) */
|
/** Whether the entire form is required (shows asterisk indicator) */
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
|
/** Custom label for the required indicator */
|
||||||
|
requiredLabel?: string;
|
||||||
/** Error message to display */
|
/** Error message to display */
|
||||||
errorMessage?: string;
|
errorMessage?: string;
|
||||||
/** Text direction: 'ltr' (left-to-right), 'rtl' (right-to-left), or 'auto' (auto-detect from content) */
|
/** Text direction: 'ltr' (left-to-right), 'rtl' (right-to-left), or 'auto' (auto-detect from content) */
|
||||||
@@ -57,6 +59,7 @@ function FormField({
|
|||||||
value = {},
|
value = {},
|
||||||
onChange,
|
onChange,
|
||||||
required = false,
|
required = false,
|
||||||
|
requiredLabel,
|
||||||
errorMessage,
|
errorMessage,
|
||||||
dir = "auto",
|
dir = "auto",
|
||||||
disabled = false,
|
disabled = false,
|
||||||
@@ -103,6 +106,7 @@ function FormField({
|
|||||||
headline={headline}
|
headline={headline}
|
||||||
description={description}
|
description={description}
|
||||||
required={required}
|
required={required}
|
||||||
|
requiredLabel={requiredLabel}
|
||||||
imageUrl={imageUrl}
|
imageUrl={imageUrl}
|
||||||
videoUrl={videoUrl}
|
videoUrl={videoUrl}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ interface MatrixProps {
|
|||||||
onChange: (value: Record<string, string>) => void;
|
onChange: (value: Record<string, string>) => void;
|
||||||
/** Whether the field is required (shows asterisk indicator) */
|
/** Whether the field is required (shows asterisk indicator) */
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
|
/** Custom label for the required indicator */
|
||||||
|
requiredLabel?: string;
|
||||||
/** Error message to display */
|
/** Error message to display */
|
||||||
errorMessage?: string;
|
errorMessage?: string;
|
||||||
/** Text direction: 'ltr' (left-to-right), 'rtl' (right-to-left), or 'auto' (auto-detect from content) */
|
/** Text direction: 'ltr' (left-to-right), 'rtl' (right-to-left), or 'auto' (auto-detect from content) */
|
||||||
@@ -57,6 +59,7 @@ function Matrix({
|
|||||||
value = {},
|
value = {},
|
||||||
onChange,
|
onChange,
|
||||||
required = false,
|
required = false,
|
||||||
|
requiredLabel,
|
||||||
errorMessage,
|
errorMessage,
|
||||||
dir = "auto",
|
dir = "auto",
|
||||||
disabled = false,
|
disabled = false,
|
||||||
@@ -84,6 +87,7 @@ function Matrix({
|
|||||||
headline={headline}
|
headline={headline}
|
||||||
description={description}
|
description={description}
|
||||||
required={required}
|
required={required}
|
||||||
|
requiredLabel={requiredLabel}
|
||||||
htmlFor={inputId}
|
htmlFor={inputId}
|
||||||
imageUrl={imageUrl}
|
imageUrl={imageUrl}
|
||||||
videoUrl={videoUrl}
|
videoUrl={videoUrl}
|
||||||
|
|||||||
@@ -45,6 +45,8 @@ interface MultiSelectProps {
|
|||||||
onChange: (value: string[]) => void;
|
onChange: (value: string[]) => void;
|
||||||
/** Whether the field is required (shows asterisk indicator) */
|
/** Whether the field is required (shows asterisk indicator) */
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
|
/** Custom label for the required indicator */
|
||||||
|
requiredLabel?: string;
|
||||||
/** Error message to display below the options */
|
/** Error message to display below the options */
|
||||||
errorMessage?: string;
|
errorMessage?: string;
|
||||||
/** Text direction: 'ltr' (left-to-right), 'rtl' (right-to-left), or 'auto' (auto-detect from content) */
|
/** Text direction: 'ltr' (left-to-right), 'rtl' (right-to-left), or 'auto' (auto-detect from content) */
|
||||||
@@ -405,6 +407,7 @@ function MultiSelect({
|
|||||||
value = [],
|
value = [],
|
||||||
onChange,
|
onChange,
|
||||||
required = false,
|
required = false,
|
||||||
|
requiredLabel,
|
||||||
errorMessage,
|
errorMessage,
|
||||||
dir = "auto",
|
dir = "auto",
|
||||||
disabled = false,
|
disabled = false,
|
||||||
@@ -473,6 +476,7 @@ function MultiSelect({
|
|||||||
headline={headline}
|
headline={headline}
|
||||||
description={description}
|
description={description}
|
||||||
required={required}
|
required={required}
|
||||||
|
requiredLabel={requiredLabel}
|
||||||
htmlFor={inputId}
|
htmlFor={inputId}
|
||||||
imageUrl={imageUrl}
|
imageUrl={imageUrl}
|
||||||
videoUrl={videoUrl}
|
videoUrl={videoUrl}
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ interface NPSProps {
|
|||||||
colorCoding?: boolean;
|
colorCoding?: boolean;
|
||||||
/** Whether the field is required (shows asterisk indicator) */
|
/** Whether the field is required (shows asterisk indicator) */
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
|
/** Custom label for the required indicator */
|
||||||
|
requiredLabel?: string;
|
||||||
/** Error message to display */
|
/** Error message to display */
|
||||||
errorMessage?: string;
|
errorMessage?: string;
|
||||||
/** Text direction: 'ltr' (left-to-right), 'rtl' (right-to-left), or 'auto' (auto-detect from content) */
|
/** Text direction: 'ltr' (left-to-right), 'rtl' (right-to-left), or 'auto' (auto-detect from content) */
|
||||||
@@ -48,6 +50,7 @@ function NPS({
|
|||||||
upperLabel,
|
upperLabel,
|
||||||
colorCoding = false,
|
colorCoding = false,
|
||||||
required = false,
|
required = false,
|
||||||
|
requiredLabel,
|
||||||
errorMessage,
|
errorMessage,
|
||||||
dir = "auto",
|
dir = "auto",
|
||||||
disabled = false,
|
disabled = false,
|
||||||
@@ -171,6 +174,7 @@ function NPS({
|
|||||||
headline={headline}
|
headline={headline}
|
||||||
description={description}
|
description={description}
|
||||||
required={required}
|
required={required}
|
||||||
|
requiredLabel={requiredLabel}
|
||||||
htmlFor={inputId}
|
htmlFor={inputId}
|
||||||
imageUrl={imageUrl}
|
imageUrl={imageUrl}
|
||||||
videoUrl={videoUrl}
|
videoUrl={videoUrl}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ interface OpenTextProps {
|
|||||||
value?: string;
|
value?: string;
|
||||||
onChange: (value: string) => void;
|
onChange: (value: string) => void;
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
|
requiredLabel?: string;
|
||||||
longAnswer?: boolean;
|
longAnswer?: boolean;
|
||||||
inputType?: "text" | "email" | "url" | "phone" | "number";
|
inputType?: "text" | "email" | "url" | "phone" | "number";
|
||||||
charLimit?: {
|
charLimit?: {
|
||||||
@@ -37,6 +38,7 @@ function OpenText({
|
|||||||
inputId,
|
inputId,
|
||||||
onChange,
|
onChange,
|
||||||
required = false,
|
required = false,
|
||||||
|
requiredLabel,
|
||||||
longAnswer = false,
|
longAnswer = false,
|
||||||
inputType = "text",
|
inputType = "text",
|
||||||
charLimit,
|
charLimit,
|
||||||
@@ -72,6 +74,7 @@ function OpenText({
|
|||||||
headline={headline}
|
headline={headline}
|
||||||
description={description}
|
description={description}
|
||||||
required={required}
|
required={required}
|
||||||
|
requiredLabel={requiredLabel}
|
||||||
htmlFor={inputId}
|
htmlFor={inputId}
|
||||||
imageUrl={imageUrl}
|
imageUrl={imageUrl}
|
||||||
videoUrl={videoUrl}
|
videoUrl={videoUrl}
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ interface PictureSelectProps {
|
|||||||
allowMulti?: boolean;
|
allowMulti?: boolean;
|
||||||
/** Whether the field is required (shows asterisk indicator) */
|
/** Whether the field is required (shows asterisk indicator) */
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
|
/** Custom label for the required indicator */
|
||||||
|
requiredLabel?: string;
|
||||||
/** Error message to display */
|
/** Error message to display */
|
||||||
errorMessage?: string;
|
errorMessage?: string;
|
||||||
/** Text direction: 'ltr' (left-to-right), 'rtl' (right-to-left), or 'auto' (auto-detect from content) */
|
/** Text direction: 'ltr' (left-to-right), 'rtl' (right-to-left), or 'auto' (auto-detect from content) */
|
||||||
@@ -59,6 +61,7 @@ function PictureSelect({
|
|||||||
onChange,
|
onChange,
|
||||||
allowMulti = false,
|
allowMulti = false,
|
||||||
required = false,
|
required = false,
|
||||||
|
requiredLabel,
|
||||||
errorMessage,
|
errorMessage,
|
||||||
dir = "auto",
|
dir = "auto",
|
||||||
disabled = false,
|
disabled = false,
|
||||||
@@ -96,6 +99,7 @@ function PictureSelect({
|
|||||||
headline={headline}
|
headline={headline}
|
||||||
description={description}
|
description={description}
|
||||||
required={required}
|
required={required}
|
||||||
|
requiredLabel={requiredLabel}
|
||||||
htmlFor={inputId}
|
htmlFor={inputId}
|
||||||
imageUrl={imageUrl}
|
imageUrl={imageUrl}
|
||||||
videoUrl={videoUrl}
|
videoUrl={videoUrl}
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ interface RankingProps {
|
|||||||
onChange: (value: string[]) => void;
|
onChange: (value: string[]) => void;
|
||||||
/** Whether the field is required (shows asterisk indicator) */
|
/** Whether the field is required (shows asterisk indicator) */
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
|
/** Custom label for the required indicator */
|
||||||
|
requiredLabel?: string;
|
||||||
/** Error message to display */
|
/** Error message to display */
|
||||||
errorMessage?: string;
|
errorMessage?: string;
|
||||||
/** Text direction: 'ltr' (left-to-right), 'rtl' (right-to-left), or 'auto' (auto-detect from content) */
|
/** Text direction: 'ltr' (left-to-right), 'rtl' (right-to-left), or 'auto' (auto-detect from content) */
|
||||||
@@ -191,6 +193,7 @@ function Ranking({
|
|||||||
value = [],
|
value = [],
|
||||||
onChange,
|
onChange,
|
||||||
required = false,
|
required = false,
|
||||||
|
requiredLabel,
|
||||||
errorMessage,
|
errorMessage,
|
||||||
dir = "auto",
|
dir = "auto",
|
||||||
disabled = false,
|
disabled = false,
|
||||||
@@ -249,6 +252,7 @@ function Ranking({
|
|||||||
headline={headline}
|
headline={headline}
|
||||||
description={description}
|
description={description}
|
||||||
required={required}
|
required={required}
|
||||||
|
requiredLabel={requiredLabel}
|
||||||
htmlFor={inputId}
|
htmlFor={inputId}
|
||||||
imageUrl={imageUrl}
|
imageUrl={imageUrl}
|
||||||
videoUrl={videoUrl}
|
videoUrl={videoUrl}
|
||||||
|
|||||||
@@ -137,6 +137,8 @@ interface RatingProps {
|
|||||||
colorCoding?: boolean;
|
colorCoding?: boolean;
|
||||||
/** Whether the field is required (shows asterisk indicator) */
|
/** Whether the field is required (shows asterisk indicator) */
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
|
/** Custom label for the required indicator */
|
||||||
|
requiredLabel?: string;
|
||||||
/** Error message to display */
|
/** Error message to display */
|
||||||
errorMessage?: string;
|
errorMessage?: string;
|
||||||
/** Text direction: 'ltr' (left-to-right), 'rtl' (right-to-left), or 'auto' (auto-detect from content) */
|
/** Text direction: 'ltr' (left-to-right), 'rtl' (right-to-left), or 'auto' (auto-detect from content) */
|
||||||
@@ -162,6 +164,7 @@ function Rating({
|
|||||||
upperLabel,
|
upperLabel,
|
||||||
colorCoding = false,
|
colorCoding = false,
|
||||||
required = false,
|
required = false,
|
||||||
|
requiredLabel,
|
||||||
errorMessage,
|
errorMessage,
|
||||||
dir = "auto",
|
dir = "auto",
|
||||||
disabled = false,
|
disabled = false,
|
||||||
@@ -406,6 +409,7 @@ function Rating({
|
|||||||
headline={headline}
|
headline={headline}
|
||||||
description={description}
|
description={description}
|
||||||
required={required}
|
required={required}
|
||||||
|
requiredLabel={requiredLabel}
|
||||||
htmlFor={inputId}
|
htmlFor={inputId}
|
||||||
imageUrl={imageUrl}
|
imageUrl={imageUrl}
|
||||||
videoUrl={videoUrl}
|
videoUrl={videoUrl}
|
||||||
|
|||||||
@@ -41,6 +41,8 @@ interface SingleSelectProps {
|
|||||||
onChange: (value: string) => void;
|
onChange: (value: string) => void;
|
||||||
/** Whether the field is required (shows asterisk indicator) */
|
/** Whether the field is required (shows asterisk indicator) */
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
|
/** Custom label for the required indicator */
|
||||||
|
requiredLabel?: string;
|
||||||
/** Error message to display below the options */
|
/** Error message to display below the options */
|
||||||
errorMessage?: string;
|
errorMessage?: string;
|
||||||
/** Text direction: 'ltr' (left-to-right), 'rtl' (right-to-left), or 'auto' (auto-detect from content) */
|
/** Text direction: 'ltr' (left-to-right), 'rtl' (right-to-left), or 'auto' (auto-detect from content) */
|
||||||
@@ -76,6 +78,7 @@ function SingleSelect({
|
|||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
required = false,
|
required = false,
|
||||||
|
requiredLabel,
|
||||||
errorMessage,
|
errorMessage,
|
||||||
dir = "auto",
|
dir = "auto",
|
||||||
disabled = false,
|
disabled = false,
|
||||||
@@ -141,6 +144,7 @@ function SingleSelect({
|
|||||||
headline={headline}
|
headline={headline}
|
||||||
description={description}
|
description={description}
|
||||||
required={required}
|
required={required}
|
||||||
|
requiredLabel={requiredLabel}
|
||||||
htmlFor={inputId}
|
htmlFor={inputId}
|
||||||
imageUrl={imageUrl}
|
imageUrl={imageUrl}
|
||||||
videoUrl={videoUrl}
|
videoUrl={videoUrl}
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ interface ElementHeaderProps extends React.ComponentProps<"div"> {
|
|||||||
headline: string;
|
headline: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
|
/** Custom label for the required indicator. Defaults to "Required" */
|
||||||
|
requiredLabel?: string;
|
||||||
htmlFor?: string;
|
htmlFor?: string;
|
||||||
imageUrl?: string;
|
imageUrl?: string;
|
||||||
videoUrl?: string;
|
videoUrl?: string;
|
||||||
@@ -43,6 +45,7 @@ function ElementHeader({
|
|||||||
headline,
|
headline,
|
||||||
description,
|
description,
|
||||||
required = false,
|
required = false,
|
||||||
|
requiredLabel = "Required",
|
||||||
htmlFor,
|
htmlFor,
|
||||||
className,
|
className,
|
||||||
imageUrl,
|
imageUrl,
|
||||||
@@ -73,7 +76,9 @@ function ElementHeader({
|
|||||||
{/* Headline */}
|
{/* Headline */}
|
||||||
<div>
|
<div>
|
||||||
<div>
|
<div>
|
||||||
{required ? <span className="label-headline mb-[3px] text-xs opacity-60">Required</span> : null}
|
{required ? (
|
||||||
|
<span className="label-headline mb-[3px] text-xs opacity-60">{requiredLabel}</span>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
{isHeadlineHtml && safeHeadlineHtml ? (
|
{isHeadlineHtml && safeHeadlineHtml ? (
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
},
|
},
|
||||||
"resolveJsonModule": true
|
"resolveJsonModule": true
|
||||||
},
|
},
|
||||||
|
"exclude": ["src/**/*.stories.tsx", "src/**/story-helpers.tsx", "src/**/*.test.ts"],
|
||||||
"extends": "@formbricks/config-typescript/react-library.json",
|
"extends": "@formbricks/config-typescript/react-library.json",
|
||||||
"include": ["src"]
|
"include": ["src"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ export default defineConfig({
|
|||||||
tsconfigPaths(),
|
tsconfigPaths(),
|
||||||
dts({
|
dts({
|
||||||
include: ["src"],
|
include: ["src"],
|
||||||
exclude: ["**/*.stories.tsx", "**/*.test.ts", "**/story-helpers.ts"],
|
exclude: ["**/*.stories.tsx", "**/*.test.ts", "**/story-helpers.tsx"],
|
||||||
}),
|
}),
|
||||||
tailwindcss(),
|
tailwindcss(),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -28,6 +28,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite build --watch --mode dev",
|
"dev": "vite build --watch --mode dev",
|
||||||
"build": "tsc && vite build",
|
"build": "tsc && vite build",
|
||||||
|
"build:analyze": "tsc && ANALYZE=true vite build",
|
||||||
"build:dev": "tsc && vite build --mode dev",
|
"build:dev": "tsc && vite build --mode dev",
|
||||||
"go": "vite build --watch --mode dev",
|
"go": "vite build --watch --mode dev",
|
||||||
"lint": "eslint src --fix --ext .ts,.js,.tsx,.jsx",
|
"lint": "eslint src --fix --ext .ts,.js,.tsx,.jsx",
|
||||||
@@ -58,6 +59,7 @@
|
|||||||
"autoprefixer": "10.4.21",
|
"autoprefixer": "10.4.21",
|
||||||
"concurrently": "9.1.2",
|
"concurrently": "9.1.2",
|
||||||
"postcss": "8.5.3",
|
"postcss": "8.5.3",
|
||||||
|
"rollup-plugin-visualizer": "6.0.5",
|
||||||
"tailwindcss": "4.1.17",
|
"tailwindcss": "4.1.17",
|
||||||
"terser": "5.39.1",
|
"terser": "5.39.1",
|
||||||
"vite": "6.4.1",
|
"vite": "6.4.1",
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useState } from "preact/hooks";
|
import { useState } from "preact/hooks";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { FormField, type FormFieldConfig } from "@formbricks/survey-ui";
|
import { FormField, type FormFieldConfig } from "@formbricks/survey-ui";
|
||||||
import { type TResponseData, type TResponseTtc } from "@formbricks/types/responses";
|
import { type TResponseData, type TResponseTtc } from "@formbricks/types/responses";
|
||||||
import type { TSurveyAddressElement } from "@formbricks/types/surveys/elements";
|
import type { TSurveyAddressElement } from "@formbricks/types/surveys/elements";
|
||||||
@@ -29,6 +30,7 @@ export function AddressElement({
|
|||||||
}: Readonly<AddressElementProps>) {
|
}: Readonly<AddressElementProps>) {
|
||||||
const [startTime, setStartTime] = useState(performance.now());
|
const [startTime, setStartTime] = useState(performance.now());
|
||||||
const isCurrent = element.id === currentElementId;
|
const isCurrent = element.id === currentElementId;
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
useTtc(element.id, ttc, setTtc, startTime, setStartTime, isCurrent);
|
useTtc(element.id, ttc, setTtc, startTime, setStartTime, isCurrent);
|
||||||
|
|
||||||
@@ -118,6 +120,7 @@ export function AddressElement({
|
|||||||
value={convertToValueObject(value)}
|
value={convertToValueObject(value)}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
required={element.required}
|
required={element.required}
|
||||||
|
requiredLabel={t("common.required")}
|
||||||
dir={dir}
|
dir={dir}
|
||||||
imageUrl={element.imageUrl}
|
imageUrl={element.imageUrl}
|
||||||
videoUrl={element.videoUrl}
|
videoUrl={element.videoUrl}
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ export function ConsentElement({
|
|||||||
value={value === "accepted"}
|
value={value === "accepted"}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
required={element.required}
|
required={element.required}
|
||||||
|
requiredLabel={t("common.required")}
|
||||||
errorMessage={errorMessage}
|
errorMessage={errorMessage}
|
||||||
dir={dir}
|
dir={dir}
|
||||||
imageUrl={element.imageUrl}
|
imageUrl={element.imageUrl}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useState } from "preact/hooks";
|
import { useState } from "preact/hooks";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { FormField, type FormFieldConfig } from "@formbricks/survey-ui";
|
import { FormField, type FormFieldConfig } from "@formbricks/survey-ui";
|
||||||
import { type TResponseData, type TResponseTtc } from "@formbricks/types/responses";
|
import { type TResponseData, type TResponseTtc } from "@formbricks/types/responses";
|
||||||
import type { TSurveyContactInfoElement } from "@formbricks/types/surveys/elements";
|
import type { TSurveyContactInfoElement } from "@formbricks/types/surveys/elements";
|
||||||
@@ -30,6 +31,7 @@ export function ContactInfoElement({
|
|||||||
}: Readonly<ContactInfoElementProps>) {
|
}: Readonly<ContactInfoElementProps>) {
|
||||||
const [startTime, setStartTime] = useState(performance.now());
|
const [startTime, setStartTime] = useState(performance.now());
|
||||||
const isCurrent = element.id === currentElementId;
|
const isCurrent = element.id === currentElementId;
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
useTtc(element.id, ttc, setTtc, startTime, setStartTime, isCurrent);
|
useTtc(element.id, ttc, setTtc, startTime, setStartTime, isCurrent);
|
||||||
|
|
||||||
@@ -114,6 +116,7 @@ export function ContactInfoElement({
|
|||||||
value={convertToValueObject(value)}
|
value={convertToValueObject(value)}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
required={element.required}
|
required={element.required}
|
||||||
|
requiredLabel={t("common.required")}
|
||||||
dir={dir}
|
dir={dir}
|
||||||
imageUrl={element.imageUrl}
|
imageUrl={element.imageUrl}
|
||||||
videoUrl={element.videoUrl}
|
videoUrl={element.videoUrl}
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ export function DateElement({
|
|||||||
minDate={getMinDate()}
|
minDate={getMinDate()}
|
||||||
maxDate={getMaxDate()}
|
maxDate={getMaxDate()}
|
||||||
required={element.required}
|
required={element.required}
|
||||||
|
requiredLabel={t("common.required")}
|
||||||
errorMessage={errorMessage}
|
errorMessage={errorMessage}
|
||||||
locale={languageCode}
|
locale={languageCode}
|
||||||
imageUrl={element.imageUrl}
|
imageUrl={element.imageUrl}
|
||||||
|
|||||||
@@ -354,6 +354,7 @@ export function FileUploadElement({
|
|||||||
allowMultiple={element.allowMultipleFiles}
|
allowMultiple={element.allowMultipleFiles}
|
||||||
allowedFileExtensions={element.allowedFileExtensions}
|
allowedFileExtensions={element.allowedFileExtensions}
|
||||||
required={element.required}
|
required={element.required}
|
||||||
|
requiredLabel={t("common.required")}
|
||||||
errorMessage={errorMessage}
|
errorMessage={errorMessage}
|
||||||
isUploading={isUploading}
|
isUploading={isUploading}
|
||||||
imageUrl={element.imageUrl}
|
imageUrl={element.imageUrl}
|
||||||
|
|||||||
@@ -151,6 +151,7 @@ export function MatrixElement({
|
|||||||
value={convertValueToIds(value)}
|
value={convertValueToIds(value)}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
required={element.required}
|
required={element.required}
|
||||||
|
requiredLabel={t("common.required")}
|
||||||
errorMessage={errorMessage}
|
errorMessage={errorMessage}
|
||||||
imageUrl={element.imageUrl}
|
imageUrl={element.imageUrl}
|
||||||
videoUrl={element.videoUrl}
|
videoUrl={element.videoUrl}
|
||||||
|
|||||||
@@ -263,6 +263,7 @@ export function MultipleChoiceMultiElement({
|
|||||||
value={selectedValues}
|
value={selectedValues}
|
||||||
onChange={handleMultiSelectChange}
|
onChange={handleMultiSelectChange}
|
||||||
required={element.required}
|
required={element.required}
|
||||||
|
requiredLabel={t("common.required")}
|
||||||
errorMessage={errorMessage}
|
errorMessage={errorMessage}
|
||||||
dir={dir}
|
dir={dir}
|
||||||
otherOptionId={otherOption?.id}
|
otherOptionId={otherOption?.id}
|
||||||
|
|||||||
@@ -185,6 +185,7 @@ export function MultipleChoiceSingleElement({
|
|||||||
value={selectedValue}
|
value={selectedValue}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
required={element.required}
|
required={element.required}
|
||||||
|
requiredLabel={t("common.required")}
|
||||||
errorMessage={errorMessage}
|
errorMessage={errorMessage}
|
||||||
dir={dir}
|
dir={dir}
|
||||||
otherOptionId={otherOption?.id}
|
otherOptionId={otherOption?.id}
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ export function NPSElement({
|
|||||||
upperLabel={getLocalizedValue(element.upperLabel, languageCode)}
|
upperLabel={getLocalizedValue(element.upperLabel, languageCode)}
|
||||||
colorCoding={element.isColorCodingEnabled}
|
colorCoding={element.isColorCodingEnabled}
|
||||||
required={element.required}
|
required={element.required}
|
||||||
|
requiredLabel={t("common.required")}
|
||||||
errorMessage={errorMessage}
|
errorMessage={errorMessage}
|
||||||
dir={dir}
|
dir={dir}
|
||||||
imageUrl={element.imageUrl}
|
imageUrl={element.imageUrl}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { useState } from "preact/hooks";
|
import { useState } from "preact/hooks";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { OpenText } from "@formbricks/survey-ui";
|
import { OpenText } from "@formbricks/survey-ui";
|
||||||
import { ZEmail, ZUrl } from "@formbricks/types/common";
|
|
||||||
import { type TResponseData, type TResponseTtc } from "@formbricks/types/responses";
|
import { type TResponseData, type TResponseTtc } from "@formbricks/types/responses";
|
||||||
import type { TSurveyOpenTextElement } from "@formbricks/types/surveys/elements";
|
import type { TSurveyOpenTextElement } from "@formbricks/types/surveys/elements";
|
||||||
import { getLocalizedValue } from "@/lib/i18n";
|
import { getLocalizedValue } from "@/lib/i18n";
|
||||||
import { getUpdatedTtc, useTtc } from "@/lib/ttc";
|
import { getUpdatedTtc, useTtc } from "@/lib/ttc";
|
||||||
|
import { validateEmail, validatePhone, validateUrl } from "@/lib/validation";
|
||||||
|
|
||||||
interface OpenTextElementProps {
|
interface OpenTextElementProps {
|
||||||
element: TSurveyOpenTextElement;
|
element: TSurveyOpenTextElement;
|
||||||
@@ -50,27 +50,24 @@ export function OpenTextElement({
|
|||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
const validateEmail = (): boolean => {
|
const checkEmail = (): boolean => {
|
||||||
if (!ZEmail.safeParse(value).success) {
|
if (!validateEmail(value)) {
|
||||||
setErrorMessage(t("errors.please_enter_a_valid_email_address"));
|
setErrorMessage(t("errors.please_enter_a_valid_email_address"));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
const validateUrl = (): boolean => {
|
const checkUrl = (): boolean => {
|
||||||
if (!ZUrl.safeParse(value).success) {
|
if (!validateUrl(value)) {
|
||||||
setErrorMessage(t("errors.please_enter_a_valid_url"));
|
setErrorMessage(t("errors.please_enter_a_valid_url"));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
const validatePhone = (): boolean => {
|
const checkPhone = (): boolean => {
|
||||||
// Match the same pattern: must start with digit or +, end with digit
|
if (!validatePhone(value)) {
|
||||||
// Allows digits, +, -, and spaces in between
|
|
||||||
const phoneRegex = /^[0-9+][0-9+\- ]*[0-9]$/;
|
|
||||||
if (!phoneRegex.test(value)) {
|
|
||||||
setErrorMessage(t("errors.please_enter_a_valid_phone_number"));
|
setErrorMessage(t("errors.please_enter_a_valid_phone_number"));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -81,13 +78,13 @@ export function OpenTextElement({
|
|||||||
if (!value || value.trim() === "") return true;
|
if (!value || value.trim() === "") return true;
|
||||||
|
|
||||||
if (element.inputType === "email") {
|
if (element.inputType === "email") {
|
||||||
return validateEmail();
|
return checkEmail();
|
||||||
}
|
}
|
||||||
if (element.inputType === "url") {
|
if (element.inputType === "url") {
|
||||||
return validateUrl();
|
return checkUrl();
|
||||||
}
|
}
|
||||||
if (element.inputType === "phone") {
|
if (element.inputType === "phone") {
|
||||||
return validatePhone();
|
return checkPhone();
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
@@ -123,6 +120,7 @@ export function OpenTextElement({
|
|||||||
value={value}
|
value={value}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
required={element.required}
|
required={element.required}
|
||||||
|
requiredLabel={t("common.required")}
|
||||||
longAnswer={element.longAnswer !== false}
|
longAnswer={element.longAnswer !== false}
|
||||||
inputType={getInputType()}
|
inputType={getInputType()}
|
||||||
charLimit={element.inputType === "text" ? element.charLimit : undefined}
|
charLimit={element.inputType === "text" ? element.charLimit : undefined}
|
||||||
|
|||||||
@@ -95,6 +95,7 @@ export function PictureSelectionElement({
|
|||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
allowMulti={element.allowMulti}
|
allowMulti={element.allowMulti}
|
||||||
required={element.required}
|
required={element.required}
|
||||||
|
requiredLabel={t("common.required")}
|
||||||
dir={dir}
|
dir={dir}
|
||||||
errorMessage={errorMessage}
|
errorMessage={errorMessage}
|
||||||
imageUrl={element.imageUrl}
|
imageUrl={element.imageUrl}
|
||||||
|
|||||||
@@ -136,6 +136,7 @@ export function RankingElement({
|
|||||||
value={selectedValues}
|
value={selectedValues}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
required={element.required}
|
required={element.required}
|
||||||
|
requiredLabel={t("common.required")}
|
||||||
errorMessage={errorMessage}
|
errorMessage={errorMessage}
|
||||||
imageUrl={element.imageUrl}
|
imageUrl={element.imageUrl}
|
||||||
videoUrl={element.videoUrl}
|
videoUrl={element.videoUrl}
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ export function RatingElement({
|
|||||||
upperLabel={getLocalizedValue(element.upperLabel, languageCode)}
|
upperLabel={getLocalizedValue(element.upperLabel, languageCode)}
|
||||||
colorCoding={element.isColorCodingEnabled}
|
colorCoding={element.isColorCodingEnabled}
|
||||||
required={element.required}
|
required={element.required}
|
||||||
|
requiredLabel={t("common.required")}
|
||||||
dir={dir}
|
dir={dir}
|
||||||
imageUrl={element.imageUrl}
|
imageUrl={element.imageUrl}
|
||||||
videoUrl={element.videoUrl}
|
videoUrl={element.videoUrl}
|
||||||
|
|||||||
@@ -2,12 +2,12 @@ import { useEffect, useRef, useState } from "preact/hooks";
|
|||||||
import { type TJsFileUploadParams } from "@formbricks/types/js";
|
import { type TJsFileUploadParams } from "@formbricks/types/js";
|
||||||
import { type TResponseData, TResponseDataValue, type TResponseTtc } from "@formbricks/types/responses";
|
import { type TResponseData, TResponseDataValue, type TResponseTtc } from "@formbricks/types/responses";
|
||||||
import { type TUploadFileConfig } from "@formbricks/types/storage";
|
import { type TUploadFileConfig } from "@formbricks/types/storage";
|
||||||
import { TSurveyBlock } from "@formbricks/types/surveys/blocks";
|
import { type TSurveyBlock } from "@formbricks/types/surveys/blocks";
|
||||||
|
import { TSurveyElementTypeEnum } from "@formbricks/types/surveys/constants";
|
||||||
import {
|
import {
|
||||||
TSurveyElement,
|
type TSurveyElement,
|
||||||
TSurveyElementTypeEnum,
|
type TSurveyMatrixElement,
|
||||||
TSurveyMatrixElement,
|
type TSurveyRankingElement,
|
||||||
TSurveyRankingElement,
|
|
||||||
} from "@formbricks/types/surveys/elements";
|
} from "@formbricks/types/surveys/elements";
|
||||||
import { BackButton } from "@/components/buttons/back-button";
|
import { BackButton } from "@/components/buttons/back-button";
|
||||||
import { SubmitButton } from "@/components/buttons/submit-button";
|
import { SubmitButton } from "@/components/buttons/submit-button";
|
||||||
|
|||||||
@@ -2,11 +2,8 @@ import { useEffect, useRef } from "preact/hooks";
|
|||||||
import { type TJsFileUploadParams } from "@formbricks/types/js";
|
import { type TJsFileUploadParams } from "@formbricks/types/js";
|
||||||
import { type TResponseData, type TResponseDataValue, type TResponseTtc } from "@formbricks/types/responses";
|
import { type TResponseData, type TResponseDataValue, type TResponseTtc } from "@formbricks/types/responses";
|
||||||
import { type TUploadFileConfig } from "@formbricks/types/storage";
|
import { type TUploadFileConfig } from "@formbricks/types/storage";
|
||||||
import {
|
import { TSurveyElementTypeEnum } from "@formbricks/types/surveys/constants";
|
||||||
TSurveyElement,
|
import { type TSurveyElement, type TSurveyElementChoice } from "@formbricks/types/surveys/elements";
|
||||||
TSurveyElementChoice,
|
|
||||||
TSurveyElementTypeEnum,
|
|
||||||
} from "@formbricks/types/surveys/elements";
|
|
||||||
import { AddressElement } from "@/components/elements/address-element";
|
import { AddressElement } from "@/components/elements/address-element";
|
||||||
import { CalElement } from "@/components/elements/cal-element";
|
import { CalElement } from "@/components/elements/cal-element";
|
||||||
import { ConsentElement } from "@/components/elements/consent-element";
|
import { ConsentElement } from "@/components/elements/consent-element";
|
||||||
|
|||||||
@@ -4,8 +4,17 @@ import { I18nextProvider } from "react-i18next";
|
|||||||
import i18n from "../../lib/i18n.config";
|
import i18n from "../../lib/i18n.config";
|
||||||
|
|
||||||
export const I18nProvider = ({ language, children }: { language: string; children?: ComponentChildren }) => {
|
export const I18nProvider = ({ language, children }: { language: string; children?: ComponentChildren }) => {
|
||||||
useEffect(() => {
|
// Set language synchronously on initial render so children get the correct translations immediately.
|
||||||
|
// This is safe because all translations are pre-loaded (bundled) in i18n.config.ts.
|
||||||
|
if (i18n.language !== language) {
|
||||||
i18n.changeLanguage(language);
|
i18n.changeLanguage(language);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle language prop changes after initial render
|
||||||
|
useEffect(() => {
|
||||||
|
if (i18n.language !== language) {
|
||||||
|
i18n.changeLanguage(language);
|
||||||
|
}
|
||||||
}, [language]);
|
}, [language]);
|
||||||
|
|
||||||
// work around for react-i18next not supporting preact
|
// work around for react-i18next not supporting preact
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { describe, expect, test, vi } from "vitest";
|
import { describe, expect, test, vi } from "vitest";
|
||||||
import { TJsEnvironmentStateSurvey } from "@formbricks/types/js";
|
import { type TJsEnvironmentStateSurvey } from "@formbricks/types/js";
|
||||||
import { TResponseData, TResponseVariables } from "@formbricks/types/responses";
|
import { type TResponseData, type TResponseVariables } from "@formbricks/types/responses";
|
||||||
import { TSurveyBlockLogicAction } from "@formbricks/types/surveys/blocks";
|
import { type TSurveyBlockLogicAction } from "@formbricks/types/surveys/blocks";
|
||||||
import { TSurveyElementTypeEnum } from "@formbricks/types/surveys/elements";
|
import { TSurveyElementTypeEnum } from "@formbricks/types/surveys/constants";
|
||||||
import { TConditionGroup, TSingleCondition } from "@formbricks/types/surveys/logic";
|
import { type TConditionGroup, type TSingleCondition } from "@formbricks/types/surveys/logic";
|
||||||
import { TSurveyVariable } from "@formbricks/types/surveys/types";
|
import { type TSurveyVariable } from "@formbricks/types/surveys/types";
|
||||||
import { evaluateLogic, isConditionGroup, performActions } from "./logic";
|
import { evaluateLogic, isConditionGroup, performActions } from "./logic";
|
||||||
|
|
||||||
// Mock the imported function
|
// Mock the imported function
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { TJsEnvironmentStateSurvey } from "@formbricks/types/js";
|
import { type TJsEnvironmentStateSurvey } from "@formbricks/types/js";
|
||||||
import { TResponseData, TResponseVariables } from "@formbricks/types/responses";
|
import { type TResponseData, type TResponseVariables } from "@formbricks/types/responses";
|
||||||
import { TActionCalculate, TSurveyBlockLogicAction } from "@formbricks/types/surveys/blocks";
|
import { type TActionCalculate, type TSurveyBlockLogicAction } from "@formbricks/types/surveys/blocks";
|
||||||
import { TSurveyElement, TSurveyElementTypeEnum } from "@formbricks/types/surveys/elements";
|
import { TSurveyElementTypeEnum } from "@formbricks/types/surveys/constants";
|
||||||
import { TConditionGroup, TSingleCondition } from "@formbricks/types/surveys/logic";
|
import type { TSurveyElement } from "@formbricks/types/surveys/elements";
|
||||||
import { TSurveyVariable } from "@formbricks/types/surveys/types";
|
import { type TConditionGroup, type TSingleCondition } from "@formbricks/types/surveys/logic";
|
||||||
|
import { type TSurveyVariable } from "@formbricks/types/surveys/types";
|
||||||
import { getLocalizedValue } from "@/lib/i18n";
|
import { getLocalizedValue } from "@/lib/i18n";
|
||||||
import { getElementsFromSurveyBlocks } from "./utils";
|
import { getElementsFromSurveyBlocks } from "./utils";
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { describe, expect, test, vi } from "vitest";
|
import { describe, expect, test, vi } from "vitest";
|
||||||
import { type TResponseData, type TResponseVariables } from "@formbricks/types/responses";
|
import { type TResponseData, type TResponseVariables } from "@formbricks/types/responses";
|
||||||
import { TSurveyElementTypeEnum, type TSurveyOpenTextElement } from "@formbricks/types/surveys/elements";
|
import { TSurveyElementTypeEnum } from "@formbricks/types/surveys/constants";
|
||||||
|
import { type TSurveyOpenTextElement } from "@formbricks/types/surveys/elements";
|
||||||
import { parseRecallInformation, replaceRecallInfo } from "./recall";
|
import { parseRecallInformation, replaceRecallInfo } from "./recall";
|
||||||
|
|
||||||
// Mock getLocalizedValue (assuming path and simple behavior)
|
// Mock getLocalizedValue (assuming path and simple behavior)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { type TResponseData, type TResponseVariables } from "@formbricks/types/responses";
|
import { type TResponseData, type TResponseVariables } from "@formbricks/types/responses";
|
||||||
import { TSurveyElement, TSurveyElementTypeEnum } from "@formbricks/types/surveys/elements";
|
import { TSurveyElementTypeEnum } from "@formbricks/types/surveys/constants";
|
||||||
|
import { type TSurveyElement } from "@formbricks/types/surveys/elements";
|
||||||
import { formatDateWithOrdinal, isValidDateString } from "@/lib/date-time";
|
import { formatDateWithOrdinal, isValidDateString } from "@/lib/date-time";
|
||||||
import { getLocalizedValue } from "@/lib/i18n";
|
import { getLocalizedValue } from "@/lib/i18n";
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { beforeEach, describe, expect, test, vi } from "vitest";
|
import { beforeEach, describe, expect, test, vi } from "vitest";
|
||||||
|
import { TSurveyElementTypeEnum } from "@formbricks/types/surveys/constants";
|
||||||
import type { TJsEnvironmentStateSurvey } from "../../../types/js";
|
import type { TJsEnvironmentStateSurvey } from "../../../types/js";
|
||||||
import { type TAllowedFileExtension, mimeTypes } from "../../../types/storage";
|
import { type TAllowedFileExtension, mimeTypes } from "../../../types/storage";
|
||||||
import { TSurveyElementTypeEnum } from "../../../types/surveys/elements";
|
|
||||||
import type { TSurveyLanguage } from "../../../types/surveys/types";
|
import type { TSurveyLanguage } from "../../../types/surveys/types";
|
||||||
import {
|
import {
|
||||||
findBlockByElementId,
|
findBlockByElementId,
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
import { type Result, err, ok, wrapThrowsAsync } from "@formbricks/types/error-handlers";
|
import { type Result, err, ok, wrapThrowsAsync } from "@formbricks/types/error-handlers";
|
||||||
import { type ApiErrorResponse } from "@formbricks/types/errors";
|
import { type ApiErrorResponse } from "@formbricks/types/errors";
|
||||||
import { type TJsEnvironmentStateSurvey } from "@formbricks/types/js";
|
import { type TJsEnvironmentStateSurvey } from "@formbricks/types/js";
|
||||||
import { TAllowedFileExtension, mimeTypes } from "@formbricks/types/storage";
|
import { type TAllowedFileExtension } from "@formbricks/types/storage";
|
||||||
import { TSurveyBlock, TSurveyBlockLogic, TSurveyBlockLogicAction } from "@formbricks/types/surveys/blocks";
|
import {
|
||||||
import { type TSurveyElement, TSurveyElementChoice } from "@formbricks/types/surveys/elements";
|
type TSurveyBlock,
|
||||||
|
type TSurveyBlockLogic,
|
||||||
|
type TSurveyBlockLogicAction,
|
||||||
|
} from "@formbricks/types/surveys/blocks";
|
||||||
|
import { type TSurveyElement, type TSurveyElementChoice } from "@formbricks/types/surveys/elements";
|
||||||
import { type TShuffleOption } from "@formbricks/types/surveys/types";
|
import { type TShuffleOption } from "@formbricks/types/surveys/types";
|
||||||
import { ApiResponse, ApiSuccessResponse } from "@/types/api";
|
import { ApiResponse, ApiSuccessResponse } from "@/types/api";
|
||||||
|
|
||||||
@@ -177,7 +181,36 @@ export const getDefaultLanguageCode = (survey: TJsEnvironmentStateSurvey): strin
|
|||||||
if (defaultSurveyLanguage) return defaultSurveyLanguage.language.code;
|
if (defaultSurveyLanguage) return defaultSurveyLanguage.language.code;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Function to convert file extension to its MIME type
|
// Inlined from @formbricks/types/storage.ts to avoid Zod dependency
|
||||||
|
const mimeTypes: Record<string, string> = {
|
||||||
|
heic: "image/heic",
|
||||||
|
png: "image/png",
|
||||||
|
jpeg: "image/jpeg",
|
||||||
|
jpg: "image/jpeg",
|
||||||
|
webp: "image/webp",
|
||||||
|
ico: "image/x-icon",
|
||||||
|
pdf: "application/pdf",
|
||||||
|
eml: "message/rfc822",
|
||||||
|
doc: "application/msword",
|
||||||
|
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||||
|
xls: "application/vnd.ms-excel",
|
||||||
|
xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||||
|
ppt: "application/vnd.ms-powerpoint",
|
||||||
|
pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
||||||
|
txt: "text/plain",
|
||||||
|
csv: "text/csv",
|
||||||
|
mp4: "video/mp4",
|
||||||
|
mov: "video/quicktime",
|
||||||
|
avi: "video/x-msvideo",
|
||||||
|
mkv: "video/x-matroska",
|
||||||
|
webm: "video/webm",
|
||||||
|
zip: "application/zip",
|
||||||
|
rar: "application/vnd.rar",
|
||||||
|
"7z": "application/x-7z-compressed",
|
||||||
|
tar: "application/x-tar",
|
||||||
|
mp3: "audio/mpeg",
|
||||||
|
};
|
||||||
|
|
||||||
export const getMimeType = (extension: TAllowedFileExtension): string => mimeTypes[extension];
|
export const getMimeType = (extension: TAllowedFileExtension): string => mimeTypes[extension];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
import { describe, expect, test } from "vitest";
|
||||||
|
import { z } from "zod";
|
||||||
|
// Used for parity check in tests only
|
||||||
|
import { validateEmail, validatePhone, validateUrl } from "./validation";
|
||||||
|
|
||||||
|
describe("Validation Logic Parity", () => {
|
||||||
|
describe("Email Validation", () => {
|
||||||
|
const zodEmail = z.string().email();
|
||||||
|
|
||||||
|
const testCases = [
|
||||||
|
"test@example.com",
|
||||||
|
"user.name+tag@example.co.uk",
|
||||||
|
"invalid-email",
|
||||||
|
"no-at-sign.com",
|
||||||
|
"@domain.com",
|
||||||
|
"user@",
|
||||||
|
"user@domain", // Zod might accept this or not depending on TLD requirement. Our regex requires TLD {2,}.
|
||||||
|
"user@domain.c", // Our regex requires 2 chars TLD.
|
||||||
|
];
|
||||||
|
|
||||||
|
testCases.forEach((email) => {
|
||||||
|
test(`should match Zod behavior for email: "${email}"`, () => {
|
||||||
|
const zodResult = zodEmail.safeParse(email).success;
|
||||||
|
const myResult = validateEmail(email);
|
||||||
|
|
||||||
|
// We aim for parity with Zod's default email validator.
|
||||||
|
// Our custom ReDoS-safe regex handles standard email formats correctly.
|
||||||
|
expect(myResult).toBe(zodResult);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("URL Validation", () => {
|
||||||
|
const zodUrl = z.string().url();
|
||||||
|
|
||||||
|
const testCases = [
|
||||||
|
"https://example.com",
|
||||||
|
"http://localhost:3000",
|
||||||
|
"ftp://files.com",
|
||||||
|
"invalid-url",
|
||||||
|
"examples",
|
||||||
|
"https://",
|
||||||
|
];
|
||||||
|
|
||||||
|
testCases.forEach((url) => {
|
||||||
|
test(`should match Zod behavior for URL: "${url}"`, () => {
|
||||||
|
const zodResult = zodUrl.safeParse(url).success;
|
||||||
|
const myResult = validateUrl(url);
|
||||||
|
|
||||||
|
if (zodResult) {
|
||||||
|
expect(myResult).toBe(true);
|
||||||
|
} else {
|
||||||
|
expect(myResult).toBe(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Note: Phone validation in the component used a custom regex, not Zod's default.
|
||||||
|
// The original component code had: const phoneRegex = /^[0-9+][0-9+\- ]*[0-9]$/;
|
||||||
|
// So we just test that function directly.
|
||||||
|
describe("Phone Validation", () => {
|
||||||
|
const testCases = [
|
||||||
|
{ input: "+1234567890", expected: true },
|
||||||
|
{ input: "123-456-7890", expected: true },
|
||||||
|
{ input: "123 456 7890", expected: true },
|
||||||
|
{ input: "invalid", expected: false },
|
||||||
|
{ input: "123-", expected: false }, // ends with separator
|
||||||
|
{ input: "-123", expected: false }, // starts with separator
|
||||||
|
{ input: "+", expected: false },
|
||||||
|
];
|
||||||
|
|
||||||
|
testCases.forEach(({ input, expected }) => {
|
||||||
|
test(`should validate phone "${input}" as ${expected}`, () => {
|
||||||
|
expect(validatePhone(input)).toBe(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
export const validateEmail = (email: string): boolean => {
|
||||||
|
// ReDoS-safe email validation regex that closely matches Zod's/HTML5's permissive behavior.
|
||||||
|
// This avoids "super-linear runtime" warnings by sticking to specific allowed characters
|
||||||
|
// rather than negated character classes where possible, and avoiding nested quantifiers.
|
||||||
|
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
|
||||||
|
return emailRegex.test(email);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const validateUrl = (url: string): boolean => {
|
||||||
|
try {
|
||||||
|
// Use URL constructor for validation
|
||||||
|
new URL(url);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const validatePhone = (phone: string): boolean => {
|
||||||
|
// Match the same pattern: must start with digit or +, end with digit
|
||||||
|
// ReDoS safe: Avoids nested repetition.
|
||||||
|
const phoneRegex = /^[0-9+][0-9+\- ]*[0-9]$/;
|
||||||
|
return phoneRegex.test(phone);
|
||||||
|
};
|
||||||
@@ -6,6 +6,7 @@ import dts from "vite-plugin-dts";
|
|||||||
import tsconfigPaths from "vite-tsconfig-paths";
|
import tsconfigPaths from "vite-tsconfig-paths";
|
||||||
import { defineConfig } from "vitest/config";
|
import { defineConfig } from "vitest/config";
|
||||||
import { copyCompiledAssetsPlugin } from "../vite-plugins/copy-compiled-assets";
|
import { copyCompiledAssetsPlugin } from "../vite-plugins/copy-compiled-assets";
|
||||||
|
import { visualizer } from "rollup-plugin-visualizer";
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = dirname(__filename);
|
const __dirname = dirname(__filename);
|
||||||
@@ -74,6 +75,7 @@ const config = ({ mode }) => {
|
|||||||
dts({ rollupTypes: true }),
|
dts({ rollupTypes: true }),
|
||||||
tsconfigPaths(),
|
tsconfigPaths(),
|
||||||
copyCompiledAssetsPlugin({ filename: "surveys", distDir: resolve(__dirname, "dist") }),
|
copyCompiledAssetsPlugin({ filename: "surveys", distDir: resolve(__dirname, "dist") }),
|
||||||
|
process.env.ANALYZE === "true" && visualizer({ filename: resolve(__dirname, "stats.html"), open: false, gzipSize: true, brotliSize: true }),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export const ZAllowedFileExtension = z.enum([
|
|||||||
"avi",
|
"avi",
|
||||||
"mkv",
|
"mkv",
|
||||||
"webm",
|
"webm",
|
||||||
|
"mp3",
|
||||||
"zip",
|
"zip",
|
||||||
"rar",
|
"rar",
|
||||||
"7z",
|
"7z",
|
||||||
@@ -50,6 +51,7 @@ export const mimeTypes: Record<TAllowedFileExtension, string> = {
|
|||||||
avi: "video/x-msvideo",
|
avi: "video/x-msvideo",
|
||||||
mkv: "video/x-matroska",
|
mkv: "video/x-matroska",
|
||||||
webm: "video/webm",
|
webm: "video/webm",
|
||||||
|
mp3: "audio/mpeg",
|
||||||
zip: "application/zip",
|
zip: "application/zip",
|
||||||
rar: "application/vnd.rar",
|
rar: "application/vnd.rar",
|
||||||
"7z": "application/x-7z-compressed",
|
"7z": "application/x-7z-compressed",
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
// Element Type Enum (same as question types)
|
||||||
|
export enum TSurveyElementTypeEnum {
|
||||||
|
FileUpload = "fileUpload",
|
||||||
|
OpenText = "openText",
|
||||||
|
MultipleChoiceSingle = "multipleChoiceSingle",
|
||||||
|
MultipleChoiceMulti = "multipleChoiceMulti",
|
||||||
|
NPS = "nps",
|
||||||
|
CTA = "cta",
|
||||||
|
Rating = "rating",
|
||||||
|
Consent = "consent",
|
||||||
|
PictureSelection = "pictureSelection",
|
||||||
|
Cal = "cal",
|
||||||
|
Date = "date",
|
||||||
|
Matrix = "matrix",
|
||||||
|
Address = "address",
|
||||||
|
Ranking = "ranking",
|
||||||
|
ContactInfo = "contactInfo",
|
||||||
|
}
|
||||||
@@ -2,26 +2,19 @@ import { z } from "zod";
|
|||||||
import { ZUrl } from "../common";
|
import { ZUrl } from "../common";
|
||||||
import { ZI18nString } from "../i18n";
|
import { ZI18nString } from "../i18n";
|
||||||
import { ZAllowedFileExtension } from "../storage";
|
import { ZAllowedFileExtension } from "../storage";
|
||||||
|
import { TSurveyElementTypeEnum } from "./constants";
|
||||||
import { FORBIDDEN_IDS } from "./validation";
|
import { FORBIDDEN_IDS } from "./validation";
|
||||||
|
|
||||||
// Element Type Enum (same as question types)
|
/**
|
||||||
export enum TSurveyElementTypeEnum {
|
* RE-EXPORTING FOR BACKWARDS COMPATIBILITY AND CONVENIENCE
|
||||||
FileUpload = "fileUpload",
|
*
|
||||||
OpenText = "openText",
|
* TSurveyElementTypeEnum is defined in `constants.ts` to avoid circular dependencies
|
||||||
MultipleChoiceSingle = "multipleChoiceSingle",
|
* and ensure that the Zod library is not included in bundles that only need the Enum value.
|
||||||
MultipleChoiceMulti = "multipleChoiceMulti",
|
*
|
||||||
NPS = "nps",
|
* However, we re-export it here so that most consumers (who also need the Zod schemas)
|
||||||
CTA = "cta",
|
* can import everything from a single file (`elements.ts`).
|
||||||
Rating = "rating",
|
*/
|
||||||
Consent = "consent",
|
export { TSurveyElementTypeEnum };
|
||||||
PictureSelection = "pictureSelection",
|
|
||||||
Cal = "cal",
|
|
||||||
Date = "date",
|
|
||||||
Matrix = "matrix",
|
|
||||||
Address = "address",
|
|
||||||
Ranking = "ranking",
|
|
||||||
ContactInfo = "contactInfo",
|
|
||||||
}
|
|
||||||
|
|
||||||
// Element ID validation (same rules as questions - USER EDITABLE)
|
// Element ID validation (same rules as questions - USER EDITABLE)
|
||||||
export const ZSurveyElementId = z.string().superRefine((id, ctx) => {
|
export const ZSurveyElementId = z.string().superRefine((id, ctx) => {
|
||||||
|
|||||||
@@ -3190,7 +3190,7 @@ const validateBlockConditions = (
|
|||||||
path: ["blocks", blockIndex, "logic", logicIndex, "conditions"],
|
path: ["blocks", blockIndex, "logic", logicIndex, "conditions"],
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
const validElementTypes = [TSurveyElementTypeEnum.OpenText];
|
const validElementTypes: TSurveyElementTypeEnum[] = [TSurveyElementTypeEnum.OpenText];
|
||||||
|
|
||||||
if (element.inputType === "number") {
|
if (element.inputType === "number") {
|
||||||
validElementTypes.push(...[TSurveyElementTypeEnum.Rating, TSurveyElementTypeEnum.NPS]);
|
validElementTypes.push(...[TSurveyElementTypeEnum.Rating, TSurveyElementTypeEnum.NPS]);
|
||||||
@@ -3397,7 +3397,10 @@ const validateBlockConditions = (
|
|||||||
path: ["blocks", blockIndex, "logic", logicIndex, "conditions"],
|
path: ["blocks", blockIndex, "logic", logicIndex, "conditions"],
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
const validElementTypes = [TSurveyElementTypeEnum.OpenText, TSurveyElementTypeEnum.Date];
|
const validElementTypes: TSurveyElementTypeEnum[] = [
|
||||||
|
TSurveyElementTypeEnum.OpenText,
|
||||||
|
TSurveyElementTypeEnum.Date,
|
||||||
|
];
|
||||||
if (!validElementTypes.includes(elem.data.type)) {
|
if (!validElementTypes.includes(elem.data.type)) {
|
||||||
issues.push({
|
issues.push({
|
||||||
code: z.ZodIssueCode.custom,
|
code: z.ZodIssueCode.custom,
|
||||||
@@ -3587,7 +3590,7 @@ const validateBlockActions = (
|
|||||||
|
|
||||||
if (variable.type === "text") {
|
if (variable.type === "text") {
|
||||||
if (action.value.type === "element") {
|
if (action.value.type === "element") {
|
||||||
const allowedElements = [
|
const allowedElements: TSurveyElementTypeEnum[] = [
|
||||||
TSurveyElementTypeEnum.OpenText,
|
TSurveyElementTypeEnum.OpenText,
|
||||||
TSurveyElementTypeEnum.MultipleChoiceSingle,
|
TSurveyElementTypeEnum.MultipleChoiceSingle,
|
||||||
TSurveyElementTypeEnum.Rating,
|
TSurveyElementTypeEnum.Rating,
|
||||||
@@ -3610,7 +3613,10 @@ const validateBlockActions = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (action.value.type === "element") {
|
if (action.value.type === "element") {
|
||||||
const allowedElements = [TSurveyElementTypeEnum.Rating, TSurveyElementTypeEnum.NPS];
|
const allowedElements: TSurveyElementTypeEnum[] = [
|
||||||
|
TSurveyElementTypeEnum.Rating,
|
||||||
|
TSurveyElementTypeEnum.NPS,
|
||||||
|
];
|
||||||
|
|
||||||
const selectedElement = allElements.get(action.value.value);
|
const selectedElement = allElements.get(action.value.value);
|
||||||
|
|
||||||
|
|||||||
Generated
+69
-9
@@ -9,6 +9,7 @@ overrides:
|
|||||||
node-forge: '>=1.3.2'
|
node-forge: '>=1.3.2'
|
||||||
tar-fs: 2.1.4
|
tar-fs: 2.1.4
|
||||||
typeorm: '>=0.3.26'
|
typeorm: '>=0.3.26'
|
||||||
|
systeminformation: 5.27.14
|
||||||
|
|
||||||
patchedDependencies:
|
patchedDependencies:
|
||||||
next-auth@4.24.12:
|
next-auth@4.24.12:
|
||||||
@@ -199,8 +200,8 @@ importers:
|
|||||||
specifier: 0.203.0
|
specifier: 0.203.0
|
||||||
version: 0.203.0(@opentelemetry/api@1.9.0)
|
version: 0.203.0(@opentelemetry/api@1.9.0)
|
||||||
'@opentelemetry/host-metrics':
|
'@opentelemetry/host-metrics':
|
||||||
specifier: 0.36.0
|
specifier: 0.38.0
|
||||||
version: 0.36.0(@opentelemetry/api@1.9.0)
|
version: 0.38.0(@opentelemetry/api@1.9.0)
|
||||||
'@opentelemetry/instrumentation':
|
'@opentelemetry/instrumentation':
|
||||||
specifier: 0.203.0
|
specifier: 0.203.0
|
||||||
version: 0.203.0(@opentelemetry/api@1.9.0)
|
version: 0.203.0(@opentelemetry/api@1.9.0)
|
||||||
@@ -1011,6 +1012,9 @@ importers:
|
|||||||
postcss:
|
postcss:
|
||||||
specifier: 8.5.3
|
specifier: 8.5.3
|
||||||
version: 8.5.3
|
version: 8.5.3
|
||||||
|
rollup-plugin-visualizer:
|
||||||
|
specifier: 6.0.5
|
||||||
|
version: 6.0.5(rollup@4.54.0)
|
||||||
tailwindcss:
|
tailwindcss:
|
||||||
specifier: 4.1.17
|
specifier: 4.1.17
|
||||||
version: 4.1.17
|
version: 4.1.17
|
||||||
@@ -2964,8 +2968,8 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@opentelemetry/api': ^1.3.0
|
'@opentelemetry/api': ^1.3.0
|
||||||
|
|
||||||
'@opentelemetry/host-metrics@0.36.0':
|
'@opentelemetry/host-metrics@0.38.0':
|
||||||
resolution: {integrity: sha512-14lNY57qa21V3ZOl6xrqLMHR0HGlnPIApR6hr3oCw/Dqs5IzxhTwt2X8Stn82vWJJis7j/ezn11oODsizHj2dQ==}
|
resolution: {integrity: sha512-5iiVhDLa3siMiq95P9/VUtwwNR4mv5/2q79iwMXDbw2if+kRsTGQhSQTClN+POpXeZIEFDlHl/R2TTZ1XWCdkA==}
|
||||||
engines: {node: ^18.19.0 || >=20.6.0}
|
engines: {node: ^18.19.0 || >=20.6.0}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@opentelemetry/api': ^1.3.0
|
'@opentelemetry/api': ^1.3.0
|
||||||
@@ -3231,12 +3235,15 @@ packages:
|
|||||||
|
|
||||||
'@otplib/plugin-crypto@12.0.1':
|
'@otplib/plugin-crypto@12.0.1':
|
||||||
resolution: {integrity: sha512-qPuhN3QrT7ZZLcLCyKOSNhuijUi9G5guMRVrxq63r9YNOxxQjPm59gVxLM+7xGnHnM6cimY57tuKsjK7y9LM1g==}
|
resolution: {integrity: sha512-qPuhN3QrT7ZZLcLCyKOSNhuijUi9G5guMRVrxq63r9YNOxxQjPm59gVxLM+7xGnHnM6cimY57tuKsjK7y9LM1g==}
|
||||||
|
deprecated: Please upgrade to v13 of otplib. Refer to otplib docs for migration paths
|
||||||
|
|
||||||
'@otplib/plugin-thirty-two@12.0.1':
|
'@otplib/plugin-thirty-two@12.0.1':
|
||||||
resolution: {integrity: sha512-MtT+uqRso909UkbrrYpJ6XFjj9D+x2Py7KjTO9JDPhL0bJUYVu5kFP4TFZW4NFAywrAtFRxOVY261u0qwb93gA==}
|
resolution: {integrity: sha512-MtT+uqRso909UkbrrYpJ6XFjj9D+x2Py7KjTO9JDPhL0bJUYVu5kFP4TFZW4NFAywrAtFRxOVY261u0qwb93gA==}
|
||||||
|
deprecated: Please upgrade to v13 of otplib. Refer to otplib docs for migration paths
|
||||||
|
|
||||||
'@otplib/preset-default@12.0.1':
|
'@otplib/preset-default@12.0.1':
|
||||||
resolution: {integrity: sha512-xf1v9oOJRyXfluBhMdpOkr+bsE+Irt+0D5uHtvg6x1eosfmHCsCC6ej/m7FXiWqdo0+ZUI6xSKDhJwc8yfiOPQ==}
|
resolution: {integrity: sha512-xf1v9oOJRyXfluBhMdpOkr+bsE+Irt+0D5uHtvg6x1eosfmHCsCC6ej/m7FXiWqdo0+ZUI6xSKDhJwc8yfiOPQ==}
|
||||||
|
deprecated: Please upgrade to v13 of otplib. Refer to otplib docs for migration paths
|
||||||
|
|
||||||
'@otplib/preset-v11@12.0.1':
|
'@otplib/preset-v11@12.0.1':
|
||||||
resolution: {integrity: sha512-9hSetMI7ECqbFiKICrNa4w70deTUfArtwXykPUvSHWOdzOlfa9ajglu7mNCntlvxycTiOAXkQGwjQCzzDEMRMg==}
|
resolution: {integrity: sha512-9hSetMI7ECqbFiKICrNa4w70deTUfArtwXykPUvSHWOdzOlfa9ajglu7mNCntlvxycTiOAXkQGwjQCzzDEMRMg==}
|
||||||
@@ -6648,6 +6655,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==}
|
resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
define-lazy-prop@2.0.0:
|
||||||
|
resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
define-lazy-prop@3.0.0:
|
define-lazy-prop@3.0.0:
|
||||||
resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==}
|
resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
@@ -7777,6 +7788,11 @@ packages:
|
|||||||
resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==}
|
resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
is-docker@2.2.1:
|
||||||
|
resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
is-docker@3.0.0:
|
is-docker@3.0.0:
|
||||||
resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==}
|
resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==}
|
||||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||||
@@ -7914,6 +7930,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==}
|
resolution: {integrity: sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==}
|
||||||
engines: {node: '>=12.13'}
|
engines: {node: '>=12.13'}
|
||||||
|
|
||||||
|
is-wsl@2.2.0:
|
||||||
|
resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
is-wsl@3.1.0:
|
is-wsl@3.1.0:
|
||||||
resolution: {integrity: sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==}
|
resolution: {integrity: sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==}
|
||||||
engines: {node: '>=16'}
|
engines: {node: '>=16'}
|
||||||
@@ -8839,6 +8859,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==}
|
resolution: {integrity: sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
open@8.4.2:
|
||||||
|
resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
openid-client@5.7.1:
|
openid-client@5.7.1:
|
||||||
resolution: {integrity: sha512-jDBPgSVfTnkIh71Hg9pRvtJc6wTwqjRkN88+gCFtYWrlP4Yx2Dsrow8uPi3qLr/aeymPF3o2+dS+wOpglK04ew==}
|
resolution: {integrity: sha512-jDBPgSVfTnkIh71Hg9pRvtJc6wTwqjRkN88+gCFtYWrlP4Yx2Dsrow8uPi3qLr/aeymPF3o2+dS+wOpglK04ew==}
|
||||||
|
|
||||||
@@ -9638,6 +9662,19 @@ packages:
|
|||||||
ripemd160@2.0.2:
|
ripemd160@2.0.2:
|
||||||
resolution: {integrity: sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==}
|
resolution: {integrity: sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==}
|
||||||
|
|
||||||
|
rollup-plugin-visualizer@6.0.5:
|
||||||
|
resolution: {integrity: sha512-9+HlNgKCVbJDs8tVtjQ43US12eqaiHyyiLMdBwQ7vSZPiHMysGNo2E88TAp1si5wx8NAoYriI2A5kuKfIakmJg==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
hasBin: true
|
||||||
|
peerDependencies:
|
||||||
|
rolldown: 1.x || ^1.0.0-beta
|
||||||
|
rollup: 2.x || 3.x || 4.x
|
||||||
|
peerDependenciesMeta:
|
||||||
|
rolldown:
|
||||||
|
optional: true
|
||||||
|
rollup:
|
||||||
|
optional: true
|
||||||
|
|
||||||
rollup@4.54.0:
|
rollup@4.54.0:
|
||||||
resolution: {integrity: sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw==}
|
resolution: {integrity: sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw==}
|
||||||
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
|
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
|
||||||
@@ -10129,8 +10166,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==}
|
resolution: {integrity: sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==}
|
||||||
engines: {node: ^14.18.0 || >=16.0.0}
|
engines: {node: ^14.18.0 || >=16.0.0}
|
||||||
|
|
||||||
systeminformation@5.23.8:
|
systeminformation@5.27.14:
|
||||||
resolution: {integrity: sha512-Osd24mNKe6jr/YoXLLK3k8TMdzaxDffhpCxgkfgBHcapykIkd50HXThM3TCEuHO2pPuCsSx2ms/SunqhU5MmsQ==}
|
resolution: {integrity: sha512-3DoNDYSZBLxBwaJtQGWNpq0fonga/VZ47HY1+7/G3YoIPaPz93Df6egSzzTKbEMmlzUpy3eQ0nR9REuYIycXGg==}
|
||||||
engines: {node: '>=8.0.0'}
|
engines: {node: '>=8.0.0'}
|
||||||
os: [darwin, linux, win32, freebsd, openbsd, netbsd, sunos, android]
|
os: [darwin, linux, win32, freebsd, openbsd, netbsd, sunos, android]
|
||||||
hasBin: true
|
hasBin: true
|
||||||
@@ -13843,10 +13880,10 @@ snapshots:
|
|||||||
'@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0)
|
'@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0)
|
||||||
'@opentelemetry/sdk-metrics': 2.0.1(@opentelemetry/api@1.9.0)
|
'@opentelemetry/sdk-metrics': 2.0.1(@opentelemetry/api@1.9.0)
|
||||||
|
|
||||||
'@opentelemetry/host-metrics@0.36.0(@opentelemetry/api@1.9.0)':
|
'@opentelemetry/host-metrics@0.38.0(@opentelemetry/api@1.9.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@opentelemetry/api': 1.9.0
|
'@opentelemetry/api': 1.9.0
|
||||||
systeminformation: 5.23.8
|
systeminformation: 5.27.14
|
||||||
|
|
||||||
'@opentelemetry/instrumentation-amqplib@0.50.0(@opentelemetry/api@1.9.0)':
|
'@opentelemetry/instrumentation-amqplib@0.50.0(@opentelemetry/api@1.9.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -17957,6 +17994,8 @@ snapshots:
|
|||||||
es-errors: 1.3.0
|
es-errors: 1.3.0
|
||||||
gopd: 1.2.0
|
gopd: 1.2.0
|
||||||
|
|
||||||
|
define-lazy-prop@2.0.0: {}
|
||||||
|
|
||||||
define-lazy-prop@3.0.0: {}
|
define-lazy-prop@3.0.0: {}
|
||||||
|
|
||||||
define-properties@1.2.1:
|
define-properties@1.2.1:
|
||||||
@@ -19381,6 +19420,8 @@ snapshots:
|
|||||||
call-bound: 1.0.4
|
call-bound: 1.0.4
|
||||||
has-tostringtag: 1.0.2
|
has-tostringtag: 1.0.2
|
||||||
|
|
||||||
|
is-docker@2.2.1: {}
|
||||||
|
|
||||||
is-docker@3.0.0: {}
|
is-docker@3.0.0: {}
|
||||||
|
|
||||||
is-extglob@2.1.1: {}
|
is-extglob@2.1.1: {}
|
||||||
@@ -19492,6 +19533,10 @@ snapshots:
|
|||||||
|
|
||||||
is-what@4.1.16: {}
|
is-what@4.1.16: {}
|
||||||
|
|
||||||
|
is-wsl@2.2.0:
|
||||||
|
dependencies:
|
||||||
|
is-docker: 2.2.1
|
||||||
|
|
||||||
is-wsl@3.1.0:
|
is-wsl@3.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
is-inside-container: 1.0.0
|
is-inside-container: 1.0.0
|
||||||
@@ -20441,6 +20486,12 @@ snapshots:
|
|||||||
is-inside-container: 1.0.0
|
is-inside-container: 1.0.0
|
||||||
wsl-utils: 0.1.0
|
wsl-utils: 0.1.0
|
||||||
|
|
||||||
|
open@8.4.2:
|
||||||
|
dependencies:
|
||||||
|
define-lazy-prop: 2.0.0
|
||||||
|
is-docker: 2.2.1
|
||||||
|
is-wsl: 2.2.0
|
||||||
|
|
||||||
openid-client@5.7.1:
|
openid-client@5.7.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
jose: 4.15.9
|
jose: 4.15.9
|
||||||
@@ -21283,6 +21334,15 @@ snapshots:
|
|||||||
hash-base: 3.1.2
|
hash-base: 3.1.2
|
||||||
inherits: 2.0.4
|
inherits: 2.0.4
|
||||||
|
|
||||||
|
rollup-plugin-visualizer@6.0.5(rollup@4.54.0):
|
||||||
|
dependencies:
|
||||||
|
open: 8.4.2
|
||||||
|
picomatch: 4.0.3
|
||||||
|
source-map: 0.7.6
|
||||||
|
yargs: 17.7.2
|
||||||
|
optionalDependencies:
|
||||||
|
rollup: 4.54.0
|
||||||
|
|
||||||
rollup@4.54.0:
|
rollup@4.54.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/estree': 1.0.8
|
'@types/estree': 1.0.8
|
||||||
@@ -21954,7 +22014,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@pkgr/core': 0.2.9
|
'@pkgr/core': 0.2.9
|
||||||
|
|
||||||
systeminformation@5.23.8: {}
|
systeminformation@5.27.14: {}
|
||||||
|
|
||||||
tabbable@6.3.0: {}
|
tabbable@6.3.0: {}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user