mirror of
https://github.com/formbricks/formbricks.git
synced 2026-04-22 11:29:22 -05:00
feat: custom favicon (#7044)
This commit is contained in:
+26
-2
@@ -1,8 +1,11 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import { IS_FORMBRICKS_CLOUD } from "@/lib/constants";
|
||||
import { IS_FORMBRICKS_CLOUD, IS_STORAGE_CONFIGURED } from "@/lib/constants";
|
||||
import { getTranslate } from "@/lingodotdev/server";
|
||||
import { getWhiteLabelPermission } from "@/modules/ee/license-check/lib/utils";
|
||||
import { FaviconCustomizationSettings } from "@/modules/ee/whitelabel/favicon-customization/components/favicon-customization-settings";
|
||||
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
|
||||
import { getSurveysWithSlugsByOrganizationId } from "@/modules/survey/lib/slug";
|
||||
import { Alert, AlertDescription } from "@/modules/ui/components/alert";
|
||||
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
||||
import { PageHeader } from "@/modules/ui/components/page-header";
|
||||
import { SettingsCard } from "../../components/SettingsCard";
|
||||
@@ -17,12 +20,17 @@ const Page = async (props: { params: Promise<{ environmentId: string }> }) => {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
const { session, currentUserMembership, organization } = await getEnvironmentAuth(params.environmentId);
|
||||
const { session, currentUserMembership, organization, isOwner, isManager } = await getEnvironmentAuth(
|
||||
params.environmentId
|
||||
);
|
||||
|
||||
if (!session) {
|
||||
throw new Error(t("common.session_not_found"));
|
||||
}
|
||||
|
||||
const hasWhiteLabelPermission = await getWhiteLabelPermission(organization.billing.plan);
|
||||
const isOwnerOrManager = isManager || isOwner;
|
||||
|
||||
const result = await getSurveysWithSlugsByOrganizationId(organization.id);
|
||||
if (!result.ok) {
|
||||
throw new Error(t("common.something_went_wrong"));
|
||||
@@ -41,6 +49,22 @@ const Page = async (props: { params: Promise<{ environmentId: string }> }) => {
|
||||
/>
|
||||
</PageHeader>
|
||||
|
||||
{!IS_STORAGE_CONFIGURED && (
|
||||
<div className="max-w-4xl">
|
||||
<Alert variant="warning">
|
||||
<AlertDescription>{t("common.storage_not_configured")}</AlertDescription>
|
||||
</Alert>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<FaviconCustomizationSettings
|
||||
organization={organization}
|
||||
hasWhiteLabelPermission={hasWhiteLabelPermission}
|
||||
environmentId={params.environmentId}
|
||||
isReadOnly={!isOwnerOrManager}
|
||||
isStorageConfigured={IS_STORAGE_CONFIGURED}
|
||||
/>
|
||||
|
||||
<SettingsCard
|
||||
title={t("environments.settings.domain.title")}
|
||||
description={t("environments.settings.domain.description")}>
|
||||
|
||||
@@ -322,6 +322,7 @@ checksums:
|
||||
common/remove: dba2fe5fe9f83f8078c687f28cba4b52
|
||||
common/remove_from_team: 69bcc7a1001c3017f9de578ee22cffd6
|
||||
common/reorder_and_hide_columns: a5e3d7c0c7ef879211d05a37be1c5069
|
||||
common/replace: 98b2268975b1a737b2e4ad837df96703
|
||||
common/report_survey: 147dd05db52e35f5d1f837460fb720f5
|
||||
common/request_pricing: 58eb24af4f098632709cb7482b70a1cb
|
||||
common/request_trial_license: 560df1240ef621f7c60d3f7d65422ccd
|
||||
@@ -980,7 +981,15 @@ checksums:
|
||||
environments/settings/billing/upgrade: 63c3b52882e0d779859307d672c178c2
|
||||
environments/settings/billing/uptime_sla_99: 25ca4060e575e1a7eee47fceb5576d7c
|
||||
environments/settings/billing/website_surveys: f4d176cc66ffcc2abf44c0d5da1642e3
|
||||
environments/settings/domain/customize_favicon_description: d3ac29934a66fd56294c0d8069fbc11e
|
||||
environments/settings/domain/customize_favicon_with_higher_plan: 43a6b834a8fd013c52923863d62248f3
|
||||
environments/settings/domain/description: f0b4d8c96da816f793cf1f4fdfaade34
|
||||
environments/settings/domain/favicon_customization: 5c975be07a2a8e319c2c58c7d8c0c78f
|
||||
environments/settings/domain/favicon_customization_description: 26c3310c2d85a3daefa3c19c01353efa
|
||||
environments/settings/domain/favicon_removed_successfully: cebbf050b77dd7218b622d3a4cc4aa18
|
||||
environments/settings/domain/favicon_saved_successfully: a6fbbe2f87b241bc5f661896299fc2b6
|
||||
environments/settings/domain/favicon_size_hint: b3a35625898353380152de0be17dd5f7
|
||||
environments/settings/domain/favicon_too_large: 82a014c0eba8ea8029fd76f61b795984
|
||||
environments/settings/domain/no_pretty_urls: 2a83d8e399d325fcd4e2db14e45b1a86
|
||||
environments/settings/domain/pretty_url: 10a4b387b6df844245fc842be29527e3
|
||||
environments/settings/domain/project: e13002ec4570f3fcc2f050f5ce974294
|
||||
|
||||
@@ -349,6 +349,7 @@
|
||||
"remove": "Entfernen",
|
||||
"remove_from_team": "Aus Team entfernen",
|
||||
"reorder_and_hide_columns": "Spalten neu anordnen und ausblenden",
|
||||
"replace": "Ersetzen",
|
||||
"report_survey": "Umfrage melden",
|
||||
"request_pricing": "Preise anfragen",
|
||||
"request_trial_license": "Testlizenz anfordern",
|
||||
@@ -1053,7 +1054,15 @@
|
||||
"website_surveys": "Website-Umfragen"
|
||||
},
|
||||
"domain": {
|
||||
"customize_favicon_description": "Lade ein individuelles Favicon hoch, um deine Link-Umfrage zu personalisieren und deine Markenpräsenz zu stärken.",
|
||||
"customize_favicon_with_higher_plan": "Favicon mit einem höheren Plan anpassen",
|
||||
"description": "Übersicht aller Umfragen mit Pretty URLs in Ihrer Organisation",
|
||||
"favicon_customization": "Favicon",
|
||||
"favicon_customization_description": "Passe das Favicon an, das in Browser-Tabs für deine Link-Umfragen angezeigt wird. Dies wirkt sich nicht auf die Admin-App aus.",
|
||||
"favicon_removed_successfully": "Favicon erfolgreich entfernt",
|
||||
"favicon_saved_successfully": "Favicon erfolgreich gespeichert",
|
||||
"favicon_size_hint": "Max. 100 KB, quadratische Bilder funktionieren am besten",
|
||||
"favicon_too_large": "Favicon muss unter 100 KB sein",
|
||||
"no_pretty_urls": "Noch keine Umfragen mit Pretty URLs konfiguriert.",
|
||||
"pretty_url": "Pretty URL",
|
||||
"project": "Projekt",
|
||||
|
||||
@@ -349,6 +349,7 @@
|
||||
"remove": "Remove",
|
||||
"remove_from_team": "Remove from team",
|
||||
"reorder_and_hide_columns": "Reorder and hide columns",
|
||||
"replace": "Replace",
|
||||
"report_survey": "Report Survey",
|
||||
"request_pricing": "Request Pricing",
|
||||
"request_trial_license": "Request trial license",
|
||||
@@ -1053,7 +1054,15 @@
|
||||
"website_surveys": "Website Surveys"
|
||||
},
|
||||
"domain": {
|
||||
"customize_favicon_description": "Upload a custom favicon to personalize your link survey experience and strengthen your brand presence.",
|
||||
"customize_favicon_with_higher_plan": "Customize favicon with a higher plan",
|
||||
"description": "Overview of all surveys using Pretty URLs across your organization",
|
||||
"favicon_customization": "Favicon",
|
||||
"favicon_customization_description": "Customize the favicon displayed in browser tabs for your link surveys. This won't affect the admin app.",
|
||||
"favicon_removed_successfully": "Favicon removed successfully",
|
||||
"favicon_saved_successfully": "Favicon saved successfully",
|
||||
"favicon_size_hint": "Max 100KB, square images work best",
|
||||
"favicon_too_large": "Favicon must be under 100KB",
|
||||
"no_pretty_urls": "No surveys with Pretty URLs configured yet.",
|
||||
"pretty_url": "Pretty URL",
|
||||
"project": "Project",
|
||||
|
||||
@@ -349,6 +349,7 @@
|
||||
"remove": "Eliminar",
|
||||
"remove_from_team": "Eliminar del equipo",
|
||||
"reorder_and_hide_columns": "Reordenar y ocultar columnas",
|
||||
"replace": "Reemplazar",
|
||||
"report_survey": "Reportar encuesta",
|
||||
"request_pricing": "Solicitar precios",
|
||||
"request_trial_license": "Solicitar licencia de prueba",
|
||||
@@ -1053,7 +1054,15 @@
|
||||
"website_surveys": "Encuestas de sitio web"
|
||||
},
|
||||
"domain": {
|
||||
"customize_favicon_description": "Sube un favicon personalizado para personalizar la experiencia de tu encuesta por enlace y fortalecer la presencia de tu marca.",
|
||||
"customize_favicon_with_higher_plan": "Personaliza el favicon con un plan superior",
|
||||
"description": "Resumen de todas las encuestas que utilizan URL bonitas en tu organización",
|
||||
"favicon_customization": "Favicon",
|
||||
"favicon_customization_description": "Personaliza el favicon que se muestra en las pestañas del navegador para tus encuestas por enlace. Esto no afectará a la aplicación de administración.",
|
||||
"favicon_removed_successfully": "Favicon eliminado correctamente",
|
||||
"favicon_saved_successfully": "Favicon guardado correctamente",
|
||||
"favicon_size_hint": "Máximo 100 KB, las imágenes cuadradas funcionan mejor",
|
||||
"favicon_too_large": "El favicon debe ser inferior a 100 KB",
|
||||
"no_pretty_urls": "Aún no hay encuestas con URL bonitas configuradas.",
|
||||
"pretty_url": "URL bonita",
|
||||
"project": "Proyecto",
|
||||
|
||||
@@ -349,6 +349,7 @@
|
||||
"remove": "Retirer",
|
||||
"remove_from_team": "Retirer de l'équipe",
|
||||
"reorder_and_hide_columns": "Réorganiser et masquer des colonnes",
|
||||
"replace": "Remplacer",
|
||||
"report_survey": "Rapport d'enquête",
|
||||
"request_pricing": "Connaître le tarif",
|
||||
"request_trial_license": "Demander une licence d'essai",
|
||||
@@ -1053,7 +1054,15 @@
|
||||
"website_surveys": "Sondages de site web"
|
||||
},
|
||||
"domain": {
|
||||
"customize_favicon_description": "Chargez un favicon personnalisé pour personnaliser l'expérience de vos enquêtes par lien et renforcer la présence de votre marque.",
|
||||
"customize_favicon_with_higher_plan": "Personnaliser le favicon avec un forfait supérieur",
|
||||
"description": "Aperçu de toutes les enquêtes utilisant des URL personnalisées dans votre organisation",
|
||||
"favicon_customization": "Favicon",
|
||||
"favicon_customization_description": "Personnalisez le favicon affiché dans les onglets du navigateur pour vos enquêtes par lien. Cela n'affectera pas l'application d'administration.",
|
||||
"favicon_removed_successfully": "Favicon supprimé avec succès",
|
||||
"favicon_saved_successfully": "Favicon enregistré avec succès",
|
||||
"favicon_size_hint": "Max 100 Ko, les images carrées fonctionnent mieux",
|
||||
"favicon_too_large": "Le favicon doit faire moins de 100 Ko",
|
||||
"no_pretty_urls": "Aucune enquête avec URL personnalisée configurée pour le moment.",
|
||||
"pretty_url": "URL personnalisée",
|
||||
"project": "Projet",
|
||||
|
||||
@@ -349,6 +349,7 @@
|
||||
"remove": "削除",
|
||||
"remove_from_team": "チームから削除",
|
||||
"reorder_and_hide_columns": "列の並び替えと非表示",
|
||||
"replace": "置き換え",
|
||||
"report_survey": "フォームを報告",
|
||||
"request_pricing": "料金を問い合わせる",
|
||||
"request_trial_license": "トライアルライセンスをリクエスト",
|
||||
@@ -1053,7 +1054,15 @@
|
||||
"website_surveys": "ウェブサイトフォーム"
|
||||
},
|
||||
"domain": {
|
||||
"customize_favicon_description": "カスタムファビコンをアップロードして、リンク調査の体験をパーソナライズし、ブランドプレゼンスを強化します。",
|
||||
"customize_favicon_with_higher_plan": "上位プランでファビコンをカスタマイズ",
|
||||
"description": "組織全体でカスタムURLを使用しているすべてのフォームの概要",
|
||||
"favicon_customization": "ファビコン",
|
||||
"favicon_customization_description": "リンク調査のブラウザタブに表示されるファビコンをカスタマイズします。管理アプリには影響しません。",
|
||||
"favicon_removed_successfully": "ファビコンを削除しました",
|
||||
"favicon_saved_successfully": "ファビコンを保存しました",
|
||||
"favicon_size_hint": "最大100KB、正方形の画像が最適です",
|
||||
"favicon_too_large": "ファビコンは100KB以下にする必要があります",
|
||||
"no_pretty_urls": "カスタムURLが設定されたフォームはまだありません。",
|
||||
"pretty_url": "カスタムURL",
|
||||
"project": "プロジェクト",
|
||||
|
||||
@@ -349,6 +349,7 @@
|
||||
"remove": "Verwijderen",
|
||||
"remove_from_team": "Verwijderen uit team",
|
||||
"reorder_and_hide_columns": "Kolommen opnieuw rangschikken en verbergen",
|
||||
"replace": "Vervangen",
|
||||
"report_survey": "Verslag enquête",
|
||||
"request_pricing": "Vraag prijzen aan",
|
||||
"request_trial_license": "Proeflicentie aanvragen",
|
||||
@@ -1053,7 +1054,15 @@
|
||||
"website_surveys": "Website-enquêtes"
|
||||
},
|
||||
"domain": {
|
||||
"customize_favicon_description": "Upload een aangepaste favicon om je linkenquête-ervaring te personaliseren en je merkpresentie te versterken.",
|
||||
"customize_favicon_with_higher_plan": "Pas favicon aan met een hoger abonnement",
|
||||
"description": "Overzicht van alle enquêtes die Pretty URL's gebruiken binnen je organisatie",
|
||||
"favicon_customization": "Favicon",
|
||||
"favicon_customization_description": "Pas de favicon aan die wordt weergegeven in browsertabbladen voor je linkenquêtes. Dit heeft geen invloed op de beheer-app.",
|
||||
"favicon_removed_successfully": "Favicon succesvol verwijderd",
|
||||
"favicon_saved_successfully": "Favicon succesvol opgeslagen",
|
||||
"favicon_size_hint": "Max. 100 KB, vierkante afbeeldingen werken het beste",
|
||||
"favicon_too_large": "Favicon moet kleiner zijn dan 100 KB",
|
||||
"no_pretty_urls": "Nog geen enquêtes met Pretty URL's geconfigureerd.",
|
||||
"pretty_url": "Pretty URL",
|
||||
"project": "Project",
|
||||
|
||||
@@ -349,6 +349,7 @@
|
||||
"remove": "remover",
|
||||
"remove_from_team": "Remover da equipe",
|
||||
"reorder_and_hide_columns": "Reordenar e ocultar colunas",
|
||||
"replace": "Substituir",
|
||||
"report_survey": "Relatório de Pesquisa",
|
||||
"request_pricing": "Solicitar Preços",
|
||||
"request_trial_license": "Pedir licença de teste",
|
||||
@@ -1053,7 +1054,15 @@
|
||||
"website_surveys": "Pesquisas de Site"
|
||||
},
|
||||
"domain": {
|
||||
"customize_favicon_description": "Faça o upload de um favicon personalizado para personalizar a experiência da sua pesquisa por link e fortalecer a presença da sua marca.",
|
||||
"customize_favicon_with_higher_plan": "Personalize o favicon com um plano superior",
|
||||
"description": "Visão geral de todas as pesquisas usando URLs amigáveis em sua organização",
|
||||
"favicon_customization": "Favicon",
|
||||
"favicon_customization_description": "Personalize o favicon exibido nas abas do navegador para suas pesquisas por link. Isso não afetará o aplicativo administrativo.",
|
||||
"favicon_removed_successfully": "Favicon removido com sucesso",
|
||||
"favicon_saved_successfully": "Favicon salvo com sucesso",
|
||||
"favicon_size_hint": "Máximo de 100KB, imagens quadradas funcionam melhor",
|
||||
"favicon_too_large": "O favicon deve ter menos de 100KB",
|
||||
"no_pretty_urls": "Nenhuma pesquisa com URLs amigáveis configuradas ainda.",
|
||||
"pretty_url": "URL amigável",
|
||||
"project": "Projeto",
|
||||
|
||||
@@ -349,6 +349,7 @@
|
||||
"remove": "Remover",
|
||||
"remove_from_team": "Remover da equipa",
|
||||
"reorder_and_hide_columns": "Reordenar e ocultar colunas",
|
||||
"replace": "Substituir",
|
||||
"report_survey": "Relatório de Inquérito",
|
||||
"request_pricing": "Pedido de Preços",
|
||||
"request_trial_license": "Solicitar licença de teste",
|
||||
@@ -1053,7 +1054,15 @@
|
||||
"website_surveys": "Inquéritos (site)"
|
||||
},
|
||||
"domain": {
|
||||
"customize_favicon_description": "Carregue um favicon personalizado para personalizar a experiência do seu inquérito por link e reforçar a presença da sua marca.",
|
||||
"customize_favicon_with_higher_plan": "Personalize o favicon com um plano superior",
|
||||
"description": "Visão geral de todos os inquéritos que utilizam URLs amigáveis na sua organização",
|
||||
"favicon_customization": "Favicon",
|
||||
"favicon_customization_description": "Personalize o favicon apresentado nos separadores do navegador para os seus inquéritos por link. Isto não afetará a aplicação de administração.",
|
||||
"favicon_removed_successfully": "Favicon removido com sucesso",
|
||||
"favicon_saved_successfully": "Favicon guardado com sucesso",
|
||||
"favicon_size_hint": "Máx. 100KB, imagens quadradas funcionam melhor",
|
||||
"favicon_too_large": "O favicon deve ter menos de 100KB",
|
||||
"no_pretty_urls": "Ainda não existem inquéritos com URLs amigáveis configurados.",
|
||||
"pretty_url": "URL amigável",
|
||||
"project": "Projeto",
|
||||
|
||||
@@ -349,6 +349,7 @@
|
||||
"remove": "Șterge",
|
||||
"remove_from_team": "Elimină din echipă",
|
||||
"reorder_and_hide_columns": "Reordonați și ascundeți coloanele",
|
||||
"replace": "Înlocuiește",
|
||||
"report_survey": "Raportează chestionarul",
|
||||
"request_pricing": "Solicită Prețuri",
|
||||
"request_trial_license": "Solicitați o licență de încercare",
|
||||
@@ -1053,7 +1054,15 @@
|
||||
"website_surveys": "Sondaje ale site-ului"
|
||||
},
|
||||
"domain": {
|
||||
"customize_favicon_description": "Încărcați un favicon personalizat pentru a oferi o experiență unică sondajului de linkuri și pentru a consolida prezența brandului.",
|
||||
"customize_favicon_with_higher_plan": "Personalizați faviconul cu un plan superior",
|
||||
"description": "Prezentare generală a tuturor chestionarelor care folosesc Pretty URLs în organizația dvs.",
|
||||
"favicon_customization": "Favicon",
|
||||
"favicon_customization_description": "Personalizați faviconul afișat în filele browserului pentru sondajele de linkuri. Acest lucru nu va afecta aplicația de administrare.",
|
||||
"favicon_removed_successfully": "Faviconul a fost eliminat cu succes",
|
||||
"favicon_saved_successfully": "Faviconul a fost salvat cu succes",
|
||||
"favicon_size_hint": "Maxim 100KB, imaginile pătrate se afișează cel mai bine",
|
||||
"favicon_too_large": "Faviconul trebuie să aibă sub 100KB",
|
||||
"no_pretty_urls": "Nu există încă chestionare cu Pretty URLs configurate.",
|
||||
"pretty_url": "Pretty URL",
|
||||
"project": "Proiect",
|
||||
|
||||
@@ -349,6 +349,7 @@
|
||||
"remove": "Удалить",
|
||||
"remove_from_team": "Удалить из команды",
|
||||
"reorder_and_hide_columns": "Изменить порядок и скрыть столбцы",
|
||||
"replace": "Заменить",
|
||||
"report_survey": "Пожаловаться на опрос",
|
||||
"request_pricing": "Запросить стоимость",
|
||||
"request_trial_license": "Запросить пробную лицензию",
|
||||
@@ -1053,7 +1054,15 @@
|
||||
"website_surveys": "Опросы на сайте"
|
||||
},
|
||||
"domain": {
|
||||
"customize_favicon_description": "Загрузите свой favicon, чтобы персонализировать опросы по ссылке и усилить узнаваемость бренда.",
|
||||
"customize_favicon_with_higher_plan": "Настройка favicon доступна на более продвинутом тарифе",
|
||||
"description": "Обзор всех опросов с Pretty URL в вашей организации",
|
||||
"favicon_customization": "Favicon",
|
||||
"favicon_customization_description": "Настройте favicon, который отображается на вкладках браузера для ваших опросов по ссылке. Это не повлияет на административное приложение.",
|
||||
"favicon_removed_successfully": "Favicon успешно удалён",
|
||||
"favicon_saved_successfully": "Favicon успешно сохранён",
|
||||
"favicon_size_hint": "Максимум 100 КБ, лучше всего подходят квадратные изображения",
|
||||
"favicon_too_large": "Размер favicon должен быть не более 100 КБ",
|
||||
"no_pretty_urls": "Пока не настроено ни одного опроса с Pretty URL.",
|
||||
"pretty_url": "Pretty URL",
|
||||
"project": "Проект",
|
||||
|
||||
@@ -349,6 +349,7 @@
|
||||
"remove": "Ta bort",
|
||||
"remove_from_team": "Ta bort från teamet",
|
||||
"reorder_and_hide_columns": "Ordna om och dölj kolumner",
|
||||
"replace": "Ersätt",
|
||||
"report_survey": "Rapportera enkät",
|
||||
"request_pricing": "Begär prissättning",
|
||||
"request_trial_license": "Begär provlicens",
|
||||
@@ -1053,7 +1054,15 @@
|
||||
"website_surveys": "Webbplatsenkäter"
|
||||
},
|
||||
"domain": {
|
||||
"customize_favicon_description": "Ladda upp en egen favicon för att anpassa din länkenkät och stärka ditt varumärke.",
|
||||
"customize_favicon_with_higher_plan": "Anpassa favicon med ett högre abonnemang",
|
||||
"description": "Översikt över alla enkäter som använder Pretty URLs i din organisation",
|
||||
"favicon_customization": "Favicon",
|
||||
"favicon_customization_description": "Anpassa faviconen som visas i webbläsarflikar för dina länkenkäter. Detta påverkar inte admin-appen.",
|
||||
"favicon_removed_successfully": "Favicon har tagits bort",
|
||||
"favicon_saved_successfully": "Favicon har sparats",
|
||||
"favicon_size_hint": "Max 100 KB, fyrkantiga bilder fungerar bäst",
|
||||
"favicon_too_large": "Favicon får inte vara större än 100 KB",
|
||||
"no_pretty_urls": "Inga enkäter med Pretty URLs har konfigurerats ännu.",
|
||||
"pretty_url": "Pretty URL",
|
||||
"project": "Projekt",
|
||||
|
||||
@@ -349,6 +349,7 @@
|
||||
"remove": "移除",
|
||||
"remove_from_team": "从团队中移除",
|
||||
"reorder_and_hide_columns": "重新排序和隐藏列",
|
||||
"replace": "替换",
|
||||
"report_survey": "报告调查",
|
||||
"request_pricing": "请求 定价",
|
||||
"request_trial_license": "申请试用许可证",
|
||||
@@ -1053,7 +1054,15 @@
|
||||
"website_surveys": "网站 调查"
|
||||
},
|
||||
"domain": {
|
||||
"customize_favicon_description": "上传自定义 Favicon,个性化您的链接问卷体验,提升品牌形象。",
|
||||
"customize_favicon_with_higher_plan": "升级套餐以自定义 Favicon",
|
||||
"description": "概览组织内所有使用美化 URL 的调查问卷",
|
||||
"favicon_customization": "Favicon",
|
||||
"favicon_customization_description": "自定义在浏览器标签页中显示的链接问卷 Favicon。此操作不会影响管理端应用。",
|
||||
"favicon_removed_successfully": "Favicon 移除成功",
|
||||
"favicon_saved_successfully": "Favicon 保存成功",
|
||||
"favicon_size_hint": "最大 100KB,建议使用方形图片",
|
||||
"favicon_too_large": "Favicon 必须小于 100KB",
|
||||
"no_pretty_urls": "尚未配置任何美化 URL 的调查问卷。",
|
||||
"pretty_url": "美化 URL",
|
||||
"project": "项目",
|
||||
|
||||
@@ -349,6 +349,7 @@
|
||||
"remove": "移除",
|
||||
"remove_from_team": "從團隊中移除",
|
||||
"reorder_and_hide_columns": "重新排序和隱藏欄位",
|
||||
"replace": "取代",
|
||||
"report_survey": "報告問卷",
|
||||
"request_pricing": "請求定價",
|
||||
"request_trial_license": "請求試用授權",
|
||||
@@ -1053,7 +1054,15 @@
|
||||
"website_surveys": "網站問卷"
|
||||
},
|
||||
"domain": {
|
||||
"customize_favicon_description": "上傳自訂 Favicon,讓您的連結問卷體驗更具個人化,並強化品牌形象。",
|
||||
"customize_favicon_with_higher_plan": "升級方案以自訂 Favicon",
|
||||
"description": "檢視貴組織內所有使用 Pretty URL 的問卷",
|
||||
"favicon_customization": "Favicon",
|
||||
"favicon_customization_description": "自訂顯示於瀏覽器分頁上的連結問卷 Favicon。這不會影響管理後台應用程式。",
|
||||
"favicon_removed_successfully": "Favicon 已成功移除",
|
||||
"favicon_saved_successfully": "Favicon 已成功儲存",
|
||||
"favicon_size_hint": "最大 100KB,建議使用正方形圖片",
|
||||
"favicon_too_large": "Favicon 必須小於 100KB",
|
||||
"no_pretty_urls": "尚未設定任何使用 Pretty URL 的問卷。",
|
||||
"pretty_url": "Pretty URL",
|
||||
"project": "專案",
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
"use server";
|
||||
|
||||
import { z } from "zod";
|
||||
import { ZId, ZUrl } from "@formbricks/types/common";
|
||||
import { authenticatedActionClient } from "@/lib/utils/action-client";
|
||||
import { checkAuthorizationUpdated } from "@/lib/utils/action-client/action-client-middleware";
|
||||
import { AuthenticatedActionClientCtx } from "@/lib/utils/action-client/types/context";
|
||||
import { withAuditLogging } from "@/modules/ee/audit-logs/lib/handler";
|
||||
import { checkWhiteLabelPermission } from "@/modules/ee/whitelabel/email-customization/actions";
|
||||
import { updateOrganizationFaviconUrl } from "@/modules/ee/whitelabel/favicon-customization/lib/organization";
|
||||
|
||||
const ZUpdateOrganizationFaviconUrlAction = z.object({
|
||||
organizationId: ZId,
|
||||
faviconUrl: ZUrl,
|
||||
});
|
||||
|
||||
export const updateOrganizationFaviconUrlAction = authenticatedActionClient
|
||||
.schema(ZUpdateOrganizationFaviconUrlAction)
|
||||
.action(
|
||||
withAuditLogging(
|
||||
"updated",
|
||||
"organization",
|
||||
async ({
|
||||
ctx,
|
||||
parsedInput,
|
||||
}: {
|
||||
ctx: AuthenticatedActionClientCtx;
|
||||
parsedInput: z.infer<typeof ZUpdateOrganizationFaviconUrlAction>;
|
||||
}) => {
|
||||
const { organizationId, faviconUrl } = parsedInput;
|
||||
|
||||
await checkAuthorizationUpdated({
|
||||
userId: ctx.user.id,
|
||||
organizationId,
|
||||
access: [
|
||||
{
|
||||
type: "organization",
|
||||
roles: ["owner", "manager"],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await checkWhiteLabelPermission(organizationId);
|
||||
|
||||
ctx.auditLoggingCtx.organizationId = organizationId;
|
||||
ctx.auditLoggingCtx.newObject = { faviconUrl };
|
||||
|
||||
return await updateOrganizationFaviconUrl(organizationId, faviconUrl);
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
const ZRemoveOrganizationFaviconUrlAction = z.object({
|
||||
organizationId: ZId,
|
||||
});
|
||||
|
||||
export const removeOrganizationFaviconUrlAction = authenticatedActionClient
|
||||
.schema(ZRemoveOrganizationFaviconUrlAction)
|
||||
.action(
|
||||
withAuditLogging(
|
||||
"updated",
|
||||
"organization",
|
||||
async ({
|
||||
ctx,
|
||||
parsedInput,
|
||||
}: {
|
||||
ctx: AuthenticatedActionClientCtx;
|
||||
parsedInput: z.infer<typeof ZRemoveOrganizationFaviconUrlAction>;
|
||||
}) => {
|
||||
const { organizationId } = parsedInput;
|
||||
|
||||
await checkAuthorizationUpdated({
|
||||
userId: ctx.user.id,
|
||||
organizationId,
|
||||
access: [{ type: "organization", roles: ["owner", "manager"] }],
|
||||
});
|
||||
|
||||
await checkWhiteLabelPermission(organizationId);
|
||||
|
||||
ctx.auditLoggingCtx.organizationId = organizationId;
|
||||
ctx.auditLoggingCtx.oldObject = { faviconUrl: "" };
|
||||
|
||||
return await updateOrganizationFaviconUrl(organizationId, null);
|
||||
}
|
||||
)
|
||||
);
|
||||
+246
@@ -0,0 +1,246 @@
|
||||
"use client";
|
||||
|
||||
import Image from "next/image";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { type ChangeEvent, useRef, useState } from "react";
|
||||
import { toast } from "react-hot-toast";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TOrganization } from "@formbricks/types/organizations";
|
||||
import { TAllowedFileExtension } from "@formbricks/types/storage";
|
||||
import { SettingsCard } from "@/app/(app)/environments/[environmentId]/settings/components/SettingsCard";
|
||||
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
||||
import {
|
||||
removeOrganizationFaviconUrlAction,
|
||||
updateOrganizationFaviconUrlAction,
|
||||
} from "@/modules/ee/whitelabel/favicon-customization/actions";
|
||||
import { handleFileUpload } from "@/modules/storage/file-upload";
|
||||
import { Alert, AlertDescription } from "@/modules/ui/components/alert";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { FileInput } from "@/modules/ui/components/file-input";
|
||||
import { Input } from "@/modules/ui/components/input";
|
||||
import { showStorageNotConfiguredToast } from "@/modules/ui/components/storage-not-configured-toast/lib/utils";
|
||||
import { ModalButton, UpgradePrompt } from "@/modules/ui/components/upgrade-prompt";
|
||||
|
||||
// Favicon recommended formats - PNG and ICO are most widely supported
|
||||
const allowedFileExtensions: TAllowedFileExtension[] = ["png", "jpeg", "jpg", "ico", "webp"];
|
||||
|
||||
// Maximum favicon size: 512x512 for high-DPI displays
|
||||
// File size limit: 100KB (realistically favicons should be much smaller)
|
||||
const MAX_FAVICON_SIZE_MB = 0.1; // 100KB
|
||||
|
||||
interface FaviconCustomizationSettingsProps {
|
||||
organization: TOrganization;
|
||||
hasWhiteLabelPermission: boolean;
|
||||
environmentId: string;
|
||||
isReadOnly: boolean;
|
||||
isStorageConfigured: boolean;
|
||||
}
|
||||
|
||||
export const FaviconCustomizationSettings = ({
|
||||
organization,
|
||||
hasWhiteLabelPermission,
|
||||
environmentId,
|
||||
isReadOnly,
|
||||
isStorageConfigured,
|
||||
}: FaviconCustomizationSettingsProps) => {
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
|
||||
const [faviconUrl, setFaviconUrl] = useState<string | undefined>(
|
||||
organization.whitelabel?.faviconUrl || undefined
|
||||
);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const handleImageUpload = async (file: File) => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const uploadResult = await handleFileUpload(file, environmentId, allowedFileExtensions);
|
||||
if (uploadResult.error) {
|
||||
toast.error(uploadResult.error);
|
||||
return;
|
||||
}
|
||||
setFaviconUrl(uploadResult.url);
|
||||
} catch (error) {
|
||||
toast.error(error instanceof Error ? error.message : t("common.something_went_wrong"));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileChange = async (event: ChangeEvent<HTMLInputElement>) => {
|
||||
if (!isStorageConfigured) {
|
||||
showStorageNotConfiguredToast();
|
||||
return;
|
||||
}
|
||||
const file = event.target.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
// Validate file size
|
||||
const fileSizeInMB = file.size / 1000000;
|
||||
if (fileSizeInMB > MAX_FAVICON_SIZE_MB) {
|
||||
toast.error(t("environments.settings.domain.favicon_too_large"));
|
||||
return;
|
||||
}
|
||||
|
||||
await handleImageUpload(file);
|
||||
setIsEditing(true);
|
||||
};
|
||||
|
||||
const saveChanges = async () => {
|
||||
if (!isEditing) {
|
||||
setIsEditing(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!faviconUrl) return;
|
||||
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const updateFaviconResponse = await updateOrganizationFaviconUrlAction({
|
||||
organizationId: organization.id,
|
||||
faviconUrl,
|
||||
});
|
||||
|
||||
if (updateFaviconResponse?.data) {
|
||||
toast.success(t("environments.settings.domain.favicon_saved_successfully"));
|
||||
router.refresh();
|
||||
} else {
|
||||
const errorMessage = getFormattedErrorMessage(updateFaviconResponse);
|
||||
toast.error(errorMessage);
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error(error instanceof Error ? error.message : t("common.something_went_wrong"));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
setIsEditing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const removeFavicon = async () => {
|
||||
setFaviconUrl(undefined);
|
||||
|
||||
if (!organization.whitelabel?.faviconUrl) {
|
||||
setIsEditing(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const removeFaviconResponse = await removeOrganizationFaviconUrlAction({
|
||||
organizationId: organization.id,
|
||||
});
|
||||
|
||||
if (removeFaviconResponse?.data) {
|
||||
toast.success(t("environments.settings.domain.favicon_removed_successfully"));
|
||||
router.refresh();
|
||||
} else {
|
||||
const errorMessage = getFormattedErrorMessage(removeFaviconResponse);
|
||||
toast.error(errorMessage);
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error(error instanceof Error ? error.message : t("common.something_went_wrong"));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
setIsEditing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const buttons: [ModalButton, ModalButton] = [
|
||||
{
|
||||
text: t("common.start_free_trial"),
|
||||
href: `/environments/${environmentId}/settings/billing`,
|
||||
},
|
||||
{
|
||||
text: t("common.learn_more"),
|
||||
href: `/environments/${environmentId}/settings/billing`,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<SettingsCard
|
||||
title={t("environments.settings.domain.favicon_customization")}
|
||||
description={t("environments.settings.domain.favicon_customization_description")}>
|
||||
{hasWhiteLabelPermission ? (
|
||||
<div className="w-full space-y-4">
|
||||
{faviconUrl ? (
|
||||
<Image
|
||||
src={faviconUrl}
|
||||
alt="Favicon"
|
||||
width={64}
|
||||
height={64}
|
||||
className="-mb-2 h-16 w-16 rounded-lg border object-contain p-1"
|
||||
/>
|
||||
) : (
|
||||
<FileInput
|
||||
id="favicon-input"
|
||||
allowedFileExtensions={allowedFileExtensions}
|
||||
environmentId={environmentId}
|
||||
onFileUpload={(files: string[]) => {
|
||||
setFaviconUrl(files[0]);
|
||||
setIsEditing(true);
|
||||
}}
|
||||
disabled={isReadOnly}
|
||||
maxSizeInMB={MAX_FAVICON_SIZE_MB}
|
||||
isStorageConfigured={isStorageConfigured}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept="image/jpeg, image/png, image/webp, image/x-icon, image/ico"
|
||||
className="hidden"
|
||||
disabled={isReadOnly}
|
||||
onChange={handleFileChange}
|
||||
/>
|
||||
|
||||
{isEditing && faviconUrl && (
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (!isStorageConfigured) {
|
||||
showStorageNotConfiguredToast();
|
||||
return;
|
||||
}
|
||||
fileInputRef.current?.click();
|
||||
}}
|
||||
variant="secondary"
|
||||
size="sm">
|
||||
{t("common.replace")}
|
||||
</Button>
|
||||
<Button variant="destructive" size="sm" onClick={removeFavicon} disabled={!isEditing}>
|
||||
{t("common.remove")}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{faviconUrl && (
|
||||
<Button onClick={saveChanges} disabled={isLoading || isReadOnly} size="sm">
|
||||
{isEditing ? t("common.save") : t("common.edit")}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Alert variant="info">
|
||||
<AlertDescription>{t("environments.settings.domain.favicon_size_hint")}</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
{isReadOnly && (
|
||||
<Alert variant="warning">
|
||||
<AlertDescription>
|
||||
{t("common.only_owners_managers_and_manage_access_members_can_perform_this_action")}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<UpgradePrompt
|
||||
title={t("environments.settings.domain.customize_favicon_with_higher_plan")}
|
||||
description={t("environments.settings.domain.customize_favicon_description")}
|
||||
buttons={buttons}
|
||||
/>
|
||||
)}
|
||||
</SettingsCard>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,141 @@
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { ResourceNotFoundError } from "@formbricks/types/errors";
|
||||
import { updateOrganizationFaviconUrl } from "./organization";
|
||||
|
||||
vi.mock("@formbricks/database", () => ({
|
||||
prisma: {
|
||||
organization: {
|
||||
findUnique: vi.fn(),
|
||||
update: vi.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
describe("favicon organization", () => {
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("updateOrganizationFaviconUrl", () => {
|
||||
test("should update organization favicon URL", async () => {
|
||||
const mockOrganization = {
|
||||
id: "clg123456789012345678901234",
|
||||
whitelabel: {
|
||||
logoUrl: "https://example.com/logo.png",
|
||||
faviconUrl: "https://example.com/old-favicon.png",
|
||||
},
|
||||
};
|
||||
|
||||
vi.mocked(prisma.organization.findUnique).mockResolvedValue(mockOrganization as never);
|
||||
vi.mocked(prisma.organization.update).mockResolvedValue({} as never);
|
||||
|
||||
const result = await updateOrganizationFaviconUrl(
|
||||
"clg123456789012345678901234",
|
||||
"https://example.com/new-favicon.png"
|
||||
);
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(prisma.organization.findUnique).toHaveBeenCalledWith({
|
||||
where: { id: "clg123456789012345678901234" },
|
||||
select: { whitelabel: true },
|
||||
});
|
||||
expect(prisma.organization.update).toHaveBeenCalledWith({
|
||||
where: { id: "clg123456789012345678901234" },
|
||||
data: {
|
||||
whitelabel: {
|
||||
...mockOrganization.whitelabel,
|
||||
faviconUrl: "https://example.com/new-favicon.png",
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("should remove organization favicon URL when passing null", async () => {
|
||||
const mockOrganization = {
|
||||
id: "clg123456789012345678901234",
|
||||
whitelabel: {
|
||||
logoUrl: "https://example.com/logo.png",
|
||||
faviconUrl: "https://example.com/old-favicon.png",
|
||||
},
|
||||
};
|
||||
|
||||
vi.mocked(prisma.organization.findUnique).mockResolvedValue(mockOrganization as never);
|
||||
vi.mocked(prisma.organization.update).mockResolvedValue({} as never);
|
||||
|
||||
const result = await updateOrganizationFaviconUrl("clg123456789012345678901234", null);
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(prisma.organization.update).toHaveBeenCalledWith({
|
||||
where: { id: "clg123456789012345678901234" },
|
||||
data: {
|
||||
whitelabel: {
|
||||
...mockOrganization.whitelabel,
|
||||
faviconUrl: null,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("should throw ResourceNotFoundError when organization is not found", async () => {
|
||||
vi.mocked(prisma.organization.findUnique).mockResolvedValue(null);
|
||||
|
||||
await expect(
|
||||
updateOrganizationFaviconUrl("clg123456789012345678901234", "https://example.com/new-favicon.png")
|
||||
).rejects.toThrow(ResourceNotFoundError);
|
||||
|
||||
expect(prisma.organization.findUnique).toHaveBeenCalledWith({
|
||||
where: { id: "clg123456789012345678901234" },
|
||||
select: { whitelabel: true },
|
||||
});
|
||||
expect(prisma.organization.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("should handle organization with no existing whitelabel", async () => {
|
||||
const mockOrganization = {
|
||||
id: "clg123456789012345678901234",
|
||||
whitelabel: null,
|
||||
};
|
||||
|
||||
vi.mocked(prisma.organization.findUnique).mockResolvedValue(mockOrganization as never);
|
||||
vi.mocked(prisma.organization.update).mockResolvedValue({} as never);
|
||||
|
||||
const result = await updateOrganizationFaviconUrl(
|
||||
"clg123456789012345678901234",
|
||||
"https://example.com/new-favicon.png"
|
||||
);
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(prisma.organization.update).toHaveBeenCalledWith({
|
||||
where: { id: "clg123456789012345678901234" },
|
||||
data: {
|
||||
whitelabel: {
|
||||
faviconUrl: "https://example.com/new-favicon.png",
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("should throw ResourceNotFoundError when prisma update fails with record not found", async () => {
|
||||
const mockOrganization = {
|
||||
id: "clg123456789012345678901234",
|
||||
whitelabel: {
|
||||
faviconUrl: "https://example.com/old-favicon.png",
|
||||
},
|
||||
};
|
||||
|
||||
const mockError = new Prisma.PrismaClientKnownRequestError("Record does not exist", {
|
||||
code: "P2015", // PrismaErrorType.RecordDoesNotExist
|
||||
clientVersion: "2.0.0",
|
||||
});
|
||||
|
||||
vi.mocked(prisma.organization.findUnique).mockResolvedValue(mockOrganization as never);
|
||||
vi.mocked(prisma.organization.update).mockRejectedValue(mockError);
|
||||
|
||||
await expect(
|
||||
updateOrganizationFaviconUrl("clg123456789012345678901234", "https://example.com/new-favicon.png")
|
||||
).rejects.toThrow(ResourceNotFoundError);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,49 @@
|
||||
import "server-only";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { PrismaErrorType } from "@formbricks/database/types/error";
|
||||
import { ZId, ZUrl } from "@formbricks/types/common";
|
||||
import { ResourceNotFoundError } from "@formbricks/types/errors";
|
||||
import { TOrganizationWhitelabel } from "@formbricks/types/organizations";
|
||||
import { validateInputs } from "@/lib/utils/validate";
|
||||
|
||||
export const updateOrganizationFaviconUrl = async (
|
||||
organizationId: string,
|
||||
faviconUrl: string | null
|
||||
): Promise<boolean> => {
|
||||
validateInputs([organizationId, ZId], [faviconUrl, ZUrl.nullable()]);
|
||||
|
||||
try {
|
||||
const organization = await prisma.organization.findUnique({
|
||||
where: { id: organizationId },
|
||||
select: { whitelabel: true },
|
||||
});
|
||||
|
||||
if (!organization) {
|
||||
throw new ResourceNotFoundError("Organization", organizationId);
|
||||
}
|
||||
|
||||
const existingWhitelabel = (organization.whitelabel ?? {}) as TOrganizationWhitelabel;
|
||||
|
||||
await prisma.organization.update({
|
||||
where: { id: organizationId },
|
||||
data: {
|
||||
whitelabel: {
|
||||
...existingWhitelabel,
|
||||
faviconUrl,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (
|
||||
error instanceof Prisma.PrismaClientKnownRequestError &&
|
||||
error.code === PrismaErrorType.RecordDoesNotExist
|
||||
) {
|
||||
throw new ResourceNotFoundError("Organization", organizationId);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
@@ -11,7 +11,7 @@ import { renderSurvey } from "@/modules/survey/link/components/survey-renderer";
|
||||
import { getExistingContactResponse } from "@/modules/survey/link/lib/data";
|
||||
import { getEnvironmentContextForLinkSurvey } from "@/modules/survey/link/lib/environment";
|
||||
import { checkAndValidateSingleUseId } from "@/modules/survey/link/lib/helper";
|
||||
import { getBasicSurveyMetadata } from "@/modules/survey/link/lib/metadata-utils";
|
||||
import { getBasicSurveyMetadata, getSurveyOpenGraphMetadata } from "@/modules/survey/link/lib/metadata-utils";
|
||||
import { getProjectByEnvironmentId } from "@/modules/survey/link/lib/project";
|
||||
|
||||
interface ContactSurveyPageProps {
|
||||
@@ -39,8 +39,38 @@ export const generateMetadata = async (props: ContactSurveyPageProps): Promise<M
|
||||
};
|
||||
}
|
||||
const { surveyId } = result.data;
|
||||
return getBasicSurveyMetadata(surveyId);
|
||||
} catch (error) {
|
||||
const { title, description, survey, ogImage } = await getBasicSurveyMetadata(surveyId);
|
||||
|
||||
if (!survey) {
|
||||
return { title, description };
|
||||
}
|
||||
|
||||
// Fetch organization whitelabel data for custom favicon
|
||||
const environmentContext = await getEnvironmentContextForLinkSurvey(survey.environmentId);
|
||||
const customFaviconUrl = environmentContext.organizationWhitelabel?.faviconUrl;
|
||||
|
||||
// Get OpenGraph metadata
|
||||
const surveyBrandColor = survey.styling?.brandColor?.light;
|
||||
const baseMetadata = getSurveyOpenGraphMetadata(survey.id, title, surveyBrandColor);
|
||||
|
||||
// Override with the custom image URL
|
||||
if (baseMetadata.openGraph) {
|
||||
baseMetadata.openGraph.images = ogImage ?? baseMetadata.openGraph.images;
|
||||
baseMetadata.openGraph.description = description;
|
||||
}
|
||||
|
||||
if (baseMetadata.twitter) {
|
||||
baseMetadata.twitter.images = ogImage ?? baseMetadata.twitter.images;
|
||||
baseMetadata.twitter.description = description;
|
||||
}
|
||||
|
||||
return {
|
||||
title,
|
||||
description,
|
||||
...baseMetadata,
|
||||
...(customFaviconUrl && { icons: customFaviconUrl }),
|
||||
};
|
||||
} catch {
|
||||
// If the token is invalid, we'll return generic metadata
|
||||
return {
|
||||
title: "Survey",
|
||||
|
||||
@@ -53,6 +53,7 @@ describe("getEnvironmentContextForLinkSurvey", () => {
|
||||
},
|
||||
},
|
||||
},
|
||||
whitelabel: null,
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -71,6 +72,7 @@ describe("getEnvironmentContextForLinkSurvey", () => {
|
||||
},
|
||||
organizationId: "clh1a2b3c4d5e6f7g8h9k",
|
||||
organizationBilling: mockData.project.organization.billing,
|
||||
organizationWhitelabel: null,
|
||||
});
|
||||
|
||||
expect(prisma.environment.findUnique).toHaveBeenCalledWith({
|
||||
@@ -88,6 +90,7 @@ describe("getEnvironmentContextForLinkSurvey", () => {
|
||||
select: {
|
||||
id: true,
|
||||
billing: true,
|
||||
whitelabel: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -198,6 +201,7 @@ describe("getEnvironmentContextForLinkSurvey", () => {
|
||||
},
|
||||
},
|
||||
},
|
||||
whitelabel: null,
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -216,6 +220,7 @@ describe("getEnvironmentContextForLinkSurvey", () => {
|
||||
},
|
||||
organizationId: "clh1a2b3c4d5e6f7g8h9u",
|
||||
organizationBilling: mockData.project.organization.billing,
|
||||
organizationWhitelabel: null,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@ import { cache as reactCache } from "react";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { ZId } from "@formbricks/types/common";
|
||||
import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/errors";
|
||||
import { TOrganizationBilling } from "@formbricks/types/organizations";
|
||||
import { TOrganizationBilling, TOrganizationWhitelabel } from "@formbricks/types/organizations";
|
||||
import { validateInputs } from "@/lib/utils/validate";
|
||||
|
||||
/**
|
||||
@@ -22,6 +22,7 @@ export interface TEnvironmentContextForLinkSurvey {
|
||||
project: TProjectForLinkSurvey;
|
||||
organizationId: string;
|
||||
organizationBilling: TOrganizationBilling;
|
||||
organizationWhitelabel: TOrganizationWhitelabel | null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -65,6 +66,7 @@ export const getEnvironmentContextForLinkSurvey = reactCache(
|
||||
select: {
|
||||
id: true,
|
||||
billing: true,
|
||||
whitelabel: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -91,7 +93,8 @@ export const getEnvironmentContextForLinkSurvey = reactCache(
|
||||
linkSurveyBranding: environment.project.linkSurveyBranding,
|
||||
},
|
||||
organizationId: environment.project.organizationId,
|
||||
organizationBilling: environment.project.organization.billing as TOrganizationBilling,
|
||||
organizationBilling: environment.project.organization.billing,
|
||||
organizationWhitelabel: environment.project.organization.whitelabel ?? null,
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import { beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { getSurveyWithMetadata } from "@/modules/survey/link/lib/data";
|
||||
import { getEnvironmentContextForLinkSurvey } from "@/modules/survey/link/lib/environment";
|
||||
import { getBasicSurveyMetadata, getSurveyOpenGraphMetadata } from "./lib/metadata-utils";
|
||||
import { getMetadataForLinkSurvey } from "./metadata";
|
||||
|
||||
@@ -8,6 +9,10 @@ vi.mock("@/modules/survey/link/lib/data", () => ({
|
||||
getSurveyWithMetadata: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/survey/link/lib/environment", () => ({
|
||||
getEnvironmentContextForLinkSurvey: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("next/navigation", () => ({
|
||||
notFound: vi.fn(),
|
||||
}));
|
||||
@@ -44,6 +49,24 @@ describe("getMetadataForLinkSurvey", () => {
|
||||
description: "Thanks a lot for your time 🙏",
|
||||
},
|
||||
});
|
||||
vi.mocked(getEnvironmentContextForLinkSurvey).mockResolvedValue({
|
||||
project: {
|
||||
id: "project-123",
|
||||
name: "Test Project",
|
||||
styling: null,
|
||||
logo: null,
|
||||
linkSurveyBranding: true,
|
||||
},
|
||||
organizationId: "org-123",
|
||||
organizationBilling: {
|
||||
plan: "free",
|
||||
period: "monthly",
|
||||
periodStart: new Date(),
|
||||
stripeCustomerId: null,
|
||||
limits: { projects: 3, monthly: { responses: 1500, miu: 2000 } },
|
||||
},
|
||||
organizationWhitelabel: null,
|
||||
});
|
||||
});
|
||||
|
||||
test("returns correct metadata for a valid link survey", async () => {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Metadata } from "next";
|
||||
import { notFound } from "next/navigation";
|
||||
import { getSurveyWithMetadata } from "@/modules/survey/link/lib/data";
|
||||
import { getEnvironmentContextForLinkSurvey } from "@/modules/survey/link/lib/environment";
|
||||
import { getBasicSurveyMetadata, getSurveyOpenGraphMetadata } from "./lib/metadata-utils";
|
||||
|
||||
export const getMetadataForLinkSurvey = async (
|
||||
@@ -16,6 +17,10 @@ export const getMetadataForLinkSurvey = async (
|
||||
const { title, description, ogImage } = await getBasicSurveyMetadata(surveyId, languageCode, survey);
|
||||
const surveyBrandColor = survey.styling?.brandColor?.light;
|
||||
|
||||
// Fetch organization whitelabel data for custom favicon
|
||||
const environmentContext = await getEnvironmentContextForLinkSurvey(survey.environmentId);
|
||||
const customFaviconUrl = environmentContext.organizationWhitelabel?.faviconUrl;
|
||||
|
||||
// Use the shared function for creating the base metadata but override with custom data
|
||||
const baseMetadata = getSurveyOpenGraphMetadata(survey.id, title, surveyBrandColor);
|
||||
|
||||
@@ -36,6 +41,7 @@ export const getMetadataForLinkSurvey = async (
|
||||
title,
|
||||
description,
|
||||
...baseMetadata,
|
||||
...(customFaviconUrl && { icons: customFaviconUrl }),
|
||||
alternates: {
|
||||
canonical: canonicalPath,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user