mirror of
https://github.com/formbricks/formbricks.git
synced 2026-03-04 10:19:31 -06:00
Compare commits
7 Commits
fix-transl
...
release/4.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
972407eb93 | ||
|
|
fcbb99c43d | ||
|
|
ec415a7aa1 | ||
|
|
a1e53c9051 | ||
|
|
680295c63e | ||
|
|
73b40469f7 | ||
|
|
282e061606 |
@@ -36,7 +36,7 @@ export const EnvironmentLayout = async ({ layoutData, children }: EnvironmentLay
|
|||||||
// Calculate derived values (no queries)
|
// Calculate derived values (no queries)
|
||||||
const { isMember, isOwner, isManager } = getAccessFlags(membership.role);
|
const { isMember, isOwner, isManager } = getAccessFlags(membership.role);
|
||||||
|
|
||||||
const { features, lastChecked, isPendingDowngrade, active } = license;
|
const { features, lastChecked, isPendingDowngrade, active, status } = license;
|
||||||
const isMultiOrgEnabled = features?.isMultiOrgEnabled ?? false;
|
const isMultiOrgEnabled = features?.isMultiOrgEnabled ?? false;
|
||||||
const organizationProjectsLimit = await getOrganizationProjectsLimit(organization.billing.limits);
|
const organizationProjectsLimit = await getOrganizationProjectsLimit(organization.billing.limits);
|
||||||
const isOwnerOrManager = isOwner || isManager;
|
const isOwnerOrManager = isOwner || isManager;
|
||||||
@@ -63,6 +63,7 @@ export const EnvironmentLayout = async ({ layoutData, children }: EnvironmentLay
|
|||||||
active={active}
|
active={active}
|
||||||
environmentId={environment.id}
|
environmentId={environment.id}
|
||||||
locale={user.locale}
|
locale={user.locale}
|
||||||
|
status={status}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex h-full">
|
<div className="flex h-full">
|
||||||
|
|||||||
@@ -254,6 +254,7 @@
|
|||||||
"label": "Bezeichnung",
|
"label": "Bezeichnung",
|
||||||
"language": "Sprache",
|
"language": "Sprache",
|
||||||
"learn_more": "Mehr erfahren",
|
"learn_more": "Mehr erfahren",
|
||||||
|
"license_expired": "License Expired",
|
||||||
"light_overlay": "Helle Überlagerung",
|
"light_overlay": "Helle Überlagerung",
|
||||||
"limits_reached": "Limits erreicht",
|
"limits_reached": "Limits erreicht",
|
||||||
"link": "Link",
|
"link": "Link",
|
||||||
@@ -460,7 +461,8 @@
|
|||||||
"you_have_reached_your_limit_of_workspace_limit": "Sie haben Ihr Limit von {projectLimit} Workspaces erreicht.",
|
"you_have_reached_your_limit_of_workspace_limit": "Sie haben Ihr Limit von {projectLimit} Workspaces erreicht.",
|
||||||
"you_have_reached_your_monthly_miu_limit_of": "Du hast dein monatliches MIU-Limit erreicht",
|
"you_have_reached_your_monthly_miu_limit_of": "Du hast dein monatliches MIU-Limit erreicht",
|
||||||
"you_have_reached_your_monthly_response_limit_of": "Du hast dein monatliches Antwortlimit erreicht",
|
"you_have_reached_your_monthly_response_limit_of": "Du hast dein monatliches Antwortlimit erreicht",
|
||||||
"you_will_be_downgraded_to_the_community_edition_on_date": "Du wirst am {date} auf die Community Edition herabgestuft."
|
"you_will_be_downgraded_to_the_community_edition_on_date": "Du wirst am {date} auf die Community Edition herabgestuft.",
|
||||||
|
"your_license_has_expired_please_renew": "Your enterprise license has expired. Please renew it to continue using enterprise features."
|
||||||
},
|
},
|
||||||
"emails": {
|
"emails": {
|
||||||
"accept": "Annehmen",
|
"accept": "Annehmen",
|
||||||
|
|||||||
@@ -254,6 +254,7 @@
|
|||||||
"label": "Label",
|
"label": "Label",
|
||||||
"language": "Language",
|
"language": "Language",
|
||||||
"learn_more": "Learn more",
|
"learn_more": "Learn more",
|
||||||
|
"license_expired": "License Expired",
|
||||||
"light_overlay": "Light overlay",
|
"light_overlay": "Light overlay",
|
||||||
"limits_reached": "Limits Reached",
|
"limits_reached": "Limits Reached",
|
||||||
"link": "Link",
|
"link": "Link",
|
||||||
@@ -460,7 +461,8 @@
|
|||||||
"you_have_reached_your_limit_of_workspace_limit": "You have reached your limit of {projectLimit} workspaces.",
|
"you_have_reached_your_limit_of_workspace_limit": "You have reached your limit of {projectLimit} workspaces.",
|
||||||
"you_have_reached_your_monthly_miu_limit_of": "You have reached your monthly MIU limit of",
|
"you_have_reached_your_monthly_miu_limit_of": "You have reached your monthly MIU limit of",
|
||||||
"you_have_reached_your_monthly_response_limit_of": "You have reached your monthly response limit of",
|
"you_have_reached_your_monthly_response_limit_of": "You have reached your monthly response limit of",
|
||||||
"you_will_be_downgraded_to_the_community_edition_on_date": "You will be downgraded to the Community Edition on {date}."
|
"you_will_be_downgraded_to_the_community_edition_on_date": "You will be downgraded to the Community Edition on {date}.",
|
||||||
|
"your_license_has_expired_please_renew": "Your enterprise license has expired. Please renew it to continue using enterprise features."
|
||||||
},
|
},
|
||||||
"emails": {
|
"emails": {
|
||||||
"accept": "Accept",
|
"accept": "Accept",
|
||||||
|
|||||||
@@ -254,6 +254,7 @@
|
|||||||
"label": "Etiqueta",
|
"label": "Etiqueta",
|
||||||
"language": "Idioma",
|
"language": "Idioma",
|
||||||
"learn_more": "Saber más",
|
"learn_more": "Saber más",
|
||||||
|
"license_expired": "License Expired",
|
||||||
"light_overlay": "Superposición clara",
|
"light_overlay": "Superposición clara",
|
||||||
"limits_reached": "Límites alcanzados",
|
"limits_reached": "Límites alcanzados",
|
||||||
"link": "Enlace",
|
"link": "Enlace",
|
||||||
@@ -460,7 +461,8 @@
|
|||||||
"you_have_reached_your_limit_of_workspace_limit": "Has alcanzado tu límite de {projectLimit} espacios de trabajo.",
|
"you_have_reached_your_limit_of_workspace_limit": "Has alcanzado tu límite de {projectLimit} espacios de trabajo.",
|
||||||
"you_have_reached_your_monthly_miu_limit_of": "Has alcanzado tu límite mensual de MIU de",
|
"you_have_reached_your_monthly_miu_limit_of": "Has alcanzado tu límite mensual de MIU de",
|
||||||
"you_have_reached_your_monthly_response_limit_of": "Has alcanzado tu límite mensual de respuestas de",
|
"you_have_reached_your_monthly_response_limit_of": "Has alcanzado tu límite mensual de respuestas de",
|
||||||
"you_will_be_downgraded_to_the_community_edition_on_date": "Serás degradado a la edición Community el {date}."
|
"you_will_be_downgraded_to_the_community_edition_on_date": "Serás degradado a la edición Community el {date}.",
|
||||||
|
"your_license_has_expired_please_renew": "Your enterprise license has expired. Please renew it to continue using enterprise features."
|
||||||
},
|
},
|
||||||
"emails": {
|
"emails": {
|
||||||
"accept": "Aceptar",
|
"accept": "Aceptar",
|
||||||
|
|||||||
@@ -254,6 +254,7 @@
|
|||||||
"label": "Étiquette",
|
"label": "Étiquette",
|
||||||
"language": "Langue",
|
"language": "Langue",
|
||||||
"learn_more": "En savoir plus",
|
"learn_more": "En savoir plus",
|
||||||
|
"license_expired": "License Expired",
|
||||||
"light_overlay": "Claire",
|
"light_overlay": "Claire",
|
||||||
"limits_reached": "Limites atteints",
|
"limits_reached": "Limites atteints",
|
||||||
"link": "Lien",
|
"link": "Lien",
|
||||||
@@ -460,7 +461,8 @@
|
|||||||
"you_have_reached_your_limit_of_workspace_limit": "Vous avez atteint votre limite de {projectLimit} espaces de travail.",
|
"you_have_reached_your_limit_of_workspace_limit": "Vous avez atteint votre limite de {projectLimit} espaces de travail.",
|
||||||
"you_have_reached_your_monthly_miu_limit_of": "Vous avez atteint votre limite mensuelle de MIU de",
|
"you_have_reached_your_monthly_miu_limit_of": "Vous avez atteint votre limite mensuelle de MIU de",
|
||||||
"you_have_reached_your_monthly_response_limit_of": "Vous avez atteint votre limite de réponses mensuelle de",
|
"you_have_reached_your_monthly_response_limit_of": "Vous avez atteint votre limite de réponses mensuelle de",
|
||||||
"you_will_be_downgraded_to_the_community_edition_on_date": "Vous serez rétrogradé à l'édition communautaire le {date}."
|
"you_will_be_downgraded_to_the_community_edition_on_date": "Vous serez rétrogradé à l'édition communautaire le {date}.",
|
||||||
|
"your_license_has_expired_please_renew": "Your enterprise license has expired. Please renew it to continue using enterprise features."
|
||||||
},
|
},
|
||||||
"emails": {
|
"emails": {
|
||||||
"accept": "Accepter",
|
"accept": "Accepter",
|
||||||
|
|||||||
@@ -254,6 +254,7 @@
|
|||||||
"label": "ラベル",
|
"label": "ラベル",
|
||||||
"language": "言語",
|
"language": "言語",
|
||||||
"learn_more": "詳細を見る",
|
"learn_more": "詳細を見る",
|
||||||
|
"license_expired": "License Expired",
|
||||||
"light_overlay": "明るいオーバーレイ",
|
"light_overlay": "明るいオーバーレイ",
|
||||||
"limits_reached": "上限に達しました",
|
"limits_reached": "上限に達しました",
|
||||||
"link": "リンク",
|
"link": "リンク",
|
||||||
@@ -460,7 +461,8 @@
|
|||||||
"you_have_reached_your_limit_of_workspace_limit": "ワークスペースの上限である{projectLimit}件に達しました。",
|
"you_have_reached_your_limit_of_workspace_limit": "ワークスペースの上限である{projectLimit}件に達しました。",
|
||||||
"you_have_reached_your_monthly_miu_limit_of": "月間MIU(月間アクティブユーザー)の上限に達しました",
|
"you_have_reached_your_monthly_miu_limit_of": "月間MIU(月間アクティブユーザー)の上限に達しました",
|
||||||
"you_have_reached_your_monthly_response_limit_of": "月間回答数の上限に達しました",
|
"you_have_reached_your_monthly_response_limit_of": "月間回答数の上限に達しました",
|
||||||
"you_will_be_downgraded_to_the_community_edition_on_date": "コミュニティ版へのダウングレードは {date} に行われます。"
|
"you_will_be_downgraded_to_the_community_edition_on_date": "コミュニティ版へのダウングレードは {date} に行われます。",
|
||||||
|
"your_license_has_expired_please_renew": "Your enterprise license has expired. Please renew it to continue using enterprise features."
|
||||||
},
|
},
|
||||||
"emails": {
|
"emails": {
|
||||||
"accept": "承認",
|
"accept": "承認",
|
||||||
|
|||||||
@@ -254,6 +254,7 @@
|
|||||||
"label": "Label",
|
"label": "Label",
|
||||||
"language": "Taal",
|
"language": "Taal",
|
||||||
"learn_more": "Meer informatie",
|
"learn_more": "Meer informatie",
|
||||||
|
"license_expired": "License Expired",
|
||||||
"light_overlay": "Lichte overlay",
|
"light_overlay": "Lichte overlay",
|
||||||
"limits_reached": "Grenzen bereikt",
|
"limits_reached": "Grenzen bereikt",
|
||||||
"link": "Link",
|
"link": "Link",
|
||||||
@@ -460,7 +461,8 @@
|
|||||||
"you_have_reached_your_limit_of_workspace_limit": "Je hebt je limiet van {projectLimit} werkruimtes bereikt.",
|
"you_have_reached_your_limit_of_workspace_limit": "Je hebt je limiet van {projectLimit} werkruimtes bereikt.",
|
||||||
"you_have_reached_your_monthly_miu_limit_of": "U heeft uw maandelijkse MIU-limiet van bereikt",
|
"you_have_reached_your_monthly_miu_limit_of": "U heeft uw maandelijkse MIU-limiet van bereikt",
|
||||||
"you_have_reached_your_monthly_response_limit_of": "U heeft uw maandelijkse responslimiet bereikt van",
|
"you_have_reached_your_monthly_response_limit_of": "U heeft uw maandelijkse responslimiet bereikt van",
|
||||||
"you_will_be_downgraded_to_the_community_edition_on_date": "Je wordt gedowngraded naar de Community-editie op {date}."
|
"you_will_be_downgraded_to_the_community_edition_on_date": "Je wordt gedowngraded naar de Community-editie op {date}.",
|
||||||
|
"your_license_has_expired_please_renew": "Your enterprise license has expired. Please renew it to continue using enterprise features."
|
||||||
},
|
},
|
||||||
"emails": {
|
"emails": {
|
||||||
"accept": "Accepteren",
|
"accept": "Accepteren",
|
||||||
|
|||||||
@@ -254,6 +254,7 @@
|
|||||||
"label": "Etiqueta",
|
"label": "Etiqueta",
|
||||||
"language": "Língua",
|
"language": "Língua",
|
||||||
"learn_more": "Saiba mais",
|
"learn_more": "Saiba mais",
|
||||||
|
"license_expired": "License Expired",
|
||||||
"light_overlay": "sobreposição leve",
|
"light_overlay": "sobreposição leve",
|
||||||
"limits_reached": "Limites Atingidos",
|
"limits_reached": "Limites Atingidos",
|
||||||
"link": "link",
|
"link": "link",
|
||||||
@@ -460,7 +461,8 @@
|
|||||||
"you_have_reached_your_limit_of_workspace_limit": "Você atingiu seu limite de {projectLimit} espaços de trabalho.",
|
"you_have_reached_your_limit_of_workspace_limit": "Você atingiu seu limite de {projectLimit} espaços de trabalho.",
|
||||||
"you_have_reached_your_monthly_miu_limit_of": "Você atingiu o seu limite mensal de MIU de",
|
"you_have_reached_your_monthly_miu_limit_of": "Você atingiu o seu limite mensal de MIU de",
|
||||||
"you_have_reached_your_monthly_response_limit_of": "Você atingiu o limite mensal de respostas de",
|
"you_have_reached_your_monthly_response_limit_of": "Você atingiu o limite mensal de respostas de",
|
||||||
"you_will_be_downgraded_to_the_community_edition_on_date": "Você será rebaixado para a Edição Comunitária em {date}."
|
"you_will_be_downgraded_to_the_community_edition_on_date": "Você será rebaixado para a Edição Comunitária em {date}.",
|
||||||
|
"your_license_has_expired_please_renew": "Your enterprise license has expired. Please renew it to continue using enterprise features."
|
||||||
},
|
},
|
||||||
"emails": {
|
"emails": {
|
||||||
"accept": "Aceitar",
|
"accept": "Aceitar",
|
||||||
|
|||||||
@@ -254,6 +254,7 @@
|
|||||||
"label": "Etiqueta",
|
"label": "Etiqueta",
|
||||||
"language": "Idioma",
|
"language": "Idioma",
|
||||||
"learn_more": "Saiba mais",
|
"learn_more": "Saiba mais",
|
||||||
|
"license_expired": "License Expired",
|
||||||
"light_overlay": "Sobreposição leve",
|
"light_overlay": "Sobreposição leve",
|
||||||
"limits_reached": "Limites Atingidos",
|
"limits_reached": "Limites Atingidos",
|
||||||
"link": "Link",
|
"link": "Link",
|
||||||
@@ -460,7 +461,8 @@
|
|||||||
"you_have_reached_your_limit_of_workspace_limit": "Atingiu o seu limite de {projectLimit} áreas de trabalho.",
|
"you_have_reached_your_limit_of_workspace_limit": "Atingiu o seu limite de {projectLimit} áreas de trabalho.",
|
||||||
"you_have_reached_your_monthly_miu_limit_of": "Atingiu o seu limite mensal de MIU de",
|
"you_have_reached_your_monthly_miu_limit_of": "Atingiu o seu limite mensal de MIU de",
|
||||||
"you_have_reached_your_monthly_response_limit_of": "Atingiu o seu limite mensal de respostas de",
|
"you_have_reached_your_monthly_response_limit_of": "Atingiu o seu limite mensal de respostas de",
|
||||||
"you_will_be_downgraded_to_the_community_edition_on_date": "Será rebaixado para a Edição Comunitária em {date}."
|
"you_will_be_downgraded_to_the_community_edition_on_date": "Será rebaixado para a Edição Comunitária em {date}.",
|
||||||
|
"your_license_has_expired_please_renew": "Your enterprise license has expired. Please renew it to continue using enterprise features."
|
||||||
},
|
},
|
||||||
"emails": {
|
"emails": {
|
||||||
"accept": "Aceitar",
|
"accept": "Aceitar",
|
||||||
|
|||||||
@@ -254,6 +254,7 @@
|
|||||||
"label": "Etichetă",
|
"label": "Etichetă",
|
||||||
"language": "Limba",
|
"language": "Limba",
|
||||||
"learn_more": "Află mai multe",
|
"learn_more": "Află mai multe",
|
||||||
|
"license_expired": "License Expired",
|
||||||
"light_overlay": "Suprapunere ușoară",
|
"light_overlay": "Suprapunere ușoară",
|
||||||
"limits_reached": "Limite atinse",
|
"limits_reached": "Limite atinse",
|
||||||
"link": "Legătura",
|
"link": "Legătura",
|
||||||
@@ -460,7 +461,8 @@
|
|||||||
"you_have_reached_your_limit_of_workspace_limit": "Ați atins limita de {projectLimit} spații de lucru.",
|
"you_have_reached_your_limit_of_workspace_limit": "Ați atins limita de {projectLimit} spații de lucru.",
|
||||||
"you_have_reached_your_monthly_miu_limit_of": "Ați atins limita lunară MIU de",
|
"you_have_reached_your_monthly_miu_limit_of": "Ați atins limita lunară MIU de",
|
||||||
"you_have_reached_your_monthly_response_limit_of": "Ați atins limita lunară de răspunsuri de",
|
"you_have_reached_your_monthly_response_limit_of": "Ați atins limita lunară de răspunsuri de",
|
||||||
"you_will_be_downgraded_to_the_community_edition_on_date": "Vei fi retrogradat la ediția Community pe {date}."
|
"you_will_be_downgraded_to_the_community_edition_on_date": "Vei fi retrogradat la ediția Community pe {date}.",
|
||||||
|
"your_license_has_expired_please_renew": "Your enterprise license has expired. Please renew it to continue using enterprise features."
|
||||||
},
|
},
|
||||||
"emails": {
|
"emails": {
|
||||||
"accept": "Acceptă",
|
"accept": "Acceptă",
|
||||||
|
|||||||
@@ -254,6 +254,7 @@
|
|||||||
"label": "Метка",
|
"label": "Метка",
|
||||||
"language": "Язык",
|
"language": "Язык",
|
||||||
"learn_more": "Подробнее",
|
"learn_more": "Подробнее",
|
||||||
|
"license_expired": "License Expired",
|
||||||
"light_overlay": "Светлый оверлей",
|
"light_overlay": "Светлый оверлей",
|
||||||
"limits_reached": "Достигнуты лимиты",
|
"limits_reached": "Достигнуты лимиты",
|
||||||
"link": "Ссылка",
|
"link": "Ссылка",
|
||||||
@@ -460,7 +461,8 @@
|
|||||||
"you_have_reached_your_limit_of_workspace_limit": "Вы достигли лимита в {projectLimit} рабочих пространств.",
|
"you_have_reached_your_limit_of_workspace_limit": "Вы достигли лимита в {projectLimit} рабочих пространств.",
|
||||||
"you_have_reached_your_monthly_miu_limit_of": "Вы достигли месячного лимита MIU:",
|
"you_have_reached_your_monthly_miu_limit_of": "Вы достигли месячного лимита MIU:",
|
||||||
"you_have_reached_your_monthly_response_limit_of": "Вы достигли месячного лимита ответов:",
|
"you_have_reached_your_monthly_response_limit_of": "Вы достигли месячного лимита ответов:",
|
||||||
"you_will_be_downgraded_to_the_community_edition_on_date": "Ваша версия будет понижена до Community Edition {date}."
|
"you_will_be_downgraded_to_the_community_edition_on_date": "Ваша версия будет понижена до Community Edition {date}.",
|
||||||
|
"your_license_has_expired_please_renew": "Your enterprise license has expired. Please renew it to continue using enterprise features."
|
||||||
},
|
},
|
||||||
"emails": {
|
"emails": {
|
||||||
"accept": "Принять",
|
"accept": "Принять",
|
||||||
|
|||||||
@@ -254,6 +254,7 @@
|
|||||||
"label": "Etikett",
|
"label": "Etikett",
|
||||||
"language": "Språk",
|
"language": "Språk",
|
||||||
"learn_more": "Läs mer",
|
"learn_more": "Läs mer",
|
||||||
|
"license_expired": "License Expired",
|
||||||
"light_overlay": "Ljust överlägg",
|
"light_overlay": "Ljust överlägg",
|
||||||
"limits_reached": "Gränser nådda",
|
"limits_reached": "Gränser nådda",
|
||||||
"link": "Länk",
|
"link": "Länk",
|
||||||
@@ -460,7 +461,8 @@
|
|||||||
"you_have_reached_your_limit_of_workspace_limit": "Du har nått din gräns på {projectLimit} arbetsytor.",
|
"you_have_reached_your_limit_of_workspace_limit": "Du har nått din gräns på {projectLimit} arbetsytor.",
|
||||||
"you_have_reached_your_monthly_miu_limit_of": "Du har nått din månatliga MIU-gräns på",
|
"you_have_reached_your_monthly_miu_limit_of": "Du har nått din månatliga MIU-gräns på",
|
||||||
"you_have_reached_your_monthly_response_limit_of": "Du har nått din månatliga svarsgräns på",
|
"you_have_reached_your_monthly_response_limit_of": "Du har nått din månatliga svarsgräns på",
|
||||||
"you_will_be_downgraded_to_the_community_edition_on_date": "Du kommer att nedgraderas till Community Edition den {date}."
|
"you_will_be_downgraded_to_the_community_edition_on_date": "Du kommer att nedgraderas till Community Edition den {date}.",
|
||||||
|
"your_license_has_expired_please_renew": "Your enterprise license has expired. Please renew it to continue using enterprise features."
|
||||||
},
|
},
|
||||||
"emails": {
|
"emails": {
|
||||||
"accept": "Acceptera",
|
"accept": "Acceptera",
|
||||||
|
|||||||
@@ -254,6 +254,7 @@
|
|||||||
"label": "标签",
|
"label": "标签",
|
||||||
"language": "语言",
|
"language": "语言",
|
||||||
"learn_more": "了解 更多",
|
"learn_more": "了解 更多",
|
||||||
|
"license_expired": "License Expired",
|
||||||
"light_overlay": "浅色遮罩层",
|
"light_overlay": "浅色遮罩层",
|
||||||
"limits_reached": "限制 达到",
|
"limits_reached": "限制 达到",
|
||||||
"link": "链接",
|
"link": "链接",
|
||||||
@@ -460,7 +461,8 @@
|
|||||||
"you_have_reached_your_limit_of_workspace_limit": "您已达到 {projectLimit} 个工作区的上限。",
|
"you_have_reached_your_limit_of_workspace_limit": "您已达到 {projectLimit} 个工作区的上限。",
|
||||||
"you_have_reached_your_monthly_miu_limit_of": "您 已经 达到 每月 的 MIU 限制",
|
"you_have_reached_your_monthly_miu_limit_of": "您 已经 达到 每月 的 MIU 限制",
|
||||||
"you_have_reached_your_monthly_response_limit_of": "您 已经 达到 每月 的 响应 限制",
|
"you_have_reached_your_monthly_response_limit_of": "您 已经 达到 每月 的 响应 限制",
|
||||||
"you_will_be_downgraded_to_the_community_edition_on_date": "您将在 {date} 降级到社区版。"
|
"you_will_be_downgraded_to_the_community_edition_on_date": "您将在 {date} 降级到社区版。",
|
||||||
|
"your_license_has_expired_please_renew": "Your enterprise license has expired. Please renew it to continue using enterprise features."
|
||||||
},
|
},
|
||||||
"emails": {
|
"emails": {
|
||||||
"accept": "接受",
|
"accept": "接受",
|
||||||
|
|||||||
@@ -254,6 +254,7 @@
|
|||||||
"label": "標籤",
|
"label": "標籤",
|
||||||
"language": "語言",
|
"language": "語言",
|
||||||
"learn_more": "瞭解更多",
|
"learn_more": "瞭解更多",
|
||||||
|
"license_expired": "License Expired",
|
||||||
"light_overlay": "淺色覆蓋",
|
"light_overlay": "淺色覆蓋",
|
||||||
"limits_reached": "已達上限",
|
"limits_reached": "已達上限",
|
||||||
"link": "連結",
|
"link": "連結",
|
||||||
@@ -460,7 +461,8 @@
|
|||||||
"you_have_reached_your_limit_of_workspace_limit": "您已達到 {projectLimit} 個工作區的上限。",
|
"you_have_reached_your_limit_of_workspace_limit": "您已達到 {projectLimit} 個工作區的上限。",
|
||||||
"you_have_reached_your_monthly_miu_limit_of": "您已達到每月 MIU 上限:",
|
"you_have_reached_your_monthly_miu_limit_of": "您已達到每月 MIU 上限:",
|
||||||
"you_have_reached_your_monthly_response_limit_of": "您已達到每月回應上限:",
|
"you_have_reached_your_monthly_response_limit_of": "您已達到每月回應上限:",
|
||||||
"you_will_be_downgraded_to_the_community_edition_on_date": "您將於 '{'date'}' 降級至社群版。"
|
"you_will_be_downgraded_to_the_community_edition_on_date": "您將於 '{'date'}' 降級至社群版。",
|
||||||
|
"your_license_has_expired_please_renew": "Your enterprise license has expired. Please renew it to continue using enterprise features."
|
||||||
},
|
},
|
||||||
"emails": {
|
"emails": {
|
||||||
"accept": "接受",
|
"accept": "接受",
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import { CheckCircle2Icon } from "lucide-react";
|
import { CheckCircle2Icon } from "lucide-react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { TResponseWithQuotas } from "@formbricks/types/responses";
|
import { TResponseWithQuotas } from "@formbricks/types/responses";
|
||||||
|
import { TSurveyElementTypeEnum } from "@formbricks/types/surveys/constants";
|
||||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||||
import { getTextContent } from "@formbricks/types/surveys/validation";
|
import { getTextContent } from "@formbricks/types/surveys/validation";
|
||||||
import { getLocalizedValue } from "@/lib/i18n/utils";
|
import { getLocalizedValue } from "@/lib/i18n/utils";
|
||||||
@@ -67,6 +68,16 @@ export const SingleResponseCardBody = ({
|
|||||||
<VerifiedEmail responseData={response.data} />
|
<VerifiedEmail responseData={response.data} />
|
||||||
)}
|
)}
|
||||||
{elements.map((question) => {
|
{elements.map((question) => {
|
||||||
|
// Skip CTA elements without external buttons only if they have no response data
|
||||||
|
// This preserves historical data from when buttonExternal was true
|
||||||
|
if (
|
||||||
|
question.type === TSurveyElementTypeEnum.CTA &&
|
||||||
|
!question.buttonExternal &&
|
||||||
|
!response.data[question.id]
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const skipped = skippedQuestions.find((skippedQuestionElement) =>
|
const skipped = skippedQuestions.find((skippedQuestionElement) =>
|
||||||
skippedQuestionElement.includes(question.id)
|
skippedQuestionElement.includes(question.id)
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -157,6 +157,7 @@ describe("License Core Logic", () => {
|
|||||||
lastChecked: expect.any(Date),
|
lastChecked: expect.any(Date),
|
||||||
isPendingDowngrade: false,
|
isPendingDowngrade: false,
|
||||||
fallbackLevel: "live" as const,
|
fallbackLevel: "live" as const,
|
||||||
|
status: "active" as const,
|
||||||
};
|
};
|
||||||
|
|
||||||
test("should return cached license from FETCH_LICENSE_CACHE_KEY if available and valid", async () => {
|
test("should return cached license from FETCH_LICENSE_CACHE_KEY if available and valid", async () => {
|
||||||
@@ -233,6 +234,7 @@ describe("License Core Logic", () => {
|
|||||||
lastChecked: previousTime,
|
lastChecked: previousTime,
|
||||||
isPendingDowngrade: true,
|
isPendingDowngrade: true,
|
||||||
fallbackLevel: "grace" as const,
|
fallbackLevel: "grace" as const,
|
||||||
|
status: "unreachable" as const,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -309,6 +311,7 @@ describe("License Core Logic", () => {
|
|||||||
lastChecked: expect.any(Date),
|
lastChecked: expect.any(Date),
|
||||||
isPendingDowngrade: false,
|
isPendingDowngrade: false,
|
||||||
fallbackLevel: "default" as const,
|
fallbackLevel: "default" as const,
|
||||||
|
status: "unreachable" as const,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -356,6 +359,7 @@ describe("License Core Logic", () => {
|
|||||||
lastChecked: expect.any(Date),
|
lastChecked: expect.any(Date),
|
||||||
isPendingDowngrade: false,
|
isPendingDowngrade: false,
|
||||||
fallbackLevel: "default" as const,
|
fallbackLevel: "default" as const,
|
||||||
|
status: "unreachable" as const,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -389,6 +393,7 @@ describe("License Core Logic", () => {
|
|||||||
lastChecked: expect.any(Date),
|
lastChecked: expect.any(Date),
|
||||||
isPendingDowngrade: false,
|
isPendingDowngrade: false,
|
||||||
fallbackLevel: "default" as const,
|
fallbackLevel: "default" as const,
|
||||||
|
status: "no-license" as const,
|
||||||
});
|
});
|
||||||
expect(mockCache.get).not.toHaveBeenCalled();
|
expect(mockCache.get).not.toHaveBeenCalled();
|
||||||
expect(mockCache.set).not.toHaveBeenCalled();
|
expect(mockCache.set).not.toHaveBeenCalled();
|
||||||
@@ -414,6 +419,7 @@ describe("License Core Logic", () => {
|
|||||||
lastChecked: expect.any(Date),
|
lastChecked: expect.any(Date),
|
||||||
isPendingDowngrade: false,
|
isPendingDowngrade: false,
|
||||||
fallbackLevel: "default" as const,
|
fallbackLevel: "default" as const,
|
||||||
|
status: "no-license" as const,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -38,6 +38,17 @@ const CONFIG = {
|
|||||||
// Types
|
// Types
|
||||||
type FallbackLevel = "live" | "cached" | "grace" | "default";
|
type FallbackLevel = "live" | "cached" | "grace" | "default";
|
||||||
|
|
||||||
|
type TEnterpriseLicenseStatusReturn = "active" | "expired" | "unreachable" | "no-license";
|
||||||
|
|
||||||
|
type TEnterpriseLicenseResult = {
|
||||||
|
active: boolean;
|
||||||
|
features: TEnterpriseLicenseFeatures | null;
|
||||||
|
lastChecked: Date;
|
||||||
|
isPendingDowngrade: boolean;
|
||||||
|
fallbackLevel: FallbackLevel;
|
||||||
|
status: TEnterpriseLicenseStatusReturn;
|
||||||
|
};
|
||||||
|
|
||||||
type TPreviousResult = {
|
type TPreviousResult = {
|
||||||
active: boolean;
|
active: boolean;
|
||||||
lastChecked: Date;
|
lastChecked: Date;
|
||||||
@@ -90,7 +101,7 @@ class LicenseApiError extends LicenseError {
|
|||||||
|
|
||||||
// Cache keys using enterprise-grade hierarchical patterns
|
// Cache keys using enterprise-grade hierarchical patterns
|
||||||
const getCacheIdentifier = () => {
|
const getCacheIdentifier = () => {
|
||||||
if (typeof window !== "undefined") {
|
if (globalThis.window !== undefined) {
|
||||||
return "browser"; // Browser environment
|
return "browser"; // Browser environment
|
||||||
}
|
}
|
||||||
if (!env.ENTERPRISE_LICENSE_KEY) {
|
if (!env.ENTERPRISE_LICENSE_KEY) {
|
||||||
@@ -142,36 +153,50 @@ const validateConfig = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Cache functions with async pattern
|
// Cache functions with async pattern
|
||||||
|
let getPreviousResultPromise: Promise<TPreviousResult> | null = null;
|
||||||
|
|
||||||
const getPreviousResult = async (): Promise<TPreviousResult> => {
|
const getPreviousResult = async (): Promise<TPreviousResult> => {
|
||||||
if (typeof window !== "undefined") {
|
if (getPreviousResultPromise) return getPreviousResultPromise;
|
||||||
|
|
||||||
|
getPreviousResultPromise = (async () => {
|
||||||
|
if (globalThis.window !== undefined) {
|
||||||
|
return {
|
||||||
|
active: false,
|
||||||
|
lastChecked: new Date(0),
|
||||||
|
features: DEFAULT_FEATURES,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await cache.get<TPreviousResult>(getCacheKeys().PREVIOUS_RESULT_CACHE_KEY);
|
||||||
|
if (result.ok && result.data) {
|
||||||
|
return {
|
||||||
|
...result.data,
|
||||||
|
lastChecked: new Date(result.data.lastChecked),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error({ error }, "Failed to get previous result from cache");
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
active: false,
|
active: false,
|
||||||
lastChecked: new Date(0),
|
lastChecked: new Date(0),
|
||||||
features: DEFAULT_FEATURES,
|
features: DEFAULT_FEATURES,
|
||||||
};
|
};
|
||||||
}
|
})();
|
||||||
|
|
||||||
try {
|
getPreviousResultPromise
|
||||||
const result = await cache.get<TPreviousResult>(getCacheKeys().PREVIOUS_RESULT_CACHE_KEY);
|
.finally(() => {
|
||||||
if (result.ok && result.data) {
|
getPreviousResultPromise = null;
|
||||||
return {
|
})
|
||||||
...result.data,
|
.catch(() => {});
|
||||||
lastChecked: new Date(result.data.lastChecked),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
logger.error({ error }, "Failed to get previous result from cache");
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return getPreviousResultPromise;
|
||||||
active: false,
|
|
||||||
lastChecked: new Date(0),
|
|
||||||
features: DEFAULT_FEATURES,
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const setPreviousResult = async (previousResult: TPreviousResult) => {
|
const setPreviousResult = async (previousResult: TPreviousResult) => {
|
||||||
if (typeof window !== "undefined") return;
|
if (globalThis.window !== undefined) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await cache.set(
|
const result = await cache.set(
|
||||||
@@ -221,12 +246,21 @@ const validateLicenseDetails = (data: unknown): TEnterpriseLicenseDetails => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Fallback functions
|
// Fallback functions
|
||||||
|
let memoryCache: {
|
||||||
|
data: TEnterpriseLicenseResult;
|
||||||
|
timestamp: number;
|
||||||
|
} | null = null;
|
||||||
|
|
||||||
|
const MEMORY_CACHE_TTL_MS = 60 * 1000; // 1 minute memory cache to avoid stampedes and reduce load when Redis is slow
|
||||||
|
|
||||||
|
let getEnterpriseLicensePromise: Promise<TEnterpriseLicenseResult> | null = null;
|
||||||
|
|
||||||
const getFallbackLevel = (
|
const getFallbackLevel = (
|
||||||
liveLicense: TEnterpriseLicenseDetails | null,
|
liveLicense: TEnterpriseLicenseDetails | null,
|
||||||
previousResult: TPreviousResult,
|
previousResult: TPreviousResult,
|
||||||
currentTime: Date
|
currentTime: Date
|
||||||
): FallbackLevel => {
|
): FallbackLevel => {
|
||||||
if (liveLicense) return "live";
|
if (liveLicense?.status === "active") return "live";
|
||||||
if (previousResult.active) {
|
if (previousResult.active) {
|
||||||
const elapsedTime = currentTime.getTime() - previousResult.lastChecked.getTime();
|
const elapsedTime = currentTime.getTime() - previousResult.lastChecked.getTime();
|
||||||
return elapsedTime < CONFIG.CACHE.GRACE_PERIOD_MS ? "grace" : "default";
|
return elapsedTime < CONFIG.CACHE.GRACE_PERIOD_MS ? "grace" : "default";
|
||||||
@@ -234,7 +268,7 @@ const getFallbackLevel = (
|
|||||||
return "default";
|
return "default";
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleInitialFailure = async (currentTime: Date) => {
|
const handleInitialFailure = async (currentTime: Date): Promise<TEnterpriseLicenseResult> => {
|
||||||
const initialFailResult: TPreviousResult = {
|
const initialFailResult: TPreviousResult = {
|
||||||
active: false,
|
active: false,
|
||||||
features: DEFAULT_FEATURES,
|
features: DEFAULT_FEATURES,
|
||||||
@@ -247,10 +281,13 @@ const handleInitialFailure = async (currentTime: Date) => {
|
|||||||
lastChecked: currentTime,
|
lastChecked: currentTime,
|
||||||
isPendingDowngrade: false,
|
isPendingDowngrade: false,
|
||||||
fallbackLevel: "default" as const,
|
fallbackLevel: "default" as const,
|
||||||
|
status: "unreachable" as const,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// API functions
|
// API functions
|
||||||
|
let fetchLicensePromise: Promise<TEnterpriseLicenseDetails | null> | null = null;
|
||||||
|
|
||||||
const fetchLicenseFromServerInternal = async (retryCount = 0): Promise<TEnterpriseLicenseDetails | null> => {
|
const fetchLicenseFromServerInternal = async (retryCount = 0): Promise<TEnterpriseLicenseDetails | null> => {
|
||||||
if (!env.ENTERPRISE_LICENSE_KEY) return null;
|
if (!env.ENTERPRISE_LICENSE_KEY) return null;
|
||||||
|
|
||||||
@@ -266,6 +303,7 @@ const fetchLicenseFromServerInternal = async (retryCount = 0): Promise<TEnterpri
|
|||||||
// first millisecond of next year => current year is fully included
|
// first millisecond of next year => current year is fully included
|
||||||
const startOfNextYear = new Date(now.getFullYear() + 1, 0, 1);
|
const startOfNextYear = new Date(now.getFullYear() + 1, 0, 1);
|
||||||
|
|
||||||
|
const startTime = Date.now();
|
||||||
const [instanceId, responseCount] = await Promise.all([
|
const [instanceId, responseCount] = await Promise.all([
|
||||||
// Skip instance ID during E2E tests to avoid license key conflicts
|
// Skip instance ID during E2E tests to avoid license key conflicts
|
||||||
// as the instance ID changes with each test run
|
// as the instance ID changes with each test run
|
||||||
@@ -279,6 +317,11 @@ const fetchLicenseFromServerInternal = async (retryCount = 0): Promise<TEnterpri
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
const duration = Date.now() - startTime;
|
||||||
|
|
||||||
|
if (duration > 1000) {
|
||||||
|
logger.warn({ duration, responseCount }, "Slow license check prerequisite data fetching (DB count)");
|
||||||
|
}
|
||||||
|
|
||||||
// No organization exists, cannot perform license check
|
// No organization exists, cannot perform license check
|
||||||
// (skip this check during E2E tests as we intentionally use null)
|
// (skip this check during E2E tests as we intentionally use null)
|
||||||
@@ -311,7 +354,19 @@ const fetchLicenseFromServerInternal = async (retryCount = 0): Promise<TEnterpri
|
|||||||
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const responseJson = (await res.json()) as { data: unknown };
|
const responseJson = (await res.json()) as { data: unknown };
|
||||||
return validateLicenseDetails(responseJson.data);
|
const licenseDetails = validateLicenseDetails(responseJson.data);
|
||||||
|
|
||||||
|
logger.debug(
|
||||||
|
{
|
||||||
|
status: licenseDetails.status,
|
||||||
|
instanceId: instanceId ?? "not-set",
|
||||||
|
responseCount,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
"License check API response received"
|
||||||
|
);
|
||||||
|
|
||||||
|
return licenseDetails;
|
||||||
}
|
}
|
||||||
|
|
||||||
const error = new LicenseApiError(`License check API responded with status: ${res.status}`, res.status);
|
const error = new LicenseApiError(`License check API responded with status: ${res.status}`, res.status);
|
||||||
@@ -342,23 +397,41 @@ export const fetchLicense = async (): Promise<TEnterpriseLicenseDetails | null>
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return await cache.withCache(
|
if (fetchLicensePromise) {
|
||||||
async () => {
|
return fetchLicensePromise;
|
||||||
return await fetchLicenseFromServerInternal();
|
}
|
||||||
},
|
|
||||||
getCacheKeys().FETCH_LICENSE_CACHE_KEY,
|
fetchLicensePromise = (async () => {
|
||||||
CONFIG.CACHE.FETCH_LICENSE_TTL_MS
|
return await cache.withCache(
|
||||||
);
|
async () => {
|
||||||
|
return await fetchLicenseFromServerInternal();
|
||||||
|
},
|
||||||
|
getCacheKeys().FETCH_LICENSE_CACHE_KEY,
|
||||||
|
CONFIG.CACHE.FETCH_LICENSE_TTL_MS
|
||||||
|
);
|
||||||
|
})();
|
||||||
|
|
||||||
|
fetchLicensePromise
|
||||||
|
.finally(() => {
|
||||||
|
fetchLicensePromise = null;
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
|
||||||
|
return fetchLicensePromise;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getEnterpriseLicense = reactCache(
|
export const getEnterpriseLicense = reactCache(async (): Promise<TEnterpriseLicenseResult> => {
|
||||||
async (): Promise<{
|
if (
|
||||||
active: boolean;
|
process.env.NODE_ENV !== "test" &&
|
||||||
features: TEnterpriseLicenseFeatures | null;
|
memoryCache &&
|
||||||
lastChecked: Date;
|
Date.now() - memoryCache.timestamp < MEMORY_CACHE_TTL_MS
|
||||||
isPendingDowngrade: boolean;
|
) {
|
||||||
fallbackLevel: FallbackLevel;
|
return memoryCache.data;
|
||||||
}> => {
|
}
|
||||||
|
|
||||||
|
if (getEnterpriseLicensePromise) return getEnterpriseLicensePromise;
|
||||||
|
|
||||||
|
getEnterpriseLicensePromise = (async () => {
|
||||||
validateConfig();
|
validateConfig();
|
||||||
|
|
||||||
if (!env.ENTERPRISE_LICENSE_KEY || env.ENTERPRISE_LICENSE_KEY.length === 0) {
|
if (!env.ENTERPRISE_LICENSE_KEY || env.ENTERPRISE_LICENSE_KEY.length === 0) {
|
||||||
@@ -368,12 +441,11 @@ export const getEnterpriseLicense = reactCache(
|
|||||||
lastChecked: new Date(),
|
lastChecked: new Date(),
|
||||||
isPendingDowngrade: false,
|
isPendingDowngrade: false,
|
||||||
fallbackLevel: "default" as const,
|
fallbackLevel: "default" as const,
|
||||||
|
status: "no-license" as const,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentTime = new Date();
|
const currentTime = new Date();
|
||||||
const liveLicenseDetails = await fetchLicense();
|
const [liveLicenseDetails, previousResult] = await Promise.all([fetchLicense(), getPreviousResult()]);
|
||||||
const previousResult = await getPreviousResult();
|
|
||||||
const fallbackLevel = getFallbackLevel(liveLicenseDetails, previousResult, currentTime);
|
const fallbackLevel = getFallbackLevel(liveLicenseDetails, previousResult, currentTime);
|
||||||
|
|
||||||
trackFallbackUsage(fallbackLevel);
|
trackFallbackUsage(fallbackLevel);
|
||||||
@@ -381,41 +453,84 @@ export const getEnterpriseLicense = reactCache(
|
|||||||
let currentLicenseState: TPreviousResult | undefined;
|
let currentLicenseState: TPreviousResult | undefined;
|
||||||
|
|
||||||
switch (fallbackLevel) {
|
switch (fallbackLevel) {
|
||||||
case "live":
|
case "live": {
|
||||||
if (!liveLicenseDetails) throw new Error("Invalid state: live license expected");
|
if (!liveLicenseDetails) throw new Error("Invalid state: live license expected");
|
||||||
currentLicenseState = {
|
currentLicenseState = {
|
||||||
active: liveLicenseDetails.status === "active",
|
active: liveLicenseDetails.status === "active",
|
||||||
features: liveLicenseDetails.features,
|
features: liveLicenseDetails.features,
|
||||||
lastChecked: currentTime,
|
lastChecked: currentTime,
|
||||||
};
|
};
|
||||||
await setPreviousResult(currentLicenseState);
|
|
||||||
return {
|
// Only update previous result if it's actually different or if it's old (1 hour)
|
||||||
|
// This prevents hammering Redis on every request when the license is active
|
||||||
|
if (
|
||||||
|
!previousResult.active ||
|
||||||
|
previousResult.active !== currentLicenseState.active ||
|
||||||
|
currentTime.getTime() - previousResult.lastChecked.getTime() > 60 * 60 * 1000
|
||||||
|
) {
|
||||||
|
await setPreviousResult(currentLicenseState);
|
||||||
|
}
|
||||||
|
|
||||||
|
const liveResult: TEnterpriseLicenseResult = {
|
||||||
active: currentLicenseState.active,
|
active: currentLicenseState.active,
|
||||||
features: currentLicenseState.features,
|
features: currentLicenseState.features,
|
||||||
lastChecked: currentTime,
|
lastChecked: currentTime,
|
||||||
isPendingDowngrade: false,
|
isPendingDowngrade: false,
|
||||||
fallbackLevel: "live" as const,
|
fallbackLevel: "live" as const,
|
||||||
|
status: liveLicenseDetails.status,
|
||||||
};
|
};
|
||||||
|
memoryCache = { data: liveResult, timestamp: Date.now() };
|
||||||
|
return liveResult;
|
||||||
|
}
|
||||||
|
|
||||||
case "grace":
|
case "grace": {
|
||||||
if (!validateFallback(previousResult)) {
|
if (!validateFallback(previousResult)) {
|
||||||
return handleInitialFailure(currentTime);
|
return await handleInitialFailure(currentTime);
|
||||||
}
|
}
|
||||||
return {
|
const graceResult: TEnterpriseLicenseResult = {
|
||||||
active: previousResult.active,
|
active: previousResult.active,
|
||||||
features: previousResult.features,
|
features: previousResult.features,
|
||||||
lastChecked: previousResult.lastChecked,
|
lastChecked: previousResult.lastChecked,
|
||||||
isPendingDowngrade: true,
|
isPendingDowngrade: true,
|
||||||
fallbackLevel: "grace" as const,
|
fallbackLevel: "grace" as const,
|
||||||
|
status: (liveLicenseDetails?.status as TEnterpriseLicenseStatusReturn) ?? "unreachable",
|
||||||
};
|
};
|
||||||
|
memoryCache = { data: graceResult, timestamp: Date.now() };
|
||||||
|
return graceResult;
|
||||||
|
}
|
||||||
|
|
||||||
case "default":
|
case "default": {
|
||||||
return handleInitialFailure(currentTime);
|
if (liveLicenseDetails?.status === "expired") {
|
||||||
|
const expiredResult: TEnterpriseLicenseResult = {
|
||||||
|
active: false,
|
||||||
|
features: DEFAULT_FEATURES,
|
||||||
|
lastChecked: currentTime,
|
||||||
|
isPendingDowngrade: false,
|
||||||
|
fallbackLevel: "default" as const,
|
||||||
|
status: "expired" as const,
|
||||||
|
};
|
||||||
|
memoryCache = { data: expiredResult, timestamp: Date.now() };
|
||||||
|
return expiredResult;
|
||||||
|
}
|
||||||
|
const failResult = await handleInitialFailure(currentTime);
|
||||||
|
memoryCache = { data: failResult, timestamp: Date.now() };
|
||||||
|
return failResult;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return handleInitialFailure(currentTime);
|
const finalFailResult = await handleInitialFailure(currentTime);
|
||||||
}
|
memoryCache = { data: finalFailResult, timestamp: Date.now() };
|
||||||
);
|
return finalFailResult;
|
||||||
|
})();
|
||||||
|
|
||||||
|
getEnterpriseLicensePromise
|
||||||
|
.finally(() => {
|
||||||
|
getEnterpriseLicensePromise = null;
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
|
||||||
|
return getEnterpriseLicensePromise;
|
||||||
|
});
|
||||||
|
|
||||||
export const getLicenseFeatures = async (): Promise<TEnterpriseLicenseFeatures | null> => {
|
export const getLicenseFeatures = async (): Promise<TEnterpriseLicenseFeatures | null> => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ type TEnterpriseLicense = {
|
|||||||
lastChecked: Date;
|
lastChecked: Date;
|
||||||
isPendingDowngrade: boolean;
|
isPendingDowngrade: boolean;
|
||||||
fallbackLevel: string;
|
fallbackLevel: string;
|
||||||
|
status: "active" | "expired" | "unreachable" | "no-license";
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ZEnvironmentAuth = z.object({
|
export const ZEnvironmentAuth = z.object({
|
||||||
|
|||||||
@@ -425,11 +425,19 @@ export const SurveyMenuBar = ({
|
|||||||
const segment = await handleSegmentUpdate();
|
const segment = await handleSegmentUpdate();
|
||||||
clearSurveyLocalStorage();
|
clearSurveyLocalStorage();
|
||||||
|
|
||||||
await updateSurveyAction({
|
const publishResult = await updateSurveyAction({
|
||||||
...localSurvey,
|
...localSurvey,
|
||||||
status,
|
status,
|
||||||
segment,
|
segment,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!publishResult?.data) {
|
||||||
|
const errorMessage = getFormattedErrorMessage(publishResult);
|
||||||
|
toast.error(errorMessage);
|
||||||
|
setIsSurveyPublishing(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setIsSurveyPublishing(false);
|
setIsSurveyPublishing(false);
|
||||||
// Set flag to prevent beforeunload warning during navigation
|
// Set flag to prevent beforeunload warning during navigation
|
||||||
isSuccessfullySavedRef.current = true;
|
isSuccessfullySavedRef.current = true;
|
||||||
@@ -467,7 +475,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:ml-4 sm:mt-0">
|
||||||
<AutoSaveIndicator isDraft={localSurvey.status === "draft"} lastSaved={lastAutoSaved} />
|
<AutoSaveIndicator isDraft={localSurvey.status === "draft"} lastSaved={lastAutoSaved} />
|
||||||
{!isStorageConfigured && (
|
{!isStorageConfigured && (
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ interface PendingDowngradeBannerProps {
|
|||||||
isPendingDowngrade: boolean;
|
isPendingDowngrade: boolean;
|
||||||
environmentId: string;
|
environmentId: string;
|
||||||
locale: TUserLocale;
|
locale: TUserLocale;
|
||||||
|
status: "active" | "expired" | "unreachable" | "no-license";
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PendingDowngradeBanner = ({
|
export const PendingDowngradeBanner = ({
|
||||||
@@ -20,11 +21,12 @@ export const PendingDowngradeBanner = ({
|
|||||||
isPendingDowngrade,
|
isPendingDowngrade,
|
||||||
environmentId,
|
environmentId,
|
||||||
locale,
|
locale,
|
||||||
|
status,
|
||||||
}: PendingDowngradeBannerProps) => {
|
}: PendingDowngradeBannerProps) => {
|
||||||
const threeDaysInMillis = 3 * 24 * 60 * 60 * 1000;
|
const threeDaysInMillis = 3 * 24 * 60 * 60 * 1000;
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const isLastCheckedWithin72Hours = lastChecked
|
const isLastCheckedWithin72Hours = lastChecked
|
||||||
? new Date().getTime() - lastChecked.getTime() < threeDaysInMillis
|
? Date.now() - lastChecked.getTime() < threeDaysInMillis
|
||||||
: false;
|
: false;
|
||||||
|
|
||||||
const scheduledDowngradeDate = new Date(lastChecked.getTime() + threeDaysInMillis);
|
const scheduledDowngradeDate = new Date(lastChecked.getTime() + threeDaysInMillis);
|
||||||
@@ -36,7 +38,34 @@ export const PendingDowngradeBanner = ({
|
|||||||
|
|
||||||
const [show, setShow] = useState(true);
|
const [show, setShow] = useState(true);
|
||||||
|
|
||||||
if (show && active && isPendingDowngrade) {
|
const isExpired = status === "expired";
|
||||||
|
|
||||||
|
const getDescription = () => {
|
||||||
|
if (isExpired) {
|
||||||
|
const expiredMessage = t("common.your_license_has_expired_please_renew");
|
||||||
|
const downgradedMessage = t("common.you_are_downgraded_to_the_community_edition");
|
||||||
|
return `${expiredMessage} ${downgradedMessage}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const unreachableMessage = t(
|
||||||
|
"common.we_were_unable_to_verify_your_license_because_the_license_server_is_unreachable"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!active) {
|
||||||
|
return `${unreachableMessage} ${t("common.you_are_downgraded_to_the_community_edition")}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLastCheckedWithin72Hours) {
|
||||||
|
const scheduledMessage = t("common.you_will_be_downgraded_to_the_community_edition_on_date", {
|
||||||
|
date: formattedDate,
|
||||||
|
});
|
||||||
|
return `${unreachableMessage} ${scheduledMessage}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${unreachableMessage} ${t("common.you_are_downgraded_to_the_community_edition")}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (show && (isPendingDowngrade || isExpired)) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
aria-live="assertive"
|
aria-live="assertive"
|
||||||
@@ -50,17 +79,10 @@ export const PendingDowngradeBanner = ({
|
|||||||
<TriangleAlertIcon className="text-error h-6 w-6" aria-hidden="true" />
|
<TriangleAlertIcon className="text-error h-6 w-6" aria-hidden="true" />
|
||||||
</div>
|
</div>
|
||||||
<div className="ml-3 w-0 flex-1">
|
<div className="ml-3 w-0 flex-1">
|
||||||
<p className="text-base font-medium text-slate-900">{t("common.pending_downgrade")}</p>
|
<p className="text-base font-medium text-slate-900">
|
||||||
<p className="mt-1 text-sm text-slate-500">
|
{isExpired ? t("common.license_expired") : t("common.pending_downgrade")}
|
||||||
{t(
|
|
||||||
"common.we_were_unable_to_verify_your_license_because_the_license_server_is_unreachable"
|
|
||||||
)}{" "}
|
|
||||||
{isLastCheckedWithin72Hours
|
|
||||||
? t("common.you_will_be_downgraded_to_the_community_edition_on_date", {
|
|
||||||
date: formattedDate,
|
|
||||||
})
|
|
||||||
: t("common.you_are_downgraded_to_the_community_edition")}
|
|
||||||
</p>
|
</p>
|
||||||
|
<p className="mt-1 text-sm text-slate-500">{getDescription()}</p>
|
||||||
|
|
||||||
<Link href={`/environments/${environmentId}/settings/enterprise`}>
|
<Link href={`/environments/${environmentId}/settings/enterprise`}>
|
||||||
<span className="text-sm text-slate-900">{t("common.learn_more")}</span>
|
<span className="text-sm text-slate-900">{t("common.learn_more")}</span>
|
||||||
|
|||||||
@@ -259,6 +259,7 @@ export const PreviewSurvey = ({
|
|||||||
setBlockId = f;
|
setBlockId = f;
|
||||||
}}
|
}}
|
||||||
onFinished={onFinished}
|
onFinished={onFinished}
|
||||||
|
placement={placement}
|
||||||
isSpamProtectionEnabled={isSpamProtectionEnabled}
|
isSpamProtectionEnabled={isSpamProtectionEnabled}
|
||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
@@ -363,6 +364,7 @@ export const PreviewSurvey = ({
|
|||||||
}}
|
}}
|
||||||
onFinished={onFinished}
|
onFinished={onFinished}
|
||||||
isSpamProtectionEnabled={isSpamProtectionEnabled}
|
isSpamProtectionEnabled={isSpamProtectionEnabled}
|
||||||
|
placement={placement}
|
||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -170,7 +170,7 @@ export const getLanguageCode = (survey: TEnvironmentStateSurvey, language?: stri
|
|||||||
|
|
||||||
const selectedLanguage = survey.languages.find((surveyLanguage) => {
|
const selectedLanguage = survey.languages.find((surveyLanguage) => {
|
||||||
return (
|
return (
|
||||||
surveyLanguage.language.code === language.toLowerCase() ||
|
surveyLanguage.language.code.toLowerCase() === language.toLowerCase() ||
|
||||||
surveyLanguage.language.alias?.toLowerCase() === language.toLowerCase()
|
surveyLanguage.language.alias?.toLowerCase() === language.toLowerCase()
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import * as React from "react";
|
|||||||
import { ElementError } from "@/components/general/element-error";
|
import { ElementError } from "@/components/general/element-error";
|
||||||
import { ElementHeader } from "@/components/general/element-header";
|
import { ElementHeader } from "@/components/general/element-header";
|
||||||
import { Label } from "@/components/general/label";
|
import { Label } from "@/components/general/label";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn, getRTLScaleOptionClasses } from "@/lib/utils";
|
||||||
|
|
||||||
interface NPSProps {
|
interface NPSProps {
|
||||||
/** Unique identifier for the element container */
|
/** Unique identifier for the element container */
|
||||||
@@ -97,18 +97,9 @@ function NPS({
|
|||||||
const isLast = number === 10; // Last option is 10
|
const isLast = number === 10; // Last option is 10
|
||||||
const isFirst = number === 0; // First option is 0
|
const isFirst = number === 0; // First option is 0
|
||||||
|
|
||||||
// Determine border radius and border classes
|
// Use CSS logical properties for RTL-aware borders and border radius
|
||||||
// Use right border for all items to create separators, left border only on first item
|
// The fieldset's dir attribute automatically handles direction
|
||||||
let borderRadiusClasses = "";
|
const { borderRadiusClasses, borderClasses } = getRTLScaleOptionClasses(isFirst, isLast);
|
||||||
let borderClasses = "border-t border-b border-r";
|
|
||||||
|
|
||||||
if (isFirst) {
|
|
||||||
borderRadiusClasses = dir === "rtl" ? "rounded-r-input" : "rounded-l-input";
|
|
||||||
borderClasses = "border-t border-b border-l border-r";
|
|
||||||
} else if (isLast) {
|
|
||||||
borderRadiusClasses = dir === "rtl" ? "rounded-l-input" : "rounded-r-input";
|
|
||||||
// Last item keeps right border for rounded corner
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
// eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions -- label is interactive
|
// eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions -- label is interactive
|
||||||
@@ -183,7 +174,7 @@ function NPS({
|
|||||||
{/* NPS Options */}
|
{/* NPS Options */}
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<ElementError errorMessage={errorMessage} dir={dir} />
|
<ElementError errorMessage={errorMessage} dir={dir} />
|
||||||
<fieldset className="w-full px-[2px]">
|
<fieldset className="w-full px-[2px]" dir={dir}>
|
||||||
<legend className="sr-only">NPS rating options</legend>
|
<legend className="sr-only">NPS rating options</legend>
|
||||||
<div className="flex w-full">{npsOptions.map((number) => renderNPSOption(number))}</div>
|
<div className="flex w-full">{npsOptions.map((number) => renderNPSOption(number))}</div>
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import {
|
|||||||
TiredFace,
|
TiredFace,
|
||||||
WearyFace,
|
WearyFace,
|
||||||
} from "@/components/general/smileys";
|
} from "@/components/general/smileys";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn, getRTLScaleOptionClasses } from "@/lib/utils";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get smiley color class based on range and index
|
* Get smiley color class based on range and index
|
||||||
@@ -220,18 +220,9 @@ function Rating({
|
|||||||
const isLast = totalLength === number;
|
const isLast = totalLength === number;
|
||||||
const isFirst = number === 1;
|
const isFirst = number === 1;
|
||||||
|
|
||||||
// Determine border radius and border classes
|
// Use CSS logical properties for RTL-aware borders and border radius
|
||||||
// Use right border for all items to create separators, left border only on first item
|
// The parent div's dir attribute automatically handles direction
|
||||||
let borderRadiusClasses = "";
|
const { borderRadiusClasses, borderClasses } = getRTLScaleOptionClasses(isFirst, isLast);
|
||||||
let borderClasses = "border-t border-b border-r";
|
|
||||||
|
|
||||||
if (isFirst) {
|
|
||||||
borderRadiusClasses = dir === "rtl" ? "rounded-r-input" : "rounded-l-input";
|
|
||||||
borderClasses = "border-t border-b border-l border-r";
|
|
||||||
} else if (isLast) {
|
|
||||||
borderRadiusClasses = dir === "rtl" ? "rounded-l-input" : "rounded-r-input";
|
|
||||||
// Last item keeps right border for rounded corner
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
// eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions -- label is interactive
|
// eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions -- label is interactive
|
||||||
@@ -418,7 +409,7 @@ function Rating({
|
|||||||
{/* Rating Options */}
|
{/* Rating Options */}
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<ElementError errorMessage={errorMessage} dir={dir} />
|
<ElementError errorMessage={errorMessage} dir={dir} />
|
||||||
<fieldset className="w-full">
|
<fieldset className="w-full" dir={dir}>
|
||||||
<legend className="sr-only">Rating options</legend>
|
<legend className="sr-only">Rating options</legend>
|
||||||
<div className="flex w-full px-[2px]">
|
<div className="flex w-full px-[2px]">
|
||||||
{ratingOptions.map((number, index) => {
|
{ratingOptions.map((number, index) => {
|
||||||
|
|||||||
@@ -35,3 +35,29 @@ export const stripInlineStyles = (html: string): string => {
|
|||||||
KEEP_CONTENT: true,
|
KEEP_CONTENT: true,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate RTL-aware border radius and border classes for rating/NPS scale options
|
||||||
|
* Uses CSS logical properties that automatically adapt to text direction
|
||||||
|
* @param isFirst - Whether this is the first item in the scale
|
||||||
|
* @param isLast - Whether this is the last item in the scale
|
||||||
|
* @returns Object containing borderRadiusClasses and borderClasses
|
||||||
|
*/
|
||||||
|
export const getRTLScaleOptionClasses = (
|
||||||
|
isFirst: boolean,
|
||||||
|
isLast: boolean
|
||||||
|
): { borderRadiusClasses: string; borderClasses: string } => {
|
||||||
|
const borderRadiusClasses = cn(
|
||||||
|
isFirst &&
|
||||||
|
"[border-start-start-radius:var(--fb-input-border-radius)] [border-end-start-radius:var(--fb-input-border-radius)]",
|
||||||
|
isLast &&
|
||||||
|
"[border-start-end-radius:var(--fb-input-border-radius)] [border-end-end-radius:var(--fb-input-border-radius)]"
|
||||||
|
);
|
||||||
|
|
||||||
|
const borderClasses = cn(
|
||||||
|
"border-t border-b border-e", // block borders (top/bottom) and inline-end border
|
||||||
|
isFirst && "border-s" // inline-start border for first item
|
||||||
|
);
|
||||||
|
|
||||||
|
return { borderRadiusClasses, borderClasses };
|
||||||
|
};
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ export function Survey({
|
|||||||
isSpamProtectionEnabled,
|
isSpamProtectionEnabled,
|
||||||
dir = "auto",
|
dir = "auto",
|
||||||
setDir,
|
setDir,
|
||||||
|
placement,
|
||||||
}: SurveyContainerProps) {
|
}: SurveyContainerProps) {
|
||||||
let apiClient: ApiClient | null = null;
|
let apiClient: ApiClient | null = null;
|
||||||
|
|
||||||
@@ -916,6 +917,7 @@ export function Survey({
|
|||||||
setBlockId={setBlockId}
|
setBlockId={setBlockId}
|
||||||
shouldResetBlockId={shouldResetQuestionId}
|
shouldResetBlockId={shouldResetQuestionId}
|
||||||
fullSizeCards={fullSizeCards}
|
fullSizeCards={fullSizeCards}
|
||||||
|
placement={placement}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { MutableRef } from "preact/hooks";
|
|||||||
import { useEffect, useMemo, useState } from "preact/hooks";
|
import { useEffect, useMemo, useState } from "preact/hooks";
|
||||||
import { JSX } from "preact/jsx-runtime";
|
import { JSX } from "preact/jsx-runtime";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import { type TPlacement } from "@formbricks/types/common";
|
||||||
import { TJsEnvironmentStateSurvey } from "@formbricks/types/js";
|
import { TJsEnvironmentStateSurvey } from "@formbricks/types/js";
|
||||||
import { TCardArrangementOptions } from "@formbricks/types/styling";
|
import { TCardArrangementOptions } from "@formbricks/types/styling";
|
||||||
|
|
||||||
@@ -17,6 +18,7 @@ interface StackedCardProps {
|
|||||||
cardWidth: number;
|
cardWidth: number;
|
||||||
hovered: boolean;
|
hovered: boolean;
|
||||||
cardArrangement: TCardArrangementOptions;
|
cardArrangement: TCardArrangementOptions;
|
||||||
|
placement: TPlacement;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const StackedCard = ({
|
export const StackedCard = ({
|
||||||
@@ -31,17 +33,24 @@ export const StackedCard = ({
|
|||||||
cardWidth,
|
cardWidth,
|
||||||
hovered,
|
hovered,
|
||||||
cardArrangement,
|
cardArrangement,
|
||||||
|
placement,
|
||||||
}: StackedCardProps) => {
|
}: StackedCardProps) => {
|
||||||
const isHidden = offset < 0;
|
const isHidden = offset < 0;
|
||||||
const [delayedOffset, setDelayedOffset] = useState<number>(offset);
|
const [delayedOffset, setDelayedOffset] = useState<number>(offset);
|
||||||
const [contentOpacity, setContentOpacity] = useState<number>(0);
|
const [contentOpacity, setContentOpacity] = useState<number>(0);
|
||||||
const currentCardHeight = offset === 0 ? "auto" : offset < 0 ? "initial" : cardHeight;
|
const currentCardHeight = offset === 0 ? "auto" : offset < 0 ? "initial" : cardHeight;
|
||||||
|
|
||||||
const getBottomStyles = () => {
|
const getTopBottomStyles = () => {
|
||||||
if (survey.type !== "link")
|
if (survey.type !== "link")
|
||||||
return {
|
if (placement === "bottomLeft" || placement === "bottomRight") {
|
||||||
bottom: 0,
|
return {
|
||||||
};
|
bottom: 0,
|
||||||
|
};
|
||||||
|
} else if (placement === "topLeft" || placement === "topRight") {
|
||||||
|
return {
|
||||||
|
top: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getDummyCardContent = () => {
|
const getDummyCardContent = () => {
|
||||||
@@ -111,7 +120,7 @@ export const StackedCard = ({
|
|||||||
pointerEvents: offset === 0 ? "auto" : "none",
|
pointerEvents: offset === 0 ? "auto" : "none",
|
||||||
...borderStyles,
|
...borderStyles,
|
||||||
...straightCardArrangementStyles,
|
...straightCardArrangementStyles,
|
||||||
...getBottomStyles(),
|
...getTopBottomStyles(),
|
||||||
}}
|
}}
|
||||||
className="pointer rounded-custom bg-survey-bg absolute inset-x-0 overflow-hidden">
|
className="pointer rounded-custom bg-survey-bg absolute inset-x-0 overflow-hidden">
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useEffect, useMemo, useRef, useState } from "preact/hooks";
|
import { useEffect, useMemo, useRef, useState } from "preact/hooks";
|
||||||
import type { JSX } from "react";
|
import type { JSX } from "react";
|
||||||
|
import { type TPlacement } from "@formbricks/types/common";
|
||||||
import { type TJsEnvironmentStateSurvey } from "@formbricks/types/js";
|
import { type TJsEnvironmentStateSurvey } from "@formbricks/types/js";
|
||||||
import { type TProjectStyling } from "@formbricks/types/project";
|
import { type TProjectStyling } from "@formbricks/types/project";
|
||||||
import { type TCardArrangementOptions } from "@formbricks/types/styling";
|
import { type TCardArrangementOptions } from "@formbricks/types/styling";
|
||||||
@@ -19,6 +20,7 @@ interface StackedCardsContainerProps {
|
|||||||
setBlockId: (blockId: string) => void;
|
setBlockId: (blockId: string) => void;
|
||||||
shouldResetBlockId?: boolean;
|
shouldResetBlockId?: boolean;
|
||||||
fullSizeCards: boolean;
|
fullSizeCards: boolean;
|
||||||
|
placement?: TPlacement;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function StackedCardsContainer({
|
export function StackedCardsContainer({
|
||||||
@@ -30,6 +32,7 @@ export function StackedCardsContainer({
|
|||||||
setBlockId,
|
setBlockId,
|
||||||
shouldResetBlockId = true,
|
shouldResetBlockId = true,
|
||||||
fullSizeCards = false,
|
fullSizeCards = false,
|
||||||
|
placement = "bottomRight",
|
||||||
}: Readonly<StackedCardsContainerProps>) {
|
}: Readonly<StackedCardsContainerProps>) {
|
||||||
const [hovered, setHovered] = useState(false);
|
const [hovered, setHovered] = useState(false);
|
||||||
const highlightBorderColor = survey.styling?.overwriteThemeStyling
|
const highlightBorderColor = survey.styling?.overwriteThemeStyling
|
||||||
@@ -179,6 +182,7 @@ export function StackedCardsContainer({
|
|||||||
cardWidth={cardWidth}
|
cardWidth={cardWidth}
|
||||||
hovered={hovered}
|
hovered={hovered}
|
||||||
cardArrangement={cardArrangement}
|
cardArrangement={cardArrangement}
|
||||||
|
placement={placement}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -36,18 +36,35 @@ export const renderSurvey = (props: SurveyContainerProps) => {
|
|||||||
throw new Error(`renderSurvey: Element with id ${containerId} not found.`);
|
throw new Error(`renderSurvey: Element with id ${containerId} not found.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { placement, darkOverlay, onClose, clickOutside, ...surveyInlineProps } = props;
|
// if survey type is link, we don't pass the placement, darkOverlay, clickOutside, onClose
|
||||||
|
if (props.survey.type === "link") {
|
||||||
|
const { placement, darkOverlay, onClose, clickOutside, ...surveyInlineProps } = props;
|
||||||
|
|
||||||
render(
|
render(
|
||||||
h(
|
h(
|
||||||
I18nProvider,
|
I18nProvider,
|
||||||
{ language },
|
{ language },
|
||||||
h(RenderSurvey, {
|
h(RenderSurvey, {
|
||||||
...surveyInlineProps,
|
...surveyInlineProps,
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
element
|
element
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
// For non-link surveys, pass placement through so it can be used in StackedCard
|
||||||
|
const { darkOverlay, onClose, clickOutside, ...surveyInlineProps } = props;
|
||||||
|
|
||||||
|
render(
|
||||||
|
h(
|
||||||
|
I18nProvider,
|
||||||
|
{ language },
|
||||||
|
h(RenderSurvey, {
|
||||||
|
...surveyInlineProps,
|
||||||
|
})
|
||||||
|
),
|
||||||
|
element
|
||||||
|
);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
const modalContainer = document.createElement("div");
|
const modalContainer = document.createElement("div");
|
||||||
modalContainer.id = "formbricks-modal-container";
|
modalContainer.id = "formbricks-modal-container";
|
||||||
|
|||||||
Reference in New Issue
Block a user