Replace portal title with entity name

Store a short entity name instead of the full home
heading so orgs can brand portals for sub-entities.
Restore hero i18n composition and keep the English
document title composed from the entity name.

Signed-off-by: Émile Ré <emile@probo.com>
This commit is contained in:
Émile Ré
2026-07-21 19:06:55 +02:00
parent 0fe814cfe4
commit 0d832508c9
46 changed files with 148 additions and 97 deletions

View File

@@ -45,6 +45,7 @@
} }
}, },
"home": { "home": {
"heroTitle": "Vertrauen bei {{name}}.",
"heroDescription": "Willkommen auf unserem Compliance-Portal. Hier finden Sie unsere Sicherheitsdokumentation und Zertifizierungen.", "heroDescription": "Willkommen auf unserem Compliance-Portal. Hier finden Sie unsere Sicherheitsdokumentation und Zertifizierungen.",
"sections": { "sections": {
"compliance": "Compliance", "compliance": "Compliance",

View File

@@ -55,6 +55,7 @@
} }
}, },
"home": { "home": {
"heroTitle": "Compliance at {{name}}.",
"heroDescription": "Welcome to our Compliance Portal. Find our security documentation and certifications here.", "heroDescription": "Welcome to our Compliance Portal. Find our security documentation and certifications here.",
"sections": { "sections": {
"compliance": "Compliance", "compliance": "Compliance",
@@ -104,4 +105,4 @@
"previous": "Previous page", "previous": "Previous page",
"next": "Next page" "next": "Next page"
} }
} }

View File

@@ -45,6 +45,7 @@
} }
}, },
"home": { "home": {
"heroTitle": "Confianza en {{name}}.",
"heroDescription": "Bienvenido a nuestro Portal de cumplimiento. Aquí encontrará nuestra documentación de seguridad y nuestras certificaciones.", "heroDescription": "Bienvenido a nuestro Portal de cumplimiento. Aquí encontrará nuestra documentación de seguridad y nuestras certificaciones.",
"sections": { "sections": {
"compliance": "Cumplimiento", "compliance": "Cumplimiento",

View File

@@ -45,6 +45,7 @@
} }
}, },
"home": { "home": {
"heroTitle": "La confiance chez {{name}}.",
"heroDescription": "Bienvenue dans notre Portail de conformité. Retrouvez ici notre documentation et nos certifications de sécurité.", "heroDescription": "Bienvenue dans notre Portail de conformité. Retrouvez ici notre documentation et nos certifications de sécurité.",
"sections": { "sections": {
"compliance": "Conformité", "compliance": "Conformité",

View File

@@ -45,6 +45,7 @@
} }
}, },
"home": { "home": {
"heroTitle": "Kepercayaan di {{name}}.",
"heroDescription": "Selamat datang di Portal Kepatuhan kami. Temukan dokumentasi keamanan dan sertifikasi kami di sini.", "heroDescription": "Selamat datang di Portal Kepatuhan kami. Temukan dokumentasi keamanan dan sertifikasi kami di sini.",
"sections": { "sections": {
"compliance": "Kepatuhan", "compliance": "Kepatuhan",

View File

@@ -45,6 +45,7 @@
} }
}, },
"home": { "home": {
"heroTitle": "La fiducia in {{name}}.",
"heroDescription": "Benvenuti nel nostro Portale di conformità. Qui potete trovare la nostra documentazione di sicurezza e le nostre certificazioni.", "heroDescription": "Benvenuti nel nostro Portale di conformità. Qui potete trovare la nostra documentazione di sicurezza e le nostre certificazioni.",
"sections": { "sections": {
"compliance": "Conformità", "compliance": "Conformità",

View File

@@ -45,6 +45,7 @@
} }
}, },
"home": { "home": {
"heroTitle": "{{name}}における信頼。",
"heroDescription": "コンプライアンスポータルへようこそ。セキュリティに関する文書や認証はこちらからご確認いただけます。", "heroDescription": "コンプライアンスポータルへようこそ。セキュリティに関する文書や認証はこちらからご確認いただけます。",
"sections": { "sections": {
"compliance": "コンプライアンス", "compliance": "コンプライアンス",

View File

@@ -45,6 +45,7 @@
} }
}, },
"home": { "home": {
"heroTitle": "{{name}}의 신뢰.",
"heroDescription": "컴플라이언스 포털에 오신 것을 환영합니다. 이곳에서 보안 문서와 인증 정보를 확인하실 수 있습니다.", "heroDescription": "컴플라이언스 포털에 오신 것을 환영합니다. 이곳에서 보안 문서와 인증 정보를 확인하실 수 있습니다.",
"sections": { "sections": {
"compliance": "컴플라이언스", "compliance": "컴플라이언스",

View File

@@ -45,6 +45,7 @@
} }
}, },
"home": { "home": {
"heroTitle": "Zaufanie w {{name}}.",
"heroDescription": "Witamy w naszym Portalu Zgodności. Znajdziesz tu naszą dokumentację bezpieczeństwa i certyfikaty.", "heroDescription": "Witamy w naszym Portalu Zgodności. Znajdziesz tu naszą dokumentację bezpieczeństwa i certyfikaty.",
"sections": { "sections": {
"compliance": "Zgodność", "compliance": "Zgodność",

View File

@@ -45,6 +45,7 @@
} }
}, },
"home": { "home": {
"heroTitle": "A confiança na {{name}}.",
"heroDescription": "Bem-vindo ao nosso Portal de Conformidade. Encontre aqui a nossa documentação de segurança e certificações.", "heroDescription": "Bem-vindo ao nosso Portal de Conformidade. Encontre aqui a nossa documentação de segurança e certificações.",
"sections": { "sections": {
"compliance": "Conformidade", "compliance": "Conformidade",

View File

@@ -45,6 +45,7 @@
} }
}, },
"home": { "home": {
"heroTitle": "{{name}}'de Güven.",
"heroDescription": "Uyumluluk Portalımıza hoş geldiniz. Güvenlik belgelerimizi ve sertifikalarımızı burada bulabilirsiniz.", "heroDescription": "Uyumluluk Portalımıza hoş geldiniz. Güvenlik belgelerimizi ve sertifikalarımızı burada bulabilirsiniz.",
"sections": { "sections": {
"compliance": "Uyumluluk", "compliance": "Uyumluluk",

View File

@@ -45,6 +45,7 @@
} }
}, },
"home": { "home": {
"heroTitle": "Довіра в {{name}}.",
"heroDescription": "Вітаємо у нашому Порталі відповідності. Тут ви знайдете нашу документацію з безпеки та сертифікати.", "heroDescription": "Вітаємо у нашому Порталі відповідності. Тут ви знайдете нашу документацію з безпеки та сертифікати.",
"sections": { "sections": {
"compliance": "Відповідність", "compliance": "Відповідність",

View File

@@ -45,6 +45,7 @@
} }
}, },
"home": { "home": {
"heroTitle": "信赖 {{name}}。",
"heroDescription": "欢迎访问我们的合规门户。您可以在此查看我们的安全文档和认证。", "heroDescription": "欢迎访问我们的合规门户。您可以在此查看我们的安全文档和认证。",
"sections": { "sections": {
"compliance": "合规", "compliance": "合规",

View File

@@ -45,7 +45,7 @@ const topBarFragment = graphql`
} }
currentCompliancePortal @required(action: THROW) { currentCompliancePortal @required(action: THROW) {
themedLogoUrl themedLogoUrl
title entityName
} }
} }
`; `;
@@ -66,7 +66,7 @@ export function TopBar({ queryKey }: TopBarProps) {
const localizedPath = useLocalizedPath(); const localizedPath = useLocalizedPath();
const { currentCompliancePortal } = data; const { currentCompliancePortal } = data;
const title = currentCompliancePortal.title; const entityName = currentCompliancePortal.entityName;
const logoUrl = currentCompliancePortal.themedLogoUrl ?? undefined; const logoUrl = currentCompliancePortal.themedLogoUrl ?? undefined;
const slots = topBar(); const slots = topBar();
@@ -82,11 +82,11 @@ export function TopBar({ queryKey }: TopBarProps) {
color="neutral" color="neutral"
radius="small" radius="small"
src={logoUrl} src={logoUrl}
fallback={title.charAt(0) || "?"} fallback={entityName.charAt(0) || "?"}
className={slots.logo()} className={slots.logo()}
/> />
<Text size={2} weight="medium" color="neutral" highContrast className={slots.brandName()}> <Text size={2} weight="medium" color="neutral" highContrast className={slots.brandName()}>
{title} {entityName}
</Text> </Text>
<Text size={2} color="neutral" className={slots.tagline()}> <Text size={2} color="neutral" className={slots.tagline()}>
{t("topBar.tagline")} {t("topBar.tagline")}

View File

@@ -45,7 +45,7 @@ export const subscribeDialogProviderFragment = graphql`
} }
currentCompliancePortal @required(action: THROW) { currentCompliancePortal @required(action: THROW) {
id id
title entityName
viewerSubscription { viewerSubscription {
id id
} }
@@ -71,7 +71,7 @@ export function SubscribeDialogProvider({
const [unsubscribeFromMailingList, isUnsubscribing] = useUnsubscribeFromMailingList(); const [unsubscribeFromMailingList, isUnsubscribing] = useUnsubscribeFromMailingList();
const viewer = data.viewer; const viewer = data.viewer;
const { id: trustCenterId, title, viewerSubscription } = data.currentCompliancePortal; const { id: trustCenterId, entityName, viewerSubscription } = data.currentCompliancePortal;
const isSubscribed = viewerSubscription != null; const isSubscribed = viewerSubscription != null;
const openSubscribe = useCallback(() => { const openSubscribe = useCallback(() => {
@@ -127,7 +127,7 @@ export function SubscribeDialogProvider({
onOpenChange={setDialogOpen} onOpenChange={setDialogOpen}
trustCenterId={trustCenterId} trustCenterId={trustCenterId}
viewerEmail={viewer.email} viewerEmail={viewer.email}
organizationName={title} organizationName={entityName}
/> />
)} )}
</SubscribeDialogContextProvider> </SubscribeDialogContextProvider>

View File

@@ -34,7 +34,7 @@ import type { HomePageQuery } from "./__generated__/HomePageQuery.graphql";
export const homePageQuery = graphql` export const homePageQuery = graphql`
query HomePageQuery @throwOnFieldError { query HomePageQuery @throwOnFieldError {
currentCompliancePortal @required(action: THROW) { currentCompliancePortal @required(action: THROW) {
title entityName
...CompliancePortalContactInfo_compliancePortal ...CompliancePortalContactInfo_compliancePortal
...ComplianceFrameworksSection_compliancePortal ...ComplianceFrameworksSection_compliancePortal
...SecurityCommitmentsSection_compliancePortal ...SecurityCommitmentsSection_compliancePortal
@@ -52,12 +52,12 @@ export function HomePage({ queryRef }: HomePageProps) {
const { t } = useTranslation(); const { t } = useTranslation();
const data = usePreloadedQuery<HomePageQuery>(homePageQuery, queryRef); const data = usePreloadedQuery<HomePageQuery>(homePageQuery, queryRef);
const { currentCompliancePortal } = data; const { currentCompliancePortal } = data;
const { title } = currentCompliancePortal; const { entityName } = currentCompliancePortal;
return ( return (
<> <>
<Hero <Hero
title={title} title={t("home.heroTitle", { name: entityName })}
description={t("home.heroDescription")} description={t("home.heroDescription")}
> >
<CompliancePortalContactInfo compliancePortalKey={currentCompliancePortal} /> <CompliancePortalContactInfo compliancePortalKey={currentCompliancePortal} />

View File

@@ -63,7 +63,7 @@ export const ndaPageQuery = graphql`
id id
} }
currentCompliancePortal @required(action: THROW) { currentCompliancePortal @required(action: THROW) {
title entityName
nonDisclosureAgreement { nonDisclosureAgreement {
fileUrl fileUrl
} }
@@ -238,7 +238,7 @@ export function NDAPage({ queryRef }: NDAPageProps) {
{t("title")} {t("title")}
</Heading> </Heading>
<Text size={2} color="neutral"> <Text size={2} color="neutral">
{t("subtitle", { name: compliancePortal.title })} {t("subtitle", { name: compliancePortal.entityName })}
</Text> </Text>
{signature.consentText != null && ( {signature.consentText != null && (
<Text size={1} color="faint" className={slots.consent()}> <Text size={1} color="faint" className={slots.consent()}>

View File

@@ -32,7 +32,7 @@ const updateCompliancePageMutation = graphql`
id id
active active
searchEngineIndexing searchEngineIndexing
title entityName
description description
websiteUrl websiteUrl
email email

View File

@@ -25,7 +25,7 @@ import { useFormWithSchema } from "#/hooks/useFormWithSchema";
const compliancePageFragment = graphql` const compliancePageFragment = graphql`
fragment CompliancePageProfileSection_compliancePageFragment on CompliancePortal { fragment CompliancePageProfileSection_compliancePageFragment on CompliancePortal {
id id
title entityName
description description
websiteUrl websiteUrl
email email
@@ -35,7 +35,7 @@ const compliancePageFragment = graphql`
`; `;
const profileSchema = z.object({ const profileSchema = z.object({
title: z.string().min(1), entityName: z.string().min(1),
description: z.string().optional(), description: z.string().optional(),
websiteUrl: z.string().optional(), websiteUrl: z.string().optional(),
email: z.string().optional(), email: z.string().optional(),
@@ -58,7 +58,7 @@ export function CompliancePageProfileSection(props: {
const { formState, handleSubmit, register } = useFormWithSchema(profileSchema, { const { formState, handleSubmit, register } = useFormWithSchema(profileSchema, {
defaultValues: { defaultValues: {
title: compliancePage.title, entityName: compliancePage.entityName,
description: compliancePage.description || "", description: compliancePage.description || "",
websiteUrl: compliancePage.websiteUrl || "", websiteUrl: compliancePage.websiteUrl || "",
email: compliancePage.email || "", email: compliancePage.email || "",
@@ -73,7 +73,7 @@ export function CompliancePageProfileSection(props: {
variables: { variables: {
input: { input: {
compliancePortalId: compliancePage.id, compliancePortalId: compliancePage.id,
title: data.title, entityName: data.entityName,
description: data.description || null, description: data.description || null,
websiteUrl: data.websiteUrl || null, websiteUrl: data.websiteUrl || null,
email: data.email || null, email: data.email || null,
@@ -96,11 +96,11 @@ export function CompliancePageProfileSection(props: {
</div> </div>
<Card padded className="space-y-4"> <Card padded className="space-y-4">
<Field <Field
{...register("title")} {...register("entityName")}
readOnly={readOnly} readOnly={readOnly}
name="title" name="entityName"
label={__("Title")} label={__("Entity name")}
placeholder={__("Compliance at Acme.")} placeholder={__("Acme")}
/> />
<div> <div>
<Label>{__("Description")}</Label> <Label>{__("Description")}</Label>

View File

@@ -233,7 +233,7 @@ export function OrganizationSidebar({
: ( : (
<div className="size-24 rounded-2xl border border-border-mid shadow-mid bg-level-1" /> <div className="size-24 rounded-2xl border border-border-mid shadow-mid bg-level-1" />
)} )}
<h1 className="text-2xl mt-6">{compliancePortal.title}</h1> <h1 className="text-2xl mt-6">{compliancePortal.entityName}</h1>
<p className="text-sm text-txt-secondary mt-1"> <p className="text-sm text-txt-secondary mt-1">
{compliancePortal.description} {compliancePortal.description}
</p> </p>

View File

@@ -24,7 +24,7 @@ import { CompliancePortalContext } from "#/providers/CompliancePortalProvider";
export function useCompliancePortal(): { export function useCompliancePortal(): {
id: string; id: string;
title: string; entityName: string;
} { } {
const context = useContext(CompliancePortalContext); const context = useContext(CompliancePortalContext);
if (!context) { if (!context) {

View File

@@ -47,7 +47,7 @@ export const ndaPageQuery = graphql`
id id
} }
currentCompliancePortal @required(action: THROW) { currentCompliancePortal @required(action: THROW) {
title entityName
nonDisclosureAgreement { nonDisclosureAgreement {
fileName fileName
fileUrl fileUrl
@@ -244,7 +244,7 @@ export function NDAPage(props: {
__( __(
"%s requires you to sign an NDA before accessing compliance documents.", "%s requires you to sign an NDA before accessing compliance documents.",
), ),
compliancePortal.title, compliancePortal.entityName,
)} )}
</p> </p>
{isMobile && nda?.fileUrl && ( {isMobile && nda?.fileUrl && (

View File

@@ -107,7 +107,7 @@ export function OverviewPage() {
url={getCompliancePortalUrl("documents")} url={getCompliancePortalUrl("documents")}
/> />
<Subprocessors <Subprocessors
title={compliancePortal.title} title={compliancePortal.entityName}
subprocessors={fragment.subprocessors.edges} subprocessors={fragment.subprocessors.edges}
url={getCompliancePortalUrl("subprocessors")} url={getCompliancePortalUrl("subprocessors")}
/> />

View File

@@ -45,7 +45,7 @@ export function SubprocessorsPage({ queryRef }: Props) {
<p className="text-sm text-txt-secondary mb-4"> <p className="text-sm text-txt-secondary mb-4">
{sprintf( {sprintf(
__("Third-party subprocessors %s work with:"), __("Third-party subprocessors %s work with:"),
data.currentCompliancePortal?.title ?? "", data.currentCompliancePortal?.entityName ?? "",
)} )}
</p> </p>
<Rows> <Rows>

View File

@@ -31,7 +31,7 @@ import type { ConnectPageQuery } from "./__generated__/ConnectPageQuery.graphql"
export const connectPageQuery = graphql` export const connectPageQuery = graphql`
query ConnectPageQuery { query ConnectPageQuery {
currentCompliancePortal @required(action: THROW) { currentCompliancePortal @required(action: THROW) {
title entityName
} }
} }
`; `;
@@ -45,10 +45,10 @@ export function ConnectPage(props: {
const safeContinueUrl = useSafeContinueUrl(); const safeContinueUrl = useSafeContinueUrl();
const { const {
currentCompliancePortal: { title }, currentCompliancePortal: { entityName },
} = usePreloadedQuery<ConnectPageQuery>(connectPageQuery, queryRef); } = usePreloadedQuery<ConnectPageQuery>(connectPageQuery, queryRef);
usePageTitle(__(`Connect to ${title}'s Compliance Page`)); usePageTitle(__(`Connect to ${entityName}'s Compliance Page`));
const initiateURL = new URL("/initiate", window.location.origin); const initiateURL = new URL("/initiate", window.location.origin);
initiateURL.searchParams.set("continue", safeContinueUrl.toString()); initiateURL.searchParams.set("continue", safeContinueUrl.toString());
@@ -57,7 +57,7 @@ export function ConnectPage(props: {
<div className="space-y-6 w-full max-w-md mx-auto pt-8"> <div className="space-y-6 w-full max-w-md mx-auto pt-8">
<div className="space-y-2 text-center"> <div className="space-y-2 text-center">
<h1 className="text-3xl font-bold"> <h1 className="text-3xl font-bold">
{__(`Connect to ${title}'s Compliance Page`)} {__(`Connect to ${entityName}'s Compliance Page`)}
</h1> </h1>
<p className="text-txt-tertiary"> <p className="text-txt-tertiary">
{__( {__(

View File

@@ -31,7 +31,7 @@ export const currentCompliancePortalGraphQuery = graphql`
currentCompliancePortal @required(action: THROW) { currentCompliancePortal @required(action: THROW) {
id id
slug slug
title entityName
description description
websiteUrl websiteUrl
email email
@@ -120,6 +120,7 @@ export const currentTrustSubprocessorsQuery = graphql`
query CompliancePortalGraphCurrentSubprocessorsQuery { query CompliancePortalGraphCurrentSubprocessorsQuery {
currentCompliancePortal { currentCompliancePortal {
id id
entityName
subprocessors(first: 50) { subprocessors(first: 50) {
edges { edges {
node { node {

View File

@@ -61,7 +61,7 @@ func TestCompliancePortal_UpdateProfile(t *testing.T) {
updateCompliancePortal(input: $input) { updateCompliancePortal(input: $input) {
compliancePortal { compliancePortal {
id id
title entityName
description description
websiteUrl websiteUrl
email email
@@ -75,7 +75,7 @@ func TestCompliancePortal_UpdateProfile(t *testing.T) {
UpdateCompliancePortal struct { UpdateCompliancePortal struct {
CompliancePortal struct { CompliancePortal struct {
ID string `json:"id"` ID string `json:"id"`
Title string `json:"title"` EntityName string `json:"entityName"`
Description *string `json:"description"` Description *string `json:"description"`
WebsiteURL *string `json:"websiteUrl"` WebsiteURL *string `json:"websiteUrl"`
Email *string `json:"email"` Email *string `json:"email"`
@@ -87,7 +87,7 @@ func TestCompliancePortal_UpdateProfile(t *testing.T) {
err = owner.Execute(updateMutation, map[string]any{ err = owner.Execute(updateMutation, map[string]any{
"input": map[string]any{ "input": map[string]any{
"compliancePortalId": compliancePortalID, "compliancePortalId": compliancePortalID,
"title": "Acme Security", "entityName": "Acme Security",
"description": "We keep your data safe.", "description": "We keep your data safe.",
"websiteUrl": "https://example.com", "websiteUrl": "https://example.com",
"email": "security@example.com", "email": "security@example.com",
@@ -98,7 +98,7 @@ func TestCompliancePortal_UpdateProfile(t *testing.T) {
tc := result.UpdateCompliancePortal.CompliancePortal tc := result.UpdateCompliancePortal.CompliancePortal
assert.Equal(t, compliancePortalID, tc.ID) assert.Equal(t, compliancePortalID, tc.ID)
assert.Equal(t, "Acme Security", tc.Title) assert.Equal(t, "Acme Security", tc.EntityName)
require.NotNil(t, tc.Description) require.NotNil(t, tc.Description)
assert.Equal(t, "We keep your data safe.", *tc.Description) assert.Equal(t, "We keep your data safe.", *tc.Description)
require.NotNil(t, tc.WebsiteURL) require.NotNil(t, tc.WebsiteURL)

View File

@@ -35,10 +35,10 @@ type mcpFile struct {
} }
type compliancePortal struct { type compliancePortal struct {
ID string `json:"id"` ID string `json:"id"`
Title string `json:"title"` EntityName string `json:"entity_name"`
Active bool `json:"active"` Active bool `json:"active"`
Logo *mcpFile `json:"logo,omitempty"` Logo *mcpFile `json:"logo,omitempty"`
} }
type compliancePortalReference struct { type compliancePortalReference struct {
@@ -175,7 +175,7 @@ func TestMCP_UpdateCompliancePortal(t *testing.T) {
var updateResult struct { var updateResult struct {
CompliancePortal struct { CompliancePortal struct {
ID string `json:"id"` ID string `json:"id"`
Title string `json:"title"` EntityName string `json:"entity_name"`
Description *string `json:"description"` Description *string `json:"description"`
WebsiteURL *string `json:"website_url"` WebsiteURL *string `json:"website_url"`
Email *string `json:"email"` Email *string `json:"email"`
@@ -184,7 +184,7 @@ func TestMCP_UpdateCompliancePortal(t *testing.T) {
} }
mc.CallToolInto("updateCompliancePortal", map[string]any{ mc.CallToolInto("updateCompliancePortal", map[string]any{
"compliance_portal_id": getResult.CompliancePortal.ID, "compliance_portal_id": getResult.CompliancePortal.ID,
"title": "Acme Security", "entity_name": "Acme Security",
"description": "We keep your data safe.", "description": "We keep your data safe.",
"website_url": "https://example.com", "website_url": "https://example.com",
"email": "security@example.com", "email": "security@example.com",
@@ -192,7 +192,7 @@ func TestMCP_UpdateCompliancePortal(t *testing.T) {
}, &updateResult) }, &updateResult)
assert.Equal(t, getResult.CompliancePortal.ID, updateResult.CompliancePortal.ID) assert.Equal(t, getResult.CompliancePortal.ID, updateResult.CompliancePortal.ID)
assert.Equal(t, "Acme Security", updateResult.CompliancePortal.Title) assert.Equal(t, "Acme Security", updateResult.CompliancePortal.EntityName)
require.NotNil(t, updateResult.CompliancePortal.Description) require.NotNil(t, updateResult.CompliancePortal.Description)
assert.Equal(t, "We keep your data safe.", *updateResult.CompliancePortal.Description) assert.Equal(t, "We keep your data safe.", *updateResult.CompliancePortal.Description)
require.NotNil(t, updateResult.CompliancePortal.WebsiteURL) require.NotNil(t, updateResult.CompliancePortal.WebsiteURL)

View File

@@ -83,20 +83,20 @@ func TestCompliancePortal_VisitorConnectViaCIMD(t *testing.T) {
const query = ` const query = `
query { query {
currentCompliancePortal { currentCompliancePortal {
title entityName
} }
} }
` `
var result struct { var result struct {
CurrentCompliancePortal struct { CurrentCompliancePortal struct {
Title string `json:"title"` EntityName string `json:"entityName"`
} `json:"currentCompliancePortal"` } `json:"currentCompliancePortal"`
} }
err := visitor.ExecuteTrust(trustHost, query, nil, &result) err := visitor.ExecuteTrust(trustHost, query, nil, &result)
require.NoError(t, err, "visitor session must authenticate trust GraphQL after CIMD connect") require.NoError(t, err, "visitor session must authenticate trust GraphQL after CIMD connect")
assert.NotEmpty(t, result.CurrentCompliancePortal.Title) assert.NotEmpty(t, result.CurrentCompliancePortal.EntityName)
} }
func TestCompliancePortal_UnknownCIMDClientRejected(t *testing.T) { func TestCompliancePortal_UnknownCIMDClientRejected(t *testing.T) {

View File

@@ -77,8 +77,8 @@ export const description: INodeProperties[] = [
description: 'Whether search engines should index the compliance portal', description: 'Whether search engines should index the compliance portal',
}, },
{ {
displayName: 'Title', displayName: 'Entity Name',
name: 'title', name: 'entityName',
type: 'string', type: 'string',
displayOptions: { displayOptions: {
show: { show: {
@@ -87,7 +87,7 @@ export const description: INodeProperties[] = [
}, },
}, },
default: '', default: '',
description: 'The title shown on the public compliance page', description: 'The entity name shown on the public compliance page',
}, },
{ {
displayName: 'Description', displayName: 'Description',
@@ -155,7 +155,7 @@ export async function execute(
const websiteUrl = this.getNodeParameter('websiteUrl', itemIndex, '') as string; const websiteUrl = this.getNodeParameter('websiteUrl', itemIndex, '') as string;
const email = this.getNodeParameter('email', itemIndex, '') as string; const email = this.getNodeParameter('email', itemIndex, '') as string;
const headquarterAddress = this.getNodeParameter('headquarterAddress', itemIndex, '') as string; const headquarterAddress = this.getNodeParameter('headquarterAddress', itemIndex, '') as string;
const title = this.getNodeParameter('title', itemIndex, '') as string; const entityName = this.getNodeParameter('entityName', itemIndex, '') as string;
const query = ` const query = `
mutation UpdateCompliancePortal($input: UpdateCompliancePortalInput!) { mutation UpdateCompliancePortal($input: UpdateCompliancePortalInput!) {
@@ -164,7 +164,7 @@ export async function execute(
id id
active active
searchEngineIndexing searchEngineIndexing
title entityName
description description
websiteUrl websiteUrl
email email
@@ -178,7 +178,7 @@ export async function execute(
const input: Record<string, unknown> = { const input: Record<string, unknown> = {
compliancePortalId, compliancePortalId,
title, entityName,
description, description,
websiteUrl, websiteUrl,
email, email,

View File

@@ -49,7 +49,7 @@ mutation($input: UpdateCompliancePortalInput!) {
id id
active active
searchEngineIndexing searchEngineIndexing
title entityName
description description
websiteUrl websiteUrl
email email
@@ -74,7 +74,7 @@ type updateResponse struct {
ID string `json:"id"` ID string `json:"id"`
Active bool `json:"active"` Active bool `json:"active"`
SearchEngineIndexing string `json:"searchEngineIndexing"` SearchEngineIndexing string `json:"searchEngineIndexing"`
Title string `json:"title"` EntityName string `json:"entityName"`
Description *string `json:"description"` Description *string `json:"description"`
WebsiteURL *string `json:"websiteUrl"` WebsiteURL *string `json:"websiteUrl"`
Email *string `json:"email"` Email *string `json:"email"`
@@ -92,7 +92,7 @@ func NewCmdUpdate(f *cmdutil.Factory) *cobra.Command {
flagWebsiteURL string flagWebsiteURL string
flagEmail string flagEmail string
flagHeadquarterAddress string flagHeadquarterAddress string
flagTitle string flagEntityName string
) )
cmd := &cobra.Command{ cmd := &cobra.Command{
@@ -193,8 +193,8 @@ func NewCmdUpdate(f *cmdutil.Factory) *cobra.Command {
input["headquarterAddress"] = flagHeadquarterAddress input["headquarterAddress"] = flagHeadquarterAddress
} }
if cmd.Flags().Changed("title") { if cmd.Flags().Changed("entity-name") {
input["title"] = flagTitle input["entityName"] = flagEntityName
} }
if len(input) == 1 { if len(input) == 1 {
@@ -232,7 +232,7 @@ func NewCmdUpdate(f *cmdutil.Factory) *cobra.Command {
cmd.Flags().StringVar(&flagWebsiteURL, "website-url", "", "Compliance page website URL") cmd.Flags().StringVar(&flagWebsiteURL, "website-url", "", "Compliance page website URL")
cmd.Flags().StringVar(&flagEmail, "email", "", "Compliance page contact email") cmd.Flags().StringVar(&flagEmail, "email", "", "Compliance page contact email")
cmd.Flags().StringVar(&flagHeadquarterAddress, "headquarter-address", "", "Compliance page headquarter address") cmd.Flags().StringVar(&flagHeadquarterAddress, "headquarter-address", "", "Compliance page headquarter address")
cmd.Flags().StringVar(&flagTitle, "title", "", "Public compliance page title") cmd.Flags().StringVar(&flagEntityName, "entity-name", "", "Entity name shown on the public compliance page")
return cmd return cmd
} }

View File

@@ -46,7 +46,7 @@ type (
Slug *string Slug *string
SearchEngineIndexing *coredata.SearchEngineIndexing SearchEngineIndexing *coredata.SearchEngineIndexing
NonDisclosureAgreementFileID *gid.GID NonDisclosureAgreementFileID *gid.GID
Title *string EntityName *string
Description **string Description **string
WebsiteURL **string WebsiteURL **string
Email **string Email **string
@@ -75,8 +75,8 @@ func (utcr *UpdateRequest) Validate() error {
v.Check(utcr.Slug, "slug", validator.SafeText(NameMaxLength)) v.Check(utcr.Slug, "slug", validator.SafeText(NameMaxLength))
v.Check(utcr.NonDisclosureAgreementFileID, "non_disclosure_agreement_file_id", validator.GID(coredata.FileEntityType)) v.Check(utcr.NonDisclosureAgreementFileID, "non_disclosure_agreement_file_id", validator.GID(coredata.FileEntityType))
if utcr.Title != nil { if utcr.EntityName != nil {
v.Check(*utcr.Title, "title", validator.Required(), validator.SafeTextNoNewLine(TitleMaxLength)) v.Check(*utcr.EntityName, "entity_name", validator.Required(), validator.SafeTextNoNewLine(TitleMaxLength))
} }
if utcr.Description != nil { if utcr.Description != nil {
@@ -214,8 +214,8 @@ func (s *Service) Update(
portal.SearchEngineIndexing = *req.SearchEngineIndexing portal.SearchEngineIndexing = *req.SearchEngineIndexing
} }
if req.Title != nil { if req.EntityName != nil {
portal.Title = *req.Title portal.EntityName = *req.EntityName
} }
if req.Description != nil { if req.Description != nil {

View File

@@ -85,7 +85,7 @@ func BuildClientMetadataDocument(
doc := oauth2.ClientMetadataDocument{ doc := oauth2.ClientMetadataDocument{
ClientID: clientID, ClientID: clientID,
ClientName: portal.Title, ClientName: "Compliance at " + portal.EntityName + ".",
ClientURI: portalRootURL, ClientURI: portalRootURL,
RedirectURIs: []string{redirectURI}, RedirectURIs: []string{redirectURI},
TokenEndpointAuthMethod: "none", TokenEndpointAuthMethod: "none",

View File

@@ -66,7 +66,7 @@ func TestBuildClientMetadataDocument(t *testing.T) {
websiteURL := "https://www.acme.com" websiteURL := "https://www.acme.com"
portal := &coredata.CompliancePortal{ portal := &coredata.CompliancePortal{
Title: "Acme Compliance Page", EntityName: "Acme",
WebsiteURL: &websiteURL, WebsiteURL: &websiteURL,
} }
@@ -76,7 +76,7 @@ func TestBuildClientMetadataDocument(t *testing.T) {
) )
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, "https://acme.example.com/.well-known/oauth-client-metadata", doc.ClientID) assert.Equal(t, "https://acme.example.com/.well-known/oauth-client-metadata", doc.ClientID)
assert.Equal(t, "Acme Compliance Page", doc.ClientName) assert.Equal(t, "Compliance at Acme.", doc.ClientName)
assert.Equal(t, []string{"https://acme.example.com/callback"}, doc.RedirectURIs) assert.Equal(t, []string{"https://acme.example.com/callback"}, doc.RedirectURIs)
assert.Equal(t, "https://acme.example.com", doc.ClientURI) assert.Equal(t, "https://acme.example.com", doc.ClientURI)
assert.Equal(t, VisitorOAuthScope, doc.Scope) assert.Equal(t, VisitorOAuthScope, doc.Scope)
@@ -88,7 +88,7 @@ func TestBuildClientMetadataDocument_LogoURIUsesBrandLogoEndpoint(t *testing.T)
logoFileID := gid.MustParseGID("WR-qMrB5AAEAGQAAAZ9mIO8B8vDFQ-i3") logoFileID := gid.MustParseGID("WR-qMrB5AAEAGQAAAZ9mIO8B8vDFQ-i3")
portal := &coredata.CompliancePortal{ portal := &coredata.CompliancePortal{
Title: "Acme Compliance Page", EntityName: "Acme",
LogoFileID: &logoFileID, LogoFileID: &logoFileID,
} }

View File

@@ -49,7 +49,7 @@ type (
NonDisclosureAgreementFileID *gid.GID `db:"non_disclosure_agreement_file_id"` NonDisclosureAgreementFileID *gid.GID `db:"non_disclosure_agreement_file_id"`
DefaultDomainID *gid.GID `db:"default_domain_id"` DefaultDomainID *gid.GID `db:"default_domain_id"`
CustomDomainID *gid.GID `db:"custom_domain_id"` CustomDomainID *gid.GID `db:"custom_domain_id"`
Title string `db:"title"` EntityName string `db:"entity_name"`
Description *string `db:"description"` Description *string `db:"description"`
WebsiteURL *string `db:"website_url"` WebsiteURL *string `db:"website_url"`
Email *string `db:"email"` Email *string `db:"email"`
@@ -129,7 +129,7 @@ SELECT
non_disclosure_agreement_file_id, non_disclosure_agreement_file_id,
default_domain_id, default_domain_id,
custom_domain_id, custom_domain_id,
title, entity_name,
description, description,
website_url, website_url,
email, email,
@@ -188,7 +188,7 @@ SELECT
non_disclosure_agreement_file_id, non_disclosure_agreement_file_id,
default_domain_id, default_domain_id,
custom_domain_id, custom_domain_id,
title, entity_name,
description, description,
website_url, website_url,
email, email,
@@ -247,7 +247,7 @@ SELECT
non_disclosure_agreement_file_id, non_disclosure_agreement_file_id,
default_domain_id, default_domain_id,
custom_domain_id, custom_domain_id,
title, entity_name,
description, description,
website_url, website_url,
email, email,
@@ -306,7 +306,7 @@ SELECT
non_disclosure_agreement_file_id, non_disclosure_agreement_file_id,
default_domain_id, default_domain_id,
custom_domain_id, custom_domain_id,
title, entity_name,
description, description,
website_url, website_url,
email, email,
@@ -365,7 +365,7 @@ SELECT
non_disclosure_agreement_file_id, non_disclosure_agreement_file_id,
default_domain_id, default_domain_id,
custom_domain_id, custom_domain_id,
title, entity_name,
description, description,
website_url, website_url,
email, email,
@@ -420,7 +420,7 @@ INSERT INTO trust_centers (
non_disclosure_agreement_file_id, non_disclosure_agreement_file_id,
default_domain_id, default_domain_id,
custom_domain_id, custom_domain_id,
title, entity_name,
description, description,
website_url, website_url,
email, email,
@@ -440,7 +440,7 @@ INSERT INTO trust_centers (
@non_disclosure_agreement_file_id, @non_disclosure_agreement_file_id,
@default_domain_id, @default_domain_id,
@custom_domain_id, @custom_domain_id,
@title, @entity_name,
@description, @description,
@website_url, @website_url,
@email, @email,
@@ -463,7 +463,7 @@ INSERT INTO trust_centers (
"non_disclosure_agreement_file_id": tc.NonDisclosureAgreementFileID, "non_disclosure_agreement_file_id": tc.NonDisclosureAgreementFileID,
"default_domain_id": tc.DefaultDomainID, "default_domain_id": tc.DefaultDomainID,
"custom_domain_id": tc.CustomDomainID, "custom_domain_id": tc.CustomDomainID,
"title": tc.Title, "entity_name": tc.EntityName,
"description": tc.Description, "description": tc.Description,
"website_url": tc.WebsiteURL, "website_url": tc.WebsiteURL,
"email": tc.Email, "email": tc.Email,
@@ -502,7 +502,7 @@ SET
non_disclosure_agreement_file_id = @non_disclosure_agreement_file_id, non_disclosure_agreement_file_id = @non_disclosure_agreement_file_id,
default_domain_id = @default_domain_id, default_domain_id = @default_domain_id,
custom_domain_id = @custom_domain_id, custom_domain_id = @custom_domain_id,
title = @title, entity_name = @entity_name,
description = @description, description = @description,
website_url = @website_url, website_url = @website_url,
email = @email, email = @email,
@@ -525,7 +525,7 @@ WHERE
"non_disclosure_agreement_file_id": tc.NonDisclosureAgreementFileID, "non_disclosure_agreement_file_id": tc.NonDisclosureAgreementFileID,
"default_domain_id": tc.DefaultDomainID, "default_domain_id": tc.DefaultDomainID,
"custom_domain_id": tc.CustomDomainID, "custom_domain_id": tc.CustomDomainID,
"title": tc.Title, "entity_name": tc.EntityName,
"description": tc.Description, "description": tc.Description,
"website_url": tc.WebsiteURL, "website_url": tc.WebsiteURL,
"email": tc.Email, "email": tc.Email,

View File

@@ -0,0 +1,36 @@
-- Copyright (c) 2026 Probo Inc <hello@probo.com>.
--
-- Permission is hereby granted, free of charge, to any person obtaining a copy
-- of this software and associated documentation files (the "Software"), to deal
-- in the Software without restriction, including without limitation the rights
-- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-- copies of the Software, and to permit persons to whom the Software is
-- furnished to do so, subject to the following conditions:
--
-- The above copyright notice and this permission notice shall be included in
-- all copies or substantial portions of the Software.
--
-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-- SOFTWARE.
-- entity_name is the short name shown in the hero ("Compliance at {{name}}.")
-- and TopBar brand. Prefill from the organization so existing portals keep a
-- sensible default; customized full titles in the old title column are dropped.
ALTER TABLE trust_centers
ADD COLUMN entity_name TEXT NOT NULL DEFAULT '';
UPDATE trust_centers tc
SET entity_name = o.name
FROM organizations o
WHERE tc.organization_id = o.id;
ALTER TABLE trust_centers
ALTER COLUMN entity_name DROP DEFAULT;
ALTER TABLE trust_centers
DROP COLUMN title;

View File

@@ -614,7 +614,7 @@ func (s *OrganizationService) CreateOrganization(
TenantID: organization.TenantID, TenantID: organization.TenantID,
Active: false, Active: false,
Slug: slug.MakeWithEntropy(organization.Name), Slug: slug.MakeWithEntropy(organization.Name),
Title: "Compliance at " + organization.Name + ".", EntityName: organization.Name,
SearchEngineIndexing: coredata.SearchEngineIndexingNotIndexable, SearchEngineIndexing: coredata.SearchEngineIndexingNotIndexable,
MailingListID: &mailingList.ID, MailingListID: &mailingList.ID,
CreatedAt: now, CreatedAt: now,

View File

@@ -9,7 +9,7 @@ type CompliancePortal implements Node {
websiteUrl: String websiteUrl: String
email: String email: String
headquarterAddress: String headquarterAddress: String
title: String! entityName: String!
nonDisclosureAgreement: NonDisclosureAgreement @goField(forceResolver: true) nonDisclosureAgreement: NonDisclosureAgreement @goField(forceResolver: true)

View File

@@ -134,7 +134,8 @@ func compliancePageHeadData() HeadDataFunc {
compliancePageBaseURL := complianceportal.CompliancePortalBaseURLFromContext(r.Context()) compliancePageBaseURL := complianceportal.CompliancePortalBaseURLFromContext(r.Context())
description := tc.Title pageTitle := "Compliance at " + tc.EntityName + "."
description := pageTitle
if tc.Description != nil && *tc.Description != "" { if tc.Description != nil && *tc.Description != "" {
description = *tc.Description description = *tc.Description
} }
@@ -142,7 +143,7 @@ func compliancePageHeadData() HeadDataFunc {
htmlLang, canonical, hreflang := SEOFromRequest(r) htmlLang, canonical, hreflang := SEOFromRequest(r)
headData := HeadData{ headData := HeadData{
Title: tc.Title, Title: pageTitle,
Description: description, Description: description,
HTMLLang: htmlLang, HTMLLang: htmlLang,
OGURL: canonical, OGURL: canonical,

View File

@@ -29,7 +29,7 @@ func NewCompliancePortal(cp *coredata.CompliancePortal) *CompliancePortal {
ID: cp.ID, ID: cp.ID,
Active: cp.Active, Active: cp.Active,
Slug: cp.Slug, Slug: cp.Slug,
Title: cp.Title, EntityName: cp.EntityName,
Description: cp.Description, Description: cp.Description,
WebsiteURL: cp.WebsiteURL, WebsiteURL: cp.WebsiteURL,
Email: cp.Email, Email: cp.Email,

View File

@@ -804,7 +804,7 @@ func (r *mutationResolver) UpdateCompliancePortal(ctx context.Context, input typ
WebsiteURL: gqlutils.UnwrapOmittable(input.WebsiteURL), WebsiteURL: gqlutils.UnwrapOmittable(input.WebsiteURL),
Email: gqlutils.UnwrapOmittable(input.Email), Email: gqlutils.UnwrapOmittable(input.Email),
HeadquarterAddress: gqlutils.UnwrapOmittable(input.HeadquarterAddress), HeadquarterAddress: gqlutils.UnwrapOmittable(input.HeadquarterAddress),
Title: input.Title, EntityName: input.EntityName,
}, },
) )
if err != nil { if err != nil {

View File

@@ -344,7 +344,7 @@ type CompliancePortal implements Node
websiteUrl: String websiteUrl: String
email: String email: String
headquarterAddress: String headquarterAddress: String
title: String! entityName: String!
createdAt: Datetime! createdAt: Datetime!
updatedAt: Datetime! updatedAt: Datetime!
organization: Organization! @goField(forceResolver: true) organization: Organization! @goField(forceResolver: true)
@@ -747,7 +747,7 @@ input UpdateCompliancePortalInput {
compliancePortalId: ID! compliancePortalId: ID!
active: Boolean active: Boolean
searchEngineIndexing: SearchEngineIndexing searchEngineIndexing: SearchEngineIndexing
title: String entityName: String
description: String @goField(omittable: true) description: String @goField(omittable: true)
websiteUrl: String @goField(omittable: true) websiteUrl: String @goField(omittable: true)
email: String @goField(omittable: true) email: String @goField(omittable: true)

View File

@@ -38,7 +38,7 @@ type CompliancePortal struct {
WebsiteURL *string `json:"websiteUrl,omitempty"` WebsiteURL *string `json:"websiteUrl,omitempty"`
Email *string `json:"email,omitempty"` Email *string `json:"email,omitempty"`
HeadquarterAddress *string `json:"headquarterAddress,omitempty"` HeadquarterAddress *string `json:"headquarterAddress,omitempty"`
Title string `json:"title"` EntityName string `json:"entityName"`
CreatedAt time.Time `json:"createdAt"` CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"` UpdatedAt time.Time `json:"updatedAt"`
Organization *Organization `json:"organization"` Organization *Organization `json:"organization"`
@@ -67,7 +67,7 @@ func NewCompliancePortal(tc *coredata.CompliancePortal) *CompliancePortal {
WebsiteURL: tc.WebsiteURL, WebsiteURL: tc.WebsiteURL,
Email: tc.Email, Email: tc.Email,
HeadquarterAddress: tc.HeadquarterAddress, HeadquarterAddress: tc.HeadquarterAddress,
Title: tc.Title, EntityName: tc.EntityName,
CreatedAt: tc.CreatedAt, CreatedAt: tc.CreatedAt,
UpdatedAt: tc.UpdatedAt, UpdatedAt: tc.UpdatedAt,
} }

View File

@@ -4954,8 +4954,8 @@ func (r *Resolver) UpdateCompliancePortalTool(ctx context.Context, req *mcp.Call
updateReq.Email = UnwrapOmittable(input.Email) updateReq.Email = UnwrapOmittable(input.Email)
updateReq.HeadquarterAddress = UnwrapOmittable(input.HeadquarterAddress) updateReq.HeadquarterAddress = UnwrapOmittable(input.HeadquarterAddress)
if title := UnwrapOmittable(input.Title); title != nil { if entityName := UnwrapOmittable(input.EntityName); entityName != nil {
updateReq.Title = *title updateReq.EntityName = *entityName
} }
compliancePortal, _, err := prb.Update(ctx, scope, updateReq) compliancePortal, _, err := prb.Update(ctx, scope, updateReq)

View File

@@ -8832,7 +8832,7 @@ components:
- organization_id - organization_id
- active - active
- search_engine_indexing - search_engine_indexing
- title - entity_name
- created_at - created_at
- updated_at - updated_at
properties: properties:
@@ -8870,9 +8870,9 @@ components:
- string - string
- "null" - "null"
description: Compliance page headquarter address description: Compliance page headquarter address
title: entity_name:
type: string type: string
description: Public compliance page title description: Entity name shown on the public compliance page
created_at: created_at:
type: string type: string
format: date-time format: date-time
@@ -9079,11 +9079,11 @@ components:
- "null" - "null"
description: Compliance page headquarter address description: Compliance page headquarter address
go.probo.inc/mcpgen/omittable: true go.probo.inc/mcpgen/omittable: true
title: entity_name:
type: type:
- string - string
- "null" - "null"
description: Public compliance page title description: Entity name shown on the public compliance page
go.probo.inc/mcpgen/omittable: true go.probo.inc/mcpgen/omittable: true
UpdateCompliancePortalOutput: UpdateCompliancePortalOutput:

View File

@@ -31,7 +31,7 @@ func NewCompliancePortal(tc *coredata.CompliancePortal) *CompliancePortal {
OrganizationID: tc.OrganizationID, OrganizationID: tc.OrganizationID,
Active: tc.Active, Active: tc.Active,
SearchEngineIndexing: tc.SearchEngineIndexing, SearchEngineIndexing: tc.SearchEngineIndexing,
Title: tc.Title, EntityName: tc.EntityName,
Description: tc.Description, Description: tc.Description,
WebsiteURL: tc.WebsiteURL, WebsiteURL: tc.WebsiteURL,
Email: tc.Email, Email: tc.Email,