mirror of
https://github.com/documenso/documenso.git
synced 2026-07-25 17:35:05 +10:00
refactor: unify settings layout, navigation and copy
Consolidate the personal, team and organisation settings surfaces behind a single system so every page shares one width, one navigation implementation and one copy standard. - add shared SettingsNav (aria-current active state, exact matching for the team General route, non-interactive section labels) and delete the four divergent nav implementations it replaces - constrain the settings content column to max-w-3xl in all three layouts and strip the per-page max-w-2xl/max-w-xl wrappers so forms and tables render identically - rewrite SettingsHeader subtitles to remove "Here you can" / "On this page" filler, normalize title casing and punctuation, and translate the hardcoded team general header - align the billing header with SettingsHeader typography and drop redundant fieldset width constraints in the document/email preference forms - fix the wrong team group success toast, team email meta title, misnamed page exports, untranslated Manage label and Create Team casing mismatch - update manage-organisation e2e assertions for the corrected copy
This commit is contained in:
@@ -155,7 +155,7 @@ export const TeamCreateDialog = ({ trigger, onCreated, ...props }: TeamCreateDia
|
||||
<DialogTrigger onClick={(e) => e.stopPropagation()} asChild={true}>
|
||||
{trigger ?? (
|
||||
<Button className="flex-shrink-0" variant="secondary">
|
||||
<Trans>Create team</Trans>
|
||||
<Trans>Create Team</Trans>
|
||||
</Button>
|
||||
)}
|
||||
</DialogTrigger>
|
||||
@@ -163,7 +163,7 @@ export const TeamCreateDialog = ({ trigger, onCreated, ...props }: TeamCreateDia
|
||||
<DialogContent position="center">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
<Trans>Create team</Trans>
|
||||
<Trans>Create Team</Trans>
|
||||
</DialogTitle>
|
||||
|
||||
<DialogDescription>
|
||||
|
||||
@@ -79,7 +79,7 @@ export const TeamGroupCreateDialog = ({ ...props }: TeamGroupCreateDialogProps)
|
||||
|
||||
toast({
|
||||
title: t`Success`,
|
||||
description: t`Team members have been added.`,
|
||||
description: t`Team groups have been added.`,
|
||||
duration: 5000,
|
||||
});
|
||||
|
||||
@@ -87,7 +87,7 @@ export const TeamGroupCreateDialog = ({ ...props }: TeamGroupCreateDialogProps)
|
||||
} catch {
|
||||
toast({
|
||||
title: t`An unknown error occurred`,
|
||||
description: t`We encountered an unknown error while attempting to add team members. Please try again later.`,
|
||||
description: t`We encountered an unknown error while attempting to add the groups. Please try again later.`,
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
@@ -134,7 +134,7 @@ export const TeamGroupCreateDialog = ({ ...props }: TeamGroupCreateDialogProps)
|
||||
</DialogTitle>
|
||||
|
||||
<DialogDescription>
|
||||
<Trans>Configure the team roles for each group</Trans>
|
||||
<Trans>Configure the team roles for each group.</Trans>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
))
|
||||
@@ -260,7 +260,7 @@ export const TeamGroupCreateDialog = ({ ...props }: TeamGroupCreateDialogProps)
|
||||
</Button>
|
||||
|
||||
<Button type="submit" loading={form.formState.isSubmitting}>
|
||||
<Trans>Create Groups</Trans>
|
||||
<Trans>Add Groups</Trans>
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</>
|
||||
|
||||
@@ -106,7 +106,7 @@ export const WebhookCreateDialog = ({ trigger, ...props }: WebhookCreateDialogPr
|
||||
<Trans>Create webhook</Trans>
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
<Trans>On this page, you can create a new webhook.</Trans>
|
||||
<Trans>Receive real-time notifications when document events occur.</Trans>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
|
||||
@@ -162,7 +162,7 @@ export const DocumentPreferencesForm = ({
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form onSubmit={handleFormSubmit}>
|
||||
<fieldset className="flex h-full max-w-2xl flex-col gap-y-6" disabled={form.formState.isSubmitting}>
|
||||
<fieldset className="flex h-full flex-col gap-y-6" disabled={form.formState.isSubmitting}>
|
||||
{!isPersonalLayoutMode && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
|
||||
@@ -75,7 +75,7 @@ export const EmailPreferencesForm = ({ settings, onFormSubmit, canInherit }: Ema
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form onSubmit={handleFormSubmit}>
|
||||
<fieldset className="flex h-full max-w-2xl flex-col gap-y-6" disabled={form.formState.isSubmitting}>
|
||||
<fieldset className="flex h-full flex-col gap-y-6" disabled={form.formState.isSubmitting}>
|
||||
{organisation.organisationClaim.flags.emailDomains && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
|
||||
@@ -1,137 +0,0 @@
|
||||
import { useSession } from '@documenso/lib/client-only/providers/session';
|
||||
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
|
||||
import { canExecuteOrganisationAction, isPersonalLayout } from '@documenso/lib/utils/organisations';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { BracesIcon, CreditCardIcon, Globe2Icon, Lock, Settings2Icon, User, Users, WebhookIcon } from 'lucide-react';
|
||||
import type { HTMLAttributes } from 'react';
|
||||
import { Link, useLocation } from 'react-router';
|
||||
|
||||
export type SettingsDesktopNavProps = HTMLAttributes<HTMLDivElement>;
|
||||
|
||||
export const SettingsDesktopNav = ({ className, ...props }: SettingsDesktopNavProps) => {
|
||||
const { pathname } = useLocation();
|
||||
|
||||
const { organisations } = useSession();
|
||||
|
||||
const isPersonalLayoutMode = isPersonalLayout(organisations);
|
||||
|
||||
const hasManageableBillingOrgs = organisations.some((org) =>
|
||||
canExecuteOrganisationAction('MANAGE_BILLING', org.currentOrganisationRole),
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={cn('flex flex-col gap-y-2', className)} {...props}>
|
||||
<Link to="/settings/profile">
|
||||
<Button
|
||||
variant="ghost"
|
||||
className={cn('w-full justify-start', pathname?.startsWith('/settings/profile') && 'bg-secondary')}
|
||||
>
|
||||
<User className="mr-2 h-5 w-5" />
|
||||
<Trans>Profile</Trans>
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
{isPersonalLayoutMode && (
|
||||
<>
|
||||
<Link to="/settings/document">
|
||||
<Button variant="ghost" className={cn('w-full justify-start')}>
|
||||
<Settings2Icon className="mr-2 h-5 w-5" />
|
||||
<Trans>Preferences</Trans>
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
<Link className="w-full pl-8" to="/settings/document">
|
||||
<Button
|
||||
variant="ghost"
|
||||
className={cn('w-full justify-start', pathname?.startsWith('/settings/document') && 'bg-secondary')}
|
||||
>
|
||||
<Trans>Document</Trans>
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
<Link className="w-full pl-8" to="/settings/branding">
|
||||
<Button
|
||||
variant="ghost"
|
||||
className={cn('w-full justify-start', pathname?.startsWith('/settings/branding') && 'bg-secondary')}
|
||||
>
|
||||
<Trans>Branding</Trans>
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
<Link className="w-full pl-8" to="/settings/email">
|
||||
<Button
|
||||
variant="ghost"
|
||||
className={cn('w-full justify-start', pathname?.startsWith('/settings/email') && 'bg-secondary')}
|
||||
>
|
||||
<Trans>Email</Trans>
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
<Link to="/settings/public-profile">
|
||||
<Button
|
||||
variant="ghost"
|
||||
className={cn('w-full justify-start', pathname?.startsWith('/settings/public-profile') && 'bg-secondary')}
|
||||
>
|
||||
<Globe2Icon className="mr-2 h-5 w-5" />
|
||||
<Trans>Public Profile</Trans>
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
<Link to="/settings/tokens">
|
||||
<Button
|
||||
variant="ghost"
|
||||
className={cn('w-full justify-start', pathname?.startsWith('/settings/tokens') && 'bg-secondary')}
|
||||
>
|
||||
<BracesIcon className="mr-2 h-5 w-5" />
|
||||
<Trans>API Tokens</Trans>
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
<Link to="/settings/webhooks">
|
||||
<Button
|
||||
variant="ghost"
|
||||
className={cn('w-full justify-start', pathname?.startsWith('/settings/webhooks') && 'bg-secondary')}
|
||||
>
|
||||
<WebhookIcon className="mr-2 h-5 w-5" />
|
||||
<Trans>Webhooks</Trans>
|
||||
</Button>
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Link to="/settings/organisations">
|
||||
<Button
|
||||
variant="ghost"
|
||||
className={cn('w-full justify-start', pathname?.startsWith('/settings/organisations') && 'bg-secondary')}
|
||||
>
|
||||
<Users className="mr-2 h-5 w-5" />
|
||||
<Trans>Organisations</Trans>
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
{IS_BILLING_ENABLED() && hasManageableBillingOrgs && (
|
||||
<Link to={isPersonalLayoutMode ? '/settings/billing-personal' : `/settings/billing`}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className={cn('w-full justify-start', pathname?.startsWith('/settings/billing') && 'bg-secondary')}
|
||||
>
|
||||
<CreditCardIcon className="mr-2 h-5 w-5" />
|
||||
<Trans>Billing</Trans>
|
||||
</Button>
|
||||
</Link>
|
||||
)}
|
||||
|
||||
<Link to="/settings/security">
|
||||
<Button
|
||||
variant="ghost"
|
||||
className={cn('w-full justify-start', pathname?.startsWith('/settings/security') && 'bg-secondary')}
|
||||
>
|
||||
<Lock className="mr-2 h-5 w-5" />
|
||||
<Trans>Security</Trans>
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,144 +0,0 @@
|
||||
import { useSession } from '@documenso/lib/client-only/providers/session';
|
||||
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
|
||||
import { canExecuteOrganisationAction, isPersonalLayout } from '@documenso/lib/utils/organisations';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import {
|
||||
BracesIcon,
|
||||
CreditCardIcon,
|
||||
Globe2Icon,
|
||||
Lock,
|
||||
MailIcon,
|
||||
PaletteIcon,
|
||||
Settings2Icon,
|
||||
User,
|
||||
Users,
|
||||
WebhookIcon,
|
||||
} from 'lucide-react';
|
||||
import type { HTMLAttributes } from 'react';
|
||||
import { Link, useLocation } from 'react-router';
|
||||
|
||||
export type SettingsMobileNavProps = HTMLAttributes<HTMLDivElement>;
|
||||
|
||||
export const SettingsMobileNav = ({ className, ...props }: SettingsMobileNavProps) => {
|
||||
const { pathname } = useLocation();
|
||||
|
||||
const { organisations } = useSession();
|
||||
|
||||
const isPersonalLayoutMode = isPersonalLayout(organisations);
|
||||
|
||||
const hasManageableBillingOrgs = organisations.some((org) =>
|
||||
canExecuteOrganisationAction('MANAGE_BILLING', org.currentOrganisationRole),
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={cn('flex flex-wrap items-center justify-start gap-x-2 gap-y-4', className)} {...props}>
|
||||
<Link to="/settings/profile">
|
||||
<Button
|
||||
variant="ghost"
|
||||
className={cn('w-full justify-start', pathname?.startsWith('/settings/profile') && 'bg-secondary')}
|
||||
>
|
||||
<User className="mr-2 h-5 w-5" />
|
||||
<Trans>Profile</Trans>
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
{isPersonalLayoutMode && (
|
||||
<>
|
||||
<Link to="/settings/document">
|
||||
<Button
|
||||
variant="ghost"
|
||||
className={cn('w-full justify-start', pathname?.startsWith('/settings/document') && 'bg-secondary')}
|
||||
>
|
||||
<Settings2Icon className="mr-2 h-5 w-5" />
|
||||
<Trans>Document Preferences</Trans>
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
<Link to="/settings/branding">
|
||||
<Button
|
||||
variant="ghost"
|
||||
className={cn('w-full justify-start', pathname?.startsWith('/settings/branding') && 'bg-secondary')}
|
||||
>
|
||||
<PaletteIcon className="mr-2 h-5 w-5" />
|
||||
<Trans>Branding Preferences</Trans>
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
<Link to="/settings/email">
|
||||
<Button
|
||||
variant="ghost"
|
||||
className={cn('w-full justify-start', pathname?.startsWith('/settings/email') && 'bg-secondary')}
|
||||
>
|
||||
<MailIcon className="mr-2 h-5 w-5" />
|
||||
<Trans>Email Preferences</Trans>
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
<Link to="/settings/public-profile">
|
||||
<Button
|
||||
variant="ghost"
|
||||
className={cn('w-full justify-start', pathname?.startsWith('/settings/public-profile') && 'bg-secondary')}
|
||||
>
|
||||
<Globe2Icon className="mr-2 h-5 w-5" />
|
||||
<Trans>Public Profile</Trans>
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
<Link to="/settings/tokens">
|
||||
<Button
|
||||
variant="ghost"
|
||||
className={cn('w-full justify-start', pathname?.startsWith('/settings/tokens') && 'bg-secondary')}
|
||||
>
|
||||
<BracesIcon className="mr-2 h-5 w-5" />
|
||||
<Trans>API Tokens</Trans>
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
<Link to="/settings/webhooks">
|
||||
<Button
|
||||
variant="ghost"
|
||||
className={cn('w-full justify-start', pathname?.startsWith('/settings/webhooks') && 'bg-secondary')}
|
||||
>
|
||||
<WebhookIcon className="mr-2 h-5 w-5" />
|
||||
<Trans>Webhooks</Trans>
|
||||
</Button>
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Link to="/settings/organisations">
|
||||
<Button
|
||||
variant="ghost"
|
||||
className={cn('w-full justify-start', pathname?.startsWith('/settings/organisations') && 'bg-secondary')}
|
||||
>
|
||||
<Users className="mr-2 h-5 w-5" />
|
||||
<Trans>Organisations</Trans>
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
{IS_BILLING_ENABLED() && hasManageableBillingOrgs && (
|
||||
<Link to={isPersonalLayoutMode ? '/settings/billing-personal' : `/settings/billing`}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className={cn('w-full justify-start', pathname?.startsWith('/settings/billing') && 'bg-secondary')}
|
||||
>
|
||||
<CreditCardIcon className="mr-2 h-5 w-5" />
|
||||
<Trans>Billing</Trans>
|
||||
</Button>
|
||||
</Link>
|
||||
)}
|
||||
|
||||
<Link to="/settings/security">
|
||||
<Button
|
||||
variant="ghost"
|
||||
className={cn('w-full justify-start', pathname?.startsWith('/settings/security') && 'bg-secondary')}
|
||||
>
|
||||
<Lock className="mr-2 h-5 w-5" />
|
||||
<Trans>Security</Trans>
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,62 @@
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import type { LucideIcon } from 'lucide-react';
|
||||
import { NavLink } from 'react-router';
|
||||
|
||||
export type SettingsNavRoute = {
|
||||
path: string;
|
||||
label: string;
|
||||
icon?: LucideIcon;
|
||||
/**
|
||||
* Renders the route as a non-interactive group label, only visible on desktop.
|
||||
*/
|
||||
isSectionLabel?: boolean;
|
||||
/**
|
||||
* Indents the route under the preceding section label on desktop.
|
||||
*/
|
||||
isSubNav?: boolean;
|
||||
/**
|
||||
* Only mark the route as active on an exact path match.
|
||||
*/
|
||||
end?: boolean;
|
||||
};
|
||||
|
||||
export type SettingsNavProps = {
|
||||
routes: SettingsNavRoute[];
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export const SettingsNav = ({ routes, className }: SettingsNavProps) => {
|
||||
return (
|
||||
<nav
|
||||
className={cn(
|
||||
'flex flex-wrap items-center justify-start gap-x-2 gap-y-4 md:w-full md:flex-col md:items-start md:gap-y-2',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{routes.map((route) =>
|
||||
route.isSectionLabel ? (
|
||||
<div
|
||||
key={`${route.path}-${route.label}`}
|
||||
className="flex h-10 w-full items-center px-4 font-medium text-sm max-md:hidden"
|
||||
>
|
||||
{route.icon && <route.icon className="mr-2 h-5 w-5" />}
|
||||
{route.label}
|
||||
</div>
|
||||
) : (
|
||||
<NavLink
|
||||
key={`${route.path}-${route.label}`}
|
||||
to={route.path}
|
||||
end={route.end}
|
||||
className={cn('group justify-start md:w-full', route.isSubNav && 'md:pl-8')}
|
||||
>
|
||||
<Button variant="ghost" className="w-full justify-start group-aria-[current]:bg-secondary">
|
||||
{route.icon && <route.icon className="mr-2 h-5 w-5" />}
|
||||
{route.label}
|
||||
</Button>
|
||||
</NavLink>
|
||||
),
|
||||
)}
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
@@ -102,7 +102,9 @@ export const OrganisationEmailDomainsDataTable = () => {
|
||||
cell: ({ row }) => (
|
||||
<div className="flex justify-end space-x-2">
|
||||
<Button asChild variant="outline">
|
||||
<Link to={`/o/${organisation.url}/settings/email-domains/${row.original.id}`}>Manage</Link>
|
||||
<Link to={`/o/${organisation.url}/settings/email-domains/${row.original.id}`}>
|
||||
<Trans>Manage</Trans>
|
||||
</Link>
|
||||
</Button>
|
||||
|
||||
<OrganisationEmailDomainDeleteDialog
|
||||
|
||||
Reference in New Issue
Block a user