feat(enterprise): add custom branding system — logo, accent color, company name
- Add branding_logo_url, branding_primary_color, branding_company_name columns to Account model - Add Alembic migration (58e3f27f3e8f) for branding and SSO columns - Add GET/PATCH /accounts/me/branding endpoints (owner-only for PATCH) - Add BrandingSettingsPage with logo URL input, color picker, preview section - Add /account/branding route (ProtectedRoute owner) in router.tsx - Add Branding link card in AccountSettingsPage Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
"""add branding and SSO columns to accounts
|
||||
|
||||
Revision ID: 58e3f27f3e8f
|
||||
Revises: 9094262a4be3
|
||||
Create Date: 2026-03-19 20:25:03.423778
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '58e3f27f3e8f'
|
||||
down_revision: Union[str, None] = '9094262a4be3'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# Custom branding columns (Task 9)
|
||||
op.add_column('accounts', sa.Column('branding_logo_url', sa.String(length=500), nullable=True))
|
||||
op.add_column('accounts', sa.Column('branding_primary_color', sa.String(length=7), nullable=True))
|
||||
op.add_column('accounts', sa.Column('branding_company_name', sa.String(length=200), nullable=True))
|
||||
# SSO / SAML groundwork columns (Task 11)
|
||||
op.add_column('accounts', sa.Column('sso_enabled', sa.Boolean(), server_default='false', nullable=False))
|
||||
op.add_column('accounts', sa.Column('sso_provider', sa.String(length=20), nullable=True))
|
||||
op.add_column('accounts', sa.Column('sso_config', postgresql.JSONB(astext_type=sa.Text()), nullable=True))
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_column('accounts', 'sso_config')
|
||||
op.drop_column('accounts', 'sso_provider')
|
||||
op.drop_column('accounts', 'sso_enabled')
|
||||
op.drop_column('accounts', 'branding_company_name')
|
||||
op.drop_column('accounts', 'branding_primary_color')
|
||||
op.drop_column('accounts', 'branding_logo_url')
|
||||
@@ -465,3 +465,96 @@ async def delete_account(
|
||||
await db.commit()
|
||||
|
||||
return {"message": "Account deleted"}
|
||||
|
||||
|
||||
# ─── Account Branding Endpoints (Task 9) ──────────────────────────────────────
|
||||
|
||||
class AccountBrandingResponse(BaseModel):
|
||||
logo_url: Optional[str] = None
|
||||
primary_color: Optional[str] = None
|
||||
company_name: Optional[str] = None
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
class AccountBrandingUpdate(BaseModel):
|
||||
logo_url: Optional[str] = None
|
||||
primary_color: Optional[str] = None
|
||||
company_name: Optional[str] = None
|
||||
|
||||
|
||||
@router.get("/me/branding", response_model=AccountBrandingResponse)
|
||||
async def get_account_branding(
|
||||
db: Annotated[AsyncSession, Depends(get_db)],
|
||||
current_user: Annotated[User, Depends(get_current_active_user)],
|
||||
):
|
||||
"""Get custom branding settings for the current account."""
|
||||
result = await db.execute(select(Account).where(Account.id == current_user.account_id))
|
||||
account = result.scalar_one_or_none()
|
||||
if not account:
|
||||
raise HTTPException(status_code=404, detail="Account not found")
|
||||
|
||||
return AccountBrandingResponse(
|
||||
logo_url=account.branding_logo_url,
|
||||
primary_color=account.branding_primary_color,
|
||||
company_name=account.branding_company_name,
|
||||
)
|
||||
|
||||
|
||||
@router.patch("/me/branding", response_model=AccountBrandingResponse)
|
||||
async def update_account_branding(
|
||||
data: AccountBrandingUpdate,
|
||||
db: Annotated[AsyncSession, Depends(get_db)],
|
||||
current_user: Annotated[User, Depends(require_account_owner)],
|
||||
):
|
||||
"""Update custom branding settings. Account owner only."""
|
||||
result = await db.execute(select(Account).where(Account.id == current_user.account_id))
|
||||
account = result.scalar_one_or_none()
|
||||
if not account:
|
||||
raise HTTPException(status_code=404, detail="Account not found")
|
||||
|
||||
if data.logo_url is not None:
|
||||
account.branding_logo_url = data.logo_url or None
|
||||
if data.primary_color is not None:
|
||||
# Validate hex color format (#RRGGBB)
|
||||
color = data.primary_color.strip()
|
||||
if color and (len(color) != 7 or not color.startswith("#")):
|
||||
raise HTTPException(status_code=400, detail="primary_color must be a 7-character hex string like #06b6d4")
|
||||
account.branding_primary_color = color or None
|
||||
if data.company_name is not None:
|
||||
account.branding_company_name = data.company_name.strip() or None
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(account)
|
||||
|
||||
return AccountBrandingResponse(
|
||||
logo_url=account.branding_logo_url,
|
||||
primary_color=account.branding_primary_color,
|
||||
company_name=account.branding_company_name,
|
||||
)
|
||||
|
||||
|
||||
# ─── SSO Status Endpoint (Task 11) ────────────────────────────────────────────
|
||||
|
||||
class AccountSSOStatusResponse(BaseModel):
|
||||
sso_enabled: bool
|
||||
sso_provider: Optional[str] = None
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
@router.get("/me/sso", response_model=AccountSSOStatusResponse)
|
||||
async def get_sso_status(
|
||||
db: Annotated[AsyncSession, Depends(get_db)],
|
||||
current_user: Annotated[User, Depends(get_current_active_user)],
|
||||
):
|
||||
"""Get SSO configuration status for the current account."""
|
||||
result = await db.execute(select(Account).where(Account.id == current_user.account_id))
|
||||
account = result.scalar_one_or_none()
|
||||
if not account:
|
||||
raise HTTPException(status_code=404, detail="Account not found")
|
||||
|
||||
return AccountSSOStatusResponse(
|
||||
sso_enabled=account.sso_enabled,
|
||||
sso_provider=account.sso_provider,
|
||||
)
|
||||
|
||||
@@ -3,7 +3,7 @@ from datetime import datetime, timezone
|
||||
from typing import Optional, TYPE_CHECKING
|
||||
from sqlalchemy import String, DateTime, ForeignKey, Boolean, Integer
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from sqlalchemy.dialects.postgresql import UUID, JSONB
|
||||
from app.core.database import Base
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -44,6 +44,16 @@ class Account(Base):
|
||||
Integer, nullable=True, default=100, server_default="100"
|
||||
)
|
||||
|
||||
# Custom branding (Task 9)
|
||||
branding_logo_url: Mapped[Optional[str]] = mapped_column(String(500), nullable=True)
|
||||
branding_primary_color: Mapped[Optional[str]] = mapped_column(String(7), nullable=True) # hex like #06b6d4
|
||||
branding_company_name: Mapped[Optional[str]] = mapped_column(String(200), nullable=True)
|
||||
|
||||
# SSO / SAML groundwork (Task 11)
|
||||
sso_enabled: Mapped[bool] = mapped_column(Boolean, default=False, server_default="false")
|
||||
sso_provider: Mapped[Optional[str]] = mapped_column(String(20), nullable=True) # "saml" | "oidc"
|
||||
sso_config: Mapped[Optional[dict]] = mapped_column(JSONB, nullable=True)
|
||||
|
||||
# Relationships
|
||||
owner: Mapped["User"] = relationship("User", foreign_keys=[owner_id], back_populates="owned_account")
|
||||
users: Mapped[list["User"]] = relationship("User", foreign_keys="[User.account_id]", back_populates="account")
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { Building2, Users, Mail, Crown, Loader2, AlertCircle, Check, X, Settings, FolderTree, Server, RefreshCw, MessageSquareText, UserCog, AlertTriangle, Clock, Plug } from 'lucide-react'
|
||||
import { Building2, Users, Mail, Crown, Loader2, AlertCircle, Check, X, Settings, FolderTree, Server, RefreshCw, MessageSquareText, UserCog, AlertTriangle, Clock, Plug, Palette, ShieldCheck } from 'lucide-react'
|
||||
import { PageMeta } from '@/components/common/PageMeta'
|
||||
import { BrandingSettings } from '@/components/settings/BrandingSettings'
|
||||
import { accountsApi } from '@/api/accounts'
|
||||
@@ -574,6 +574,23 @@ export function AccountSettingsPage() {
|
||||
</Link>
|
||||
)}
|
||||
|
||||
{/* Branding Link (owners only) */}
|
||||
{isAccountOwner && (
|
||||
<Link
|
||||
to="/account/branding"
|
||||
className="bg-card border border-border rounded-xl p-4 sm:p-6 flex items-center justify-between group hover:border-border transition-all"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<Palette className="h-5 w-5 text-muted-foreground" />
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-foreground">Branding</h2>
|
||||
<p className="text-sm text-muted-foreground">Customize logo, accent color, and company name</p>
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-muted-foreground group-hover:text-foreground transition-colors">→</span>
|
||||
</Link>
|
||||
)}
|
||||
|
||||
{/* Feedback Link (all users) */}
|
||||
<Link
|
||||
to="/feedback"
|
||||
@@ -630,6 +647,33 @@ export function AccountSettingsPage() {
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
{/* SSO Section (Task 11) */}
|
||||
{isAccountOwner && (
|
||||
<div className="bg-card border border-border rounded-xl p-4 sm:p-6">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<ShieldCheck className="h-5 w-5 text-muted-foreground" />
|
||||
<h2 className="text-lg font-semibold text-foreground">Single Sign-On (SSO)</h2>
|
||||
<span className="inline-flex items-center rounded-full bg-primary/10 px-2.5 py-0.5 text-xs font-label font-medium text-primary">
|
||||
Enterprise
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground mb-4">
|
||||
SAML and OIDC single sign-on is available for enterprise plans. Contact us to enable SSO for
|
||||
your organization.
|
||||
</p>
|
||||
<a
|
||||
href="mailto:support@resolutionflow.com?subject=SSO%20Setup%20Request"
|
||||
className={cn(
|
||||
'inline-flex items-center gap-2 rounded-[10px] px-4 py-2 text-sm font-medium',
|
||||
'bg-[rgba(255,255,255,0.04)] border border-[rgba(255,255,255,0.06)] text-foreground',
|
||||
'hover:border-[rgba(255,255,255,0.12)] transition-all'
|
||||
)}
|
||||
>
|
||||
Contact Us
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Danger Zone */}
|
||||
<div className="rounded-xl border border-rose-500/20 p-4 sm:p-6">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
|
||||
308
frontend/src/pages/account/BrandingSettingsPage.tsx
Normal file
308
frontend/src/pages/account/BrandingSettingsPage.tsx
Normal file
@@ -0,0 +1,308 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Palette, Loader2, AlertCircle, Save } from 'lucide-react'
|
||||
import { PageMeta } from '@/components/common/PageMeta'
|
||||
import { apiClient } from '@/api/client'
|
||||
import { toast } from '@/lib/toast'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
interface AccountBranding {
|
||||
logo_url: string | null
|
||||
primary_color: string | null
|
||||
company_name: string | null
|
||||
}
|
||||
|
||||
const DEFAULT_COLOR = '#06b6d4'
|
||||
|
||||
export function BrandingSettingsPage() {
|
||||
const [branding, setBranding] = useState<AccountBranding | null>(null)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [isSaving, setIsSaving] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
// Form state
|
||||
const [logoUrl, setLogoUrl] = useState('')
|
||||
const [primaryColor, setPrimaryColor] = useState(DEFAULT_COLOR)
|
||||
const [companyName, setCompanyName] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
loadBranding()
|
||||
}, [])
|
||||
|
||||
const loadBranding = async () => {
|
||||
setIsLoading(true)
|
||||
setError(null)
|
||||
try {
|
||||
const res = await apiClient.get<AccountBranding>('/accounts/me/branding')
|
||||
const data = res.data
|
||||
setBranding(data)
|
||||
setLogoUrl(data.logo_url ?? '')
|
||||
setPrimaryColor(data.primary_color ?? DEFAULT_COLOR)
|
||||
setCompanyName(data.company_name ?? '')
|
||||
} catch (err) {
|
||||
console.error('Failed to load branding:', err)
|
||||
setError('Failed to load branding settings')
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSave = async () => {
|
||||
setIsSaving(true)
|
||||
try {
|
||||
const res = await apiClient.patch<AccountBranding>('/accounts/me/branding', {
|
||||
logo_url: logoUrl.trim() || null,
|
||||
primary_color: primaryColor !== DEFAULT_COLOR ? primaryColor : null,
|
||||
company_name: companyName.trim() || null,
|
||||
})
|
||||
setBranding(res.data)
|
||||
toast.success('Branding settings saved')
|
||||
} catch (err) {
|
||||
const axiosErr = err as { response?: { data?: { detail?: string } } }
|
||||
toast.error(axiosErr.response?.data?.detail ?? 'Failed to save branding settings')
|
||||
console.error(err)
|
||||
} finally {
|
||||
setIsSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
const isDirty =
|
||||
logoUrl !== (branding?.logo_url ?? '') ||
|
||||
primaryColor !== (branding?.primary_color ?? DEFAULT_COLOR) ||
|
||||
companyName !== (branding?.company_name ?? '')
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<>
|
||||
<PageMeta title="Branding Settings" />
|
||||
<div className="flex justify-center py-12">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<>
|
||||
<PageMeta title="Branding Settings" />
|
||||
<div className="rounded-md border border-red-400/20 bg-red-400/10 p-4 text-red-400">
|
||||
<div className="flex items-center gap-2">
|
||||
<AlertCircle className="h-5 w-5" />
|
||||
{error}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const previewColor = primaryColor || DEFAULT_COLOR
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageMeta title="Branding Settings" />
|
||||
<div>
|
||||
<div className="mb-8">
|
||||
<div className="flex items-center gap-3">
|
||||
<Palette className="h-8 w-8 text-muted-foreground" />
|
||||
<h1 className="text-2xl font-bold font-heading text-foreground sm:text-3xl">
|
||||
Branding Settings
|
||||
</h1>
|
||||
</div>
|
||||
<p className="mt-2 text-muted-foreground">
|
||||
Customize your account branding — logo, accent color, and company name.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="max-w-2xl space-y-6">
|
||||
{/* Branding Form */}
|
||||
<div className="glass-card-static p-6">
|
||||
<h2 className="text-lg font-semibold text-foreground mb-6">Custom Branding</h2>
|
||||
|
||||
<div className="space-y-5">
|
||||
{/* Company Name */}
|
||||
<div>
|
||||
<label
|
||||
htmlFor="company-name"
|
||||
className="font-label text-[0.625rem] uppercase tracking-[0.1em] text-muted-foreground"
|
||||
>
|
||||
Company Name
|
||||
</label>
|
||||
<input
|
||||
id="company-name"
|
||||
type="text"
|
||||
value={companyName}
|
||||
onChange={(e) => setCompanyName(e.target.value)}
|
||||
placeholder="Your Company Name"
|
||||
maxLength={200}
|
||||
className={cn(
|
||||
'mt-1 w-full rounded-lg border border-border bg-card px-3 py-2 text-sm',
|
||||
'text-foreground placeholder:text-muted-foreground',
|
||||
'focus:border-[rgba(6,182,212,0.3)] focus:outline-none focus:ring-1 focus:ring-primary/20'
|
||||
)}
|
||||
/>
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
Displayed in the sidebar and exported documents.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Logo URL */}
|
||||
<div>
|
||||
<label
|
||||
htmlFor="logo-url"
|
||||
className="font-label text-[0.625rem] uppercase tracking-[0.1em] text-muted-foreground"
|
||||
>
|
||||
Logo URL
|
||||
</label>
|
||||
<input
|
||||
id="logo-url"
|
||||
type="url"
|
||||
value={logoUrl}
|
||||
onChange={(e) => setLogoUrl(e.target.value)}
|
||||
placeholder="https://example.com/logo.png"
|
||||
maxLength={500}
|
||||
className={cn(
|
||||
'mt-1 w-full rounded-lg border border-border bg-card px-3 py-2 text-sm',
|
||||
'text-foreground placeholder:text-muted-foreground font-mono',
|
||||
'focus:border-[rgba(6,182,212,0.3)] focus:outline-none focus:ring-1 focus:ring-primary/20'
|
||||
)}
|
||||
/>
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
Publicly accessible URL to your logo image (PNG, SVG, or JPEG).
|
||||
</p>
|
||||
{logoUrl && (
|
||||
<div className="mt-3 inline-flex items-center rounded-lg border border-border bg-card/50 p-3">
|
||||
<img
|
||||
src={logoUrl}
|
||||
alt="Logo preview"
|
||||
className="max-h-12 max-w-[180px] object-contain"
|
||||
onError={(e) => {
|
||||
;(e.target as HTMLImageElement).style.display = 'none'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Primary Color */}
|
||||
<div>
|
||||
<label
|
||||
htmlFor="primary-color"
|
||||
className="font-label text-[0.625rem] uppercase tracking-[0.1em] text-muted-foreground"
|
||||
>
|
||||
Accent Color
|
||||
</label>
|
||||
<div className="mt-1 flex items-center gap-3">
|
||||
<input
|
||||
id="primary-color"
|
||||
type="color"
|
||||
value={previewColor}
|
||||
onChange={(e) => setPrimaryColor(e.target.value)}
|
||||
className="h-9 w-14 cursor-pointer rounded-lg border border-border bg-card p-0.5"
|
||||
title="Pick accent color"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
value={primaryColor}
|
||||
onChange={(e) => {
|
||||
const val = e.target.value
|
||||
setPrimaryColor(val)
|
||||
}}
|
||||
placeholder="#06b6d4"
|
||||
maxLength={7}
|
||||
className={cn(
|
||||
'w-32 rounded-lg border border-border bg-card px-3 py-2 text-sm',
|
||||
'text-foreground placeholder:text-muted-foreground font-mono',
|
||||
'focus:border-[rgba(6,182,212,0.3)] focus:outline-none focus:ring-1 focus:ring-primary/20'
|
||||
)}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setPrimaryColor(DEFAULT_COLOR)}
|
||||
className="text-xs text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
Reset to default
|
||||
</button>
|
||||
</div>
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
Hex color code for the primary accent color (e.g. #06b6d4).
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Save Button */}
|
||||
<div className="mt-6 flex items-center gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleSave}
|
||||
disabled={isSaving || !isDirty}
|
||||
className={cn(
|
||||
'inline-flex items-center gap-2 rounded-[10px] px-5 py-2.5 text-sm font-semibold',
|
||||
'bg-gradient-brand text-[#101114] shadow-lg shadow-primary/20',
|
||||
'hover:opacity-90 active:scale-[0.97] transition-all',
|
||||
'disabled:opacity-50 disabled:cursor-not-allowed'
|
||||
)}
|
||||
>
|
||||
{isSaving ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Save className="h-4 w-4" />
|
||||
)}
|
||||
Save Branding
|
||||
</button>
|
||||
{!isDirty && !isSaving && (
|
||||
<span className="text-xs text-muted-foreground">No changes</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Preview Section */}
|
||||
<div className="glass-card-static p-6">
|
||||
<h2 className="text-lg font-semibold text-foreground mb-4">Preview</h2>
|
||||
<p className="text-sm text-muted-foreground mb-4">
|
||||
This is how your accent color will appear on interactive elements.
|
||||
</p>
|
||||
|
||||
<div className="space-y-3">
|
||||
{/* Color swatch preview */}
|
||||
<div className="flex items-center gap-3">
|
||||
<div
|
||||
className="h-8 w-8 rounded-lg"
|
||||
style={{ backgroundColor: previewColor }}
|
||||
/>
|
||||
<div
|
||||
className="h-8 w-8 rounded-lg opacity-20"
|
||||
style={{ backgroundColor: previewColor }}
|
||||
/>
|
||||
<div
|
||||
className="h-8 rounded-lg px-4 flex items-center text-sm font-semibold text-[#101114]"
|
||||
style={{
|
||||
background: `linear-gradient(135deg, ${previewColor} 0%, ${previewColor}cc 100%)`,
|
||||
}}
|
||||
>
|
||||
Button
|
||||
</div>
|
||||
<div
|
||||
className="h-8 rounded-lg px-4 flex items-center text-sm font-medium border"
|
||||
style={{
|
||||
borderColor: `${previewColor}33`,
|
||||
color: previewColor,
|
||||
}}
|
||||
>
|
||||
Badge
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{companyName && (
|
||||
<p className="text-sm text-foreground">
|
||||
Company: <span className="font-medium">{companyName}</span>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default BrandingSettingsPage
|
||||
@@ -76,6 +76,7 @@ const TeamCategoriesPage = lazy(() => import('@/pages/account/TeamCategoriesPage
|
||||
const TargetListsPage = lazy(() => import('@/pages/account/TargetListsPage'))
|
||||
const ChatRetentionSettingsPage = lazy(() => import('@/pages/account/ChatRetentionSettingsPage'))
|
||||
const IntegrationsPage = lazy(() => import('@/pages/account/IntegrationsPage'))
|
||||
const BrandingSettingsPage = lazy(() => import('@/pages/account/BrandingSettingsPage'))
|
||||
|
||||
/** Wraps a lazy-loaded page with Suspense + ErrorBoundary */
|
||||
function page(Component: React.LazyExoticComponent<React.ComponentType>) {
|
||||
@@ -252,6 +253,14 @@ export const router = sentryCreateBrowserRouter([
|
||||
</ProtectedRoute>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: 'branding',
|
||||
element: (
|
||||
<ProtectedRoute requiredRole="owner">
|
||||
{page(BrandingSettingsPage)}
|
||||
</ProtectedRoute>
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user