mirror of
https://github.com/formbricks/formbricks.git
synced 2025-12-29 18:00:26 -06:00
feat:
This commit is contained in:
@@ -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,23 @@ 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}
|
||||
isFormbricksCloud={IS_FORMBRICKS_CLOUD}
|
||||
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,16 @@ 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_for_link_surveys: b3bb8254f5ab2778db9091ddc3fffa8f
|
||||
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,16 @@
|
||||
"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_for_link_surveys": "Favicon für Link-Umfragen",
|
||||
"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,16 @@
|
||||
"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_for_link_surveys": "Favicon for Link Surveys",
|
||||
"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,16 @@
|
||||
"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_for_link_surveys": "Favicon para encuestas por enlace",
|
||||
"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,16 @@
|
||||
"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_for_link_surveys": "Favicon pour les enquêtes par lien",
|
||||
"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,16 @@
|
||||
"website_surveys": "ウェブサイトフォーム"
|
||||
},
|
||||
"domain": {
|
||||
"customize_favicon_description": "カスタムファビコンをアップロードして、リンク調査の体験をパーソナライズし、ブランドプレゼンスを強化します。",
|
||||
"customize_favicon_with_higher_plan": "上位プランでファビコンをカスタマイズ",
|
||||
"description": "組織全体でカスタムURLを使用しているすべてのフォームの概要",
|
||||
"favicon_customization": "ファビコン",
|
||||
"favicon_customization_description": "リンク調査のブラウザタブに表示されるファビコンをカスタマイズします。管理アプリには影響しません。",
|
||||
"favicon_for_link_surveys": "リンク調査用ファビコン",
|
||||
"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,16 @@
|
||||
"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_for_link_surveys": "Favicon voor linkenquêtes",
|
||||
"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,16 @@
|
||||
"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_for_link_surveys": "Favicon para pesquisas por link",
|
||||
"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,16 @@
|
||||
"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_for_link_surveys": "Favicon para inquéritos por link",
|
||||
"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,16 @@
|
||||
"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_for_link_surveys": "Favicon pentru sondaje de linkuri",
|
||||
"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,16 @@
|
||||
"website_surveys": "Опросы на сайте"
|
||||
},
|
||||
"domain": {
|
||||
"customize_favicon_description": "Загрузите свой favicon, чтобы персонализировать опросы по ссылке и усилить узнаваемость бренда.",
|
||||
"customize_favicon_with_higher_plan": "Настройка favicon доступна на более продвинутом тарифе",
|
||||
"description": "Обзор всех опросов с Pretty URL в вашей организации",
|
||||
"favicon_customization": "Favicon",
|
||||
"favicon_customization_description": "Настройте favicon, который отображается на вкладках браузера для ваших опросов по ссылке. Это не повлияет на административное приложение.",
|
||||
"favicon_for_link_surveys": "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,16 @@
|
||||
"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_for_link_surveys": "Favicon för länkenkäter",
|
||||
"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,16 @@
|
||||
"website_surveys": "网站 调查"
|
||||
},
|
||||
"domain": {
|
||||
"customize_favicon_description": "上传自定义 Favicon,个性化您的链接问卷体验,提升品牌形象。",
|
||||
"customize_favicon_with_higher_plan": "升级套餐以自定义 Favicon",
|
||||
"description": "概览组织内所有使用美化 URL 的调查问卷",
|
||||
"favicon_customization": "Favicon",
|
||||
"favicon_customization_description": "自定义在浏览器标签页中显示的链接问卷 Favicon。此操作不会影响管理端应用。",
|
||||
"favicon_for_link_surveys": "链接问卷 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,16 @@
|
||||
"website_surveys": "網站問卷"
|
||||
},
|
||||
"domain": {
|
||||
"customize_favicon_description": "上傳自訂 Favicon,讓您的連結問卷體驗更具個人化,並強化品牌形象。",
|
||||
"customize_favicon_with_higher_plan": "升級方案以自訂 Favicon",
|
||||
"description": "檢視貴組織內所有使用 Pretty URL 的問卷",
|
||||
"favicon_customization": "Favicon",
|
||||
"favicon_customization_description": "自訂顯示於瀏覽器分頁上的連結問卷 Favicon。這不會影響管理後台應用程式。",
|
||||
"favicon_for_link_surveys": "連結問卷 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,89 @@
|
||||
"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 {
|
||||
removeOrganizationFaviconUrl,
|
||||
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 removeOrganizationFaviconUrl(organizationId);
|
||||
}
|
||||
)
|
||||
);
|
||||
@@ -0,0 +1,281 @@
|
||||
"use client";
|
||||
|
||||
import { RepeatIcon, Trash2Icon } from "lucide-react";
|
||||
import Image from "next/image";
|
||||
import { useRouter } from "next/navigation";
|
||||
import React, { 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 { cn } from "@/lib/cn";
|
||||
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 { Uploader } from "@/modules/ui/components/file-input/components/uploader";
|
||||
import { showStorageNotConfiguredToast } from "@/modules/ui/components/storage-not-configured-toast/lib/utils";
|
||||
import { Muted, Small } from "@/modules/ui/components/typography";
|
||||
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;
|
||||
isFormbricksCloud: boolean;
|
||||
isStorageConfigured: boolean;
|
||||
}
|
||||
|
||||
export const FaviconCustomizationSettings = ({
|
||||
organization,
|
||||
hasWhiteLabelPermission,
|
||||
environmentId,
|
||||
isReadOnly,
|
||||
isFormbricksCloud,
|
||||
isStorageConfigured,
|
||||
}: FaviconCustomizationSettingsProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [faviconFile, setFaviconFile] = useState<File | null>(null);
|
||||
const [faviconUrl, setFaviconUrl] = useState<string>(organization.whitelabel?.faviconUrl || "");
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const onFileInputChange = (files: File[]) => {
|
||||
if (!isStorageConfigured) {
|
||||
showStorageNotConfiguredToast();
|
||||
return;
|
||||
}
|
||||
|
||||
const file = 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;
|
||||
}
|
||||
|
||||
// Revoke any previous object URL so we don't leak memory
|
||||
if (faviconUrl?.startsWith("blob:")) {
|
||||
URL.revokeObjectURL(faviconUrl);
|
||||
}
|
||||
|
||||
setFaviconFile(file);
|
||||
setFaviconUrl(URL.createObjectURL(file));
|
||||
};
|
||||
|
||||
const handleDragOver = (e: React.DragEvent<HTMLLabelElement>) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
e.dataTransfer.dropEffect = "copy";
|
||||
};
|
||||
|
||||
const handleDrop = (e: React.DragEvent<HTMLLabelElement>) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
if (!isStorageConfigured) {
|
||||
showStorageNotConfiguredToast();
|
||||
return;
|
||||
}
|
||||
|
||||
const files = Array.from(e.dataTransfer.files);
|
||||
const file = files[0];
|
||||
if (!file) return;
|
||||
|
||||
const extension = file.name.split(".").pop()?.toLowerCase() as TAllowedFileExtension;
|
||||
if (!allowedFileExtensions.includes(extension)) {
|
||||
toast.error(t("common.invalid_file_type"));
|
||||
return;
|
||||
}
|
||||
onFileInputChange(files);
|
||||
};
|
||||
|
||||
const removeFavicon = async () => {
|
||||
if (faviconUrl?.startsWith("blob:")) {
|
||||
URL.revokeObjectURL(faviconUrl);
|
||||
}
|
||||
setFaviconFile(null);
|
||||
setFaviconUrl("");
|
||||
|
||||
if (inputRef.current) {
|
||||
inputRef.current.value = "";
|
||||
}
|
||||
|
||||
if (!organization.whitelabel?.faviconUrl) return;
|
||||
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!faviconFile) return;
|
||||
setIsSaving(true);
|
||||
const { url, error } = await handleFileUpload(faviconFile, environmentId, allowedFileExtensions);
|
||||
|
||||
if (error) {
|
||||
toast.error(error);
|
||||
setIsSaving(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const updateFaviconResponse = await updateOrganizationFaviconUrlAction({
|
||||
organizationId: organization.id,
|
||||
faviconUrl: url,
|
||||
});
|
||||
|
||||
if (updateFaviconResponse?.data) {
|
||||
toast.success(t("environments.settings.domain.favicon_saved_successfully"));
|
||||
setFaviconUrl(url);
|
||||
setFaviconFile(null);
|
||||
router.refresh();
|
||||
} else {
|
||||
const errorMessage = getFormattedErrorMessage(updateFaviconResponse);
|
||||
toast.error(errorMessage);
|
||||
}
|
||||
|
||||
setIsSaving(false);
|
||||
};
|
||||
|
||||
const buttons: [ModalButton, ModalButton] = [
|
||||
{
|
||||
text: isFormbricksCloud ? t("common.start_free_trial") : t("common.request_trial_license"),
|
||||
href: isFormbricksCloud
|
||||
? `/environments/${environmentId}/settings/billing`
|
||||
: "https://formbricks.com/upgrade-self-hosting-license",
|
||||
},
|
||||
{
|
||||
text: t("common.learn_more"),
|
||||
href: isFormbricksCloud
|
||||
? `/environments/${environmentId}/settings/billing`
|
||||
: "https://formbricks.com/learn-more-self-hosting-license",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<SettingsCard
|
||||
title={t("environments.settings.domain.favicon_customization")}
|
||||
description={t("environments.settings.domain.favicon_customization_description")}>
|
||||
{hasWhiteLabelPermission ? (
|
||||
<div className="flex flex-col gap-4">
|
||||
<Small>{t("environments.settings.domain.favicon_for_link_surveys")}</Small>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
{faviconUrl && (
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex h-16 w-16 items-center justify-center rounded-lg border border-slate-200 bg-white p-2">
|
||||
<Image
|
||||
src={faviconUrl}
|
||||
alt="Favicon"
|
||||
className="max-h-full max-w-full object-contain"
|
||||
width={48}
|
||||
height={48}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
disabled={isReadOnly || isSaving}
|
||||
onClick={() => {
|
||||
if (!isStorageConfigured) {
|
||||
showStorageNotConfiguredToast();
|
||||
return;
|
||||
}
|
||||
inputRef.current?.click();
|
||||
}}>
|
||||
<RepeatIcon className="h-4 w-4" />
|
||||
{t("common.replace")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
disabled={isReadOnly || isSaving}
|
||||
onClick={removeFavicon}>
|
||||
<Trash2Icon className="h-4 w-4" />
|
||||
{t("common.remove")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Uploader
|
||||
id="favicon"
|
||||
name="favicon-file"
|
||||
handleDragOver={handleDragOver}
|
||||
uploaderClassName={cn("h-24 w-52", faviconUrl ? "hidden" : "block")}
|
||||
handleDrop={handleDrop}
|
||||
allowedFileExtensions={allowedFileExtensions}
|
||||
multiple={false}
|
||||
handleUpload={onFileInputChange}
|
||||
disabled={isReadOnly}
|
||||
isStorageConfigured={isStorageConfigured}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Muted>{t("environments.settings.domain.favicon_size_hint")}</Muted>
|
||||
|
||||
{faviconFile && (
|
||||
<div className="flex gap-2">
|
||||
<Button onClick={handleSave} loading={isSaving} disabled={isReadOnly}>
|
||||
{t("common.save")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
if (faviconUrl.startsWith("blob:")) {
|
||||
URL.revokeObjectURL(faviconUrl);
|
||||
}
|
||||
setFaviconFile(null);
|
||||
setFaviconUrl(organization.whitelabel?.faviconUrl || "");
|
||||
}}>
|
||||
{t("common.cancel")}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{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,177 @@
|
||||
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 { removeOrganizationFaviconUrl, 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: "logo.png",
|
||||
faviconUrl: "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", "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: "new-favicon.png",
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("should throw ResourceNotFoundError when organization is not found", async () => {
|
||||
vi.mocked(prisma.organization.findUnique).mockResolvedValue(null);
|
||||
|
||||
await expect(
|
||||
updateOrganizationFaviconUrl("clg123456789012345678901234", "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", "new-favicon.png");
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(prisma.organization.update).toHaveBeenCalledWith({
|
||||
where: { id: "clg123456789012345678901234" },
|
||||
data: {
|
||||
whitelabel: {
|
||||
faviconUrl: "new-favicon.png",
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("removeOrganizationFaviconUrl", () => {
|
||||
test("should remove organization favicon URL", async () => {
|
||||
const mockOrganization = {
|
||||
id: "clg123456789012345678901234",
|
||||
whitelabel: {
|
||||
logoUrl: "logo.png",
|
||||
faviconUrl: "old-favicon.png",
|
||||
},
|
||||
};
|
||||
|
||||
vi.mocked(prisma.organization.findUnique).mockResolvedValue(mockOrganization as never);
|
||||
vi.mocked(prisma.organization.update).mockResolvedValue({} as never);
|
||||
|
||||
const result = await removeOrganizationFaviconUrl("clg123456789012345678901234");
|
||||
|
||||
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: null,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("should throw ResourceNotFoundError when organization is not found", async () => {
|
||||
vi.mocked(prisma.organization.findUnique).mockResolvedValue(null);
|
||||
|
||||
await expect(removeOrganizationFaviconUrl("clg123456789012345678901234")).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 removeOrganizationFaviconUrl("clg123456789012345678901234");
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(prisma.organization.update).toHaveBeenCalledWith({
|
||||
where: { id: "clg123456789012345678901234" },
|
||||
data: {
|
||||
whitelabel: {
|
||||
faviconUrl: null,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("should throw ResourceNotFoundError when prisma update fails with record not found", async () => {
|
||||
const mockOrganization = {
|
||||
id: "clg123456789012345678901234",
|
||||
whitelabel: {
|
||||
faviconUrl: "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(removeOrganizationFaviconUrl("clg123456789012345678901234")).rejects.toThrow(
|
||||
ResourceNotFoundError
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,87 @@
|
||||
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
|
||||
): Promise<boolean> => {
|
||||
validateInputs([organizationId, ZId], [faviconUrl, ZUrl]);
|
||||
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
export const removeOrganizationFaviconUrl = async (organizationId: string): Promise<boolean> => {
|
||||
validateInputs([organizationId, ZId]);
|
||||
|
||||
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: null,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
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",
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -92,6 +94,8 @@ export const getEnvironmentContextForLinkSurvey = reactCache(
|
||||
},
|
||||
organizationId: environment.project.organizationId,
|
||||
organizationBilling: environment.project.organization.billing as TOrganizationBilling,
|
||||
organizationWhitelabel:
|
||||
(environment.project.organization.whitelabel as TOrganizationWhitelabel) ?? null,
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -35,6 +35,7 @@ export type TOrganizationBilling = z.infer<typeof ZOrganizationBilling>;
|
||||
|
||||
export const ZOrganizationWhitelabel = z.object({
|
||||
logoUrl: z.string().nullable(),
|
||||
faviconUrl: z.string().url().nullish(),
|
||||
});
|
||||
|
||||
export type TOrganizationWhitelabel = z.infer<typeof ZOrganizationWhitelabel>;
|
||||
|
||||
@@ -6,6 +6,7 @@ export const ZAllowedFileExtension = z.enum([
|
||||
"jpeg",
|
||||
"jpg",
|
||||
"webp",
|
||||
"ico",
|
||||
"pdf",
|
||||
"eml",
|
||||
"doc",
|
||||
@@ -33,6 +34,7 @@ export const mimeTypes: Record<TAllowedFileExtension, string> = {
|
||||
jpeg: "image/jpeg",
|
||||
jpg: "image/jpeg",
|
||||
webp: "image/webp",
|
||||
ico: "image/x-icon",
|
||||
pdf: "application/pdf",
|
||||
eml: "message/rfc822",
|
||||
doc: "application/msword",
|
||||
|
||||
Reference in New Issue
Block a user