mirror of
https://github.com/documenso/documenso.git
synced 2026-08-15 02:53:32 +10:00
fix: improve unified settings (#3160)
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation';
|
import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation';
|
||||||
import { useSession } from '@documenso/lib/client-only/providers/session';
|
import { useSession } from '@documenso/lib/client-only/providers/session';
|
||||||
|
import { IS_AI_FEATURES_CONFIGURED } from '@documenso/lib/constants/app';
|
||||||
import { DATE_FORMATS } from '@documenso/lib/constants/date-formats';
|
import { DATE_FORMATS } from '@documenso/lib/constants/date-formats';
|
||||||
import { DOCUMENT_SIGNATURE_TYPES, DocumentSignatureType } from '@documenso/lib/constants/document';
|
import { DOCUMENT_SIGNATURE_TYPES, DocumentSignatureType } from '@documenso/lib/constants/document';
|
||||||
import { isValidLanguageCode, SUPPORTED_LANGUAGE_CODES, SUPPORTED_LANGUAGES } from '@documenso/lib/constants/i18n';
|
import { isValidLanguageCode, SUPPORTED_LANGUAGE_CODES, SUPPORTED_LANGUAGES } from '@documenso/lib/constants/i18n';
|
||||||
@@ -24,10 +25,8 @@ import { Trans } from '@lingui/react/macro';
|
|||||||
import { DocumentVisibility, type RecipientRole, type TeamGlobalSettings } from '@prisma/client';
|
import { DocumentVisibility, type RecipientRole, type TeamGlobalSettings } from '@prisma/client';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { DocumentPreferencesResetDialog } from '~/components/dialogs/document-preferences-reset-dialog';
|
import { DocumentPreferencesResetDialog } from '~/components/dialogs/document-preferences-reset-dialog';
|
||||||
import { useOptionalCurrentTeam } from '~/providers/team';
|
import { useOptionalCurrentTeam } from '~/providers/team';
|
||||||
|
|
||||||
import { DefaultRecipientsMultiSelectCombobox } from '../general/default-recipients-multiselect-combobox';
|
import { DefaultRecipientsMultiSelectCombobox } from '../general/default-recipients-multiselect-combobox';
|
||||||
import { FormStickySaveBar } from './form-sticky-save-bar';
|
import { FormStickySaveBar } from './form-sticky-save-bar';
|
||||||
import { InheritableField } from './inheritable-field';
|
import { InheritableField } from './inheritable-field';
|
||||||
@@ -64,7 +63,6 @@ type SettingsSubset = Pick<
|
|||||||
export type DocumentPreferencesFormProps = {
|
export type DocumentPreferencesFormProps = {
|
||||||
settings: SettingsSubset;
|
settings: SettingsSubset;
|
||||||
canInherit: boolean;
|
canInherit: boolean;
|
||||||
isAiFeaturesConfigured?: boolean;
|
|
||||||
onFormSubmit: (data: TDocumentPreferencesFormSchema) => Promise<void>;
|
onFormSubmit: (data: TDocumentPreferencesFormSchema) => Promise<void>;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -83,17 +81,14 @@ const getDocumentPreferencesFormValues = (settings: SettingsSubset): TDocumentPr
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DocumentPreferencesForm = ({
|
export const DocumentPreferencesForm = ({ settings, onFormSubmit, canInherit }: DocumentPreferencesFormProps) => {
|
||||||
settings,
|
|
||||||
onFormSubmit,
|
|
||||||
canInherit,
|
|
||||||
isAiFeaturesConfigured = false,
|
|
||||||
}: DocumentPreferencesFormProps) => {
|
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
const { organisations } = useSession();
|
const { organisations } = useSession();
|
||||||
const currentOrganisation = useCurrentOrganisation();
|
const currentOrganisation = useCurrentOrganisation();
|
||||||
const optionalTeam = useOptionalCurrentTeam();
|
const optionalTeam = useOptionalCurrentTeam();
|
||||||
|
|
||||||
|
const isAiFeaturesConfigured = IS_AI_FEATURES_CONFIGURED();
|
||||||
|
|
||||||
const isPersonalLayoutMode = isPersonalLayout(organisations);
|
const isPersonalLayoutMode = isPersonalLayout(organisations);
|
||||||
|
|
||||||
const ZDocumentPreferencesFormSchema = z.object({
|
const ZDocumentPreferencesFormSchema = z.object({
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ export const OrganisationUpdateForm = () => {
|
|||||||
await refreshSession();
|
await refreshSession();
|
||||||
|
|
||||||
if (url !== organisation.url) {
|
if (url !== organisation.url) {
|
||||||
await navigate(`/o/${url}/settings`);
|
await navigate(`/o/${url}/settings/general`);
|
||||||
}
|
}
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ export const TeamUpdateForm = ({ teamId, teamName, teamUrl }: UpdateTeamDialogPr
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (url !== teamUrl) {
|
if (url !== teamUrl) {
|
||||||
await navigate(`/t/${url}/settings`);
|
await navigate(`/t/${url}/settings/general`);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const error = AppError.parseError(err);
|
const error = AppError.parseError(err);
|
||||||
|
|||||||
@@ -287,7 +287,7 @@ export const AppCommandMenu = ({ open, onOpenChange }: AppCommandMenuProps) => {
|
|||||||
{
|
{
|
||||||
id: 'settings-main',
|
id: 'settings-main',
|
||||||
label: msg`Settings`,
|
label: msg`Settings`,
|
||||||
path: '/settings',
|
path: '/settings/profile',
|
||||||
icon: SettingsIcon,
|
icon: SettingsIcon,
|
||||||
shortcut: SETTINGS_PAGE_SHORTCUT.replace('+', ''),
|
shortcut: SETTINGS_PAGE_SHORTCUT.replace('+', ''),
|
||||||
},
|
},
|
||||||
@@ -389,7 +389,7 @@ export const AppCommandMenu = ({ open, onOpenChange }: AppCommandMenuProps) => {
|
|||||||
|
|
||||||
const formatChipCount = (count: number, isCapped: boolean) => (isCapped ? `≥${count}` : `${count}`);
|
const formatChipCount = (count: number, isCapped: boolean) => (isCapped ? `≥${count}` : `${count}`);
|
||||||
|
|
||||||
const goToSettings = useCallback(() => push('/settings'), [push]);
|
const goToSettings = useCallback(() => push('/settings/profile'), [push]);
|
||||||
const goToDocuments = useCallback(() => {
|
const goToDocuments = useCallback(() => {
|
||||||
if (teamUrl) {
|
if (teamUrl) {
|
||||||
push(`/t/${teamUrl}/documents?status=ALL`);
|
push(`/t/${teamUrl}/documents?status=ALL`);
|
||||||
|
|||||||
@@ -168,7 +168,7 @@ export const OrgMenuSwitcher = () => {
|
|||||||
{canExecuteOrganisationAction('MANAGE_ORGANISATION', org.currentOrganisationRole) && (
|
{canExecuteOrganisationAction('MANAGE_ORGANISATION', org.currentOrganisationRole) && (
|
||||||
<div className="absolute top-0 right-0 bottom-0 flex items-center justify-center">
|
<div className="absolute top-0 right-0 bottom-0 flex items-center justify-center">
|
||||||
<Link
|
<Link
|
||||||
to={`/o/${org.url}/settings`}
|
to={`/o/${org.url}/settings/general`}
|
||||||
className="mr-2 rounded-sm border p-1 text-muted-foreground transition-opacity duration-200 group-hover:opacity-100 md:opacity-0"
|
className="mr-2 rounded-sm border p-1 text-muted-foreground transition-opacity duration-200 group-hover:opacity-100 md:opacity-0"
|
||||||
>
|
>
|
||||||
<Settings2Icon className="h-3.5 w-3.5" />
|
<Settings2Icon className="h-3.5 w-3.5" />
|
||||||
@@ -221,7 +221,7 @@ export const OrgMenuSwitcher = () => {
|
|||||||
{canExecuteTeamAction('MANAGE_TEAM', team.currentTeamRole) && (
|
{canExecuteTeamAction('MANAGE_TEAM', team.currentTeamRole) && (
|
||||||
<div className="absolute top-0 right-0 bottom-0 flex items-center justify-center">
|
<div className="absolute top-0 right-0 bottom-0 flex items-center justify-center">
|
||||||
<Link
|
<Link
|
||||||
to={`/t/${team.url}/settings`}
|
to={`/t/${team.url}/settings/general`}
|
||||||
className="mr-2 rounded-sm border p-1 text-muted-foreground opacity-0 transition-opacity duration-200 group-hover:opacity-100"
|
className="mr-2 rounded-sm border p-1 text-muted-foreground opacity-0 transition-opacity duration-200 group-hover:opacity-100"
|
||||||
>
|
>
|
||||||
<Settings2Icon className="h-3.5 w-3.5" />
|
<Settings2Icon className="h-3.5 w-3.5" />
|
||||||
@@ -275,10 +275,10 @@ export const OrgMenuSwitcher = () => {
|
|||||||
<Link
|
<Link
|
||||||
to={
|
to={
|
||||||
canAccessOrganisationSettings
|
canAccessOrganisationSettings
|
||||||
? `/o/${currentOrganisation?.url}/settings`
|
? `/o/${currentOrganisation?.url}/settings/general`
|
||||||
: canAccessTeamSettings
|
: canAccessTeamSettings
|
||||||
? `/t/${currentTeam?.url}/settings`
|
? `/t/${currentTeam?.url}/settings/general`
|
||||||
: '/settings'
|
: '/settings/profile'
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Trans>Settings</Trans>
|
<Trans>Settings</Trans>
|
||||||
|
|||||||
@@ -157,7 +157,7 @@ export const UnifiedSettingsLayout = ({ activeScope, preferredTeamUrl = null }:
|
|||||||
}}
|
}}
|
||||||
primaryButton={
|
primaryButton={
|
||||||
<Button asChild>
|
<Button asChild>
|
||||||
<Link to="/settings">
|
<Link to="/settings/profile">
|
||||||
<Trans>Go to your settings</Trans>
|
<Trans>Go to your settings</Trans>
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ export const OrganisationTeamsTable = () => {
|
|||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<div className="flex justify-end space-x-2">
|
<div className="flex justify-end space-x-2">
|
||||||
<Button variant="outline" asChild>
|
<Button variant="outline" asChild>
|
||||||
<Link to={`/t/${row.original.url}/settings`}>
|
<Link to={`/t/${row.original.url}/settings/general`}>
|
||||||
<Trans>Manage</Trans>
|
<Trans>Manage</Trans>
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { useSession } from '@documenso/lib/client-only/providers/session';
|
|||||||
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
||||||
import { ORGANISATION_MEMBER_ROLE_MAP } from '@documenso/lib/constants/organisations-translations';
|
import { ORGANISATION_MEMBER_ROLE_MAP } from '@documenso/lib/constants/organisations-translations';
|
||||||
import { formatAvatarUrl } from '@documenso/lib/utils/avatars';
|
import { formatAvatarUrl } from '@documenso/lib/utils/avatars';
|
||||||
import { canExecuteOrganisationAction, isPersonalLayout } from '@documenso/lib/utils/organisations';
|
import { canExecuteOrganisationAction } from '@documenso/lib/utils/organisations';
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import { AvatarWithText } from '@documenso/ui/primitives/avatar';
|
import { AvatarWithText } from '@documenso/ui/primitives/avatar';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
@@ -29,8 +29,6 @@ export const UserOrganisationsTable = () => {
|
|||||||
})),
|
})),
|
||||||
});
|
});
|
||||||
|
|
||||||
const isPersonalLayoutMode = isPersonalLayout(data);
|
|
||||||
|
|
||||||
const results = {
|
const results = {
|
||||||
data: data || [],
|
data: data || [],
|
||||||
perPage: 10,
|
perPage: 10,
|
||||||
@@ -74,7 +72,7 @@ export const UserOrganisationsTable = () => {
|
|||||||
<div className="flex justify-end space-x-2">
|
<div className="flex justify-end space-x-2">
|
||||||
{canExecuteOrganisationAction('MANAGE_ORGANISATION', row.original.currentOrganisationRole) && (
|
{canExecuteOrganisationAction('MANAGE_ORGANISATION', row.original.currentOrganisationRole) && (
|
||||||
<Button variant="outline" asChild>
|
<Button variant="outline" asChild>
|
||||||
<Link to={`/o/${row.original.url}/settings`}>
|
<Link to={`/o/${row.original.url}/settings/general`}>
|
||||||
<Trans>Manage</Trans>
|
<Trans>Manage</Trans>
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
@@ -99,7 +97,7 @@ export const UserOrganisationsTable = () => {
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
] satisfies DataTableColumnDef<(typeof results)['data'][number]>[];
|
] satisfies DataTableColumnDef<(typeof results)['data'][number]>[];
|
||||||
}, [isPersonalLayoutMode]);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@@ -112,9 +110,6 @@ export const UserOrganisationsTable = () => {
|
|||||||
error={{
|
error={{
|
||||||
enable: isLoadingError,
|
enable: isLoadingError,
|
||||||
}}
|
}}
|
||||||
columnVisibility={{
|
|
||||||
actions: !isPersonalLayoutMode,
|
|
||||||
}}
|
|
||||||
skeleton={{
|
skeleton={{
|
||||||
enable: isLoading,
|
enable: isLoading,
|
||||||
rows: 3,
|
rows: 3,
|
||||||
@@ -136,14 +131,12 @@ export const UserOrganisationsTable = () => {
|
|||||||
<TableCell>
|
<TableCell>
|
||||||
<Skeleton className="h-4 w-20 rounded-full" />
|
<Skeleton className="h-4 w-20 rounded-full" />
|
||||||
</TableCell>
|
</TableCell>
|
||||||
{!isPersonalLayoutMode && (
|
<TableCell>
|
||||||
<TableCell>
|
<div className="flex flex-row justify-end space-x-2">
|
||||||
<div className="flex flex-row justify-end space-x-2">
|
<Skeleton className="h-10 w-20 rounded" />
|
||||||
<Skeleton className="h-10 w-20 rounded" />
|
<Skeleton className="h-10 w-16 rounded" />
|
||||||
<Skeleton className="h-10 w-16 rounded" />
|
</div>
|
||||||
</div>
|
</TableCell>
|
||||||
</TableCell>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -143,7 +143,7 @@ export default function DashboardPage() {
|
|||||||
|
|
||||||
{canExecuteOrganisationAction('MANAGE_ORGANISATION', org.currentOrganisationRole) && (
|
{canExecuteOrganisationAction('MANAGE_ORGANISATION', org.currentOrganisationRole) && (
|
||||||
<div className="absolute top-4 right-4 text-muted-foreground opacity-0 transition-opacity duration-200 group-hover:opacity-100">
|
<div className="absolute top-4 right-4 text-muted-foreground opacity-0 transition-opacity duration-200 group-hover:opacity-100">
|
||||||
<Link to={`/o/${org.url}/settings`}>
|
<Link to={`/o/${org.url}/settings/general`}>
|
||||||
<SettingsIcon className="h-4 w-4" />
|
<SettingsIcon className="h-4 w-4" />
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
@@ -212,7 +212,7 @@ export default function DashboardPage() {
|
|||||||
|
|
||||||
{canExecuteTeamAction('MANAGE_TEAM', team.currentTeamRole) && (
|
{canExecuteTeamAction('MANAGE_TEAM', team.currentTeamRole) && (
|
||||||
<div className="absolute top-4 right-4 text-muted-foreground opacity-0 transition-opacity duration-200 group-hover:opacity-100">
|
<div className="absolute top-4 right-4 text-muted-foreground opacity-0 transition-opacity duration-200 group-hover:opacity-100">
|
||||||
<Link to={`/t/${team.url}/settings`}>
|
<Link to={`/t/${team.url}/settings/general`}>
|
||||||
<SettingsIcon className="h-4 w-4" />
|
<SettingsIcon className="h-4 w-4" />
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ export default function OrganisationSettingsTeamsPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button asChild>
|
<Button asChild>
|
||||||
<Link to={`/o/${organisation.url}/settings`}>
|
<Link to={`/o/${organisation.url}/settings/general`}>
|
||||||
<Trans>Manage Organisation</Trans>
|
<Trans>Manage Organisation</Trans>
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
@@ -184,7 +184,7 @@ const TeamDropdownMenu = ({ team }: { team: TGetOrganisationSessionResponse[0]['
|
|||||||
</Link>
|
</Link>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem asChild>
|
<DropdownMenuItem asChild>
|
||||||
<Link to={`/t/${team.url}/settings`}>
|
<Link to={`/t/${team.url}/settings/general`}>
|
||||||
<SettingsIcon className="mr-2 h-4 w-4" />
|
<SettingsIcon className="mr-2 h-4 w-4" />
|
||||||
<Trans>Settings</Trans>
|
<Trans>Settings</Trans>
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
import { redirect } from 'react-router';
|
|
||||||
|
|
||||||
import type { Route } from './+types/_layout';
|
|
||||||
|
|
||||||
export function loader({ params }: Route.LoaderArgs) {
|
|
||||||
if (params.orgUrl) {
|
|
||||||
throw redirect(`/o/${params.orgUrl}/settings/general`);
|
|
||||||
}
|
|
||||||
|
|
||||||
throw redirect('/');
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { extractCookieFromHeaders } from '@documenso/auth/server/lib/utils/cookies';
|
import { extractCookieFromHeaders } from '@documenso/auth/server/lib/utils/cookies';
|
||||||
|
import { extractCookieFromDocument } from '@documenso/lib/client-only/cookies';
|
||||||
import type { RouteHandle } from '@documenso/lib/client-only/hooks/use-child-route-flags';
|
import type { RouteHandle } from '@documenso/lib/client-only/hooks/use-child-route-flags';
|
||||||
import { PREFERRED_TEAM_URL_COOKIE } from '@documenso/lib/constants/cookies';
|
import { PREFERRED_TEAM_URL_COOKIE } from '@documenso/lib/constants/cookies';
|
||||||
import { msg } from '@lingui/core/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
@@ -16,12 +17,29 @@ export const handle: RouteHandle = {
|
|||||||
layoutMode: 'settings',
|
layoutMode: 'settings',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Only runs on the initial document request (SSR) so the first paint has the
|
||||||
|
* correct preferred team without a hydration mismatch.
|
||||||
|
*/
|
||||||
export function loader({ request }: Route.LoaderArgs) {
|
export function loader({ request }: Route.LoaderArgs) {
|
||||||
return {
|
return {
|
||||||
preferredTeamUrl: extractCookieFromHeaders(PREFERRED_TEAM_URL_COOKIE, request.headers),
|
preferredTeamUrl: extractCookieFromHeaders(PREFERRED_TEAM_URL_COOKIE, request.headers),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs instead of the server loader on client-side navigations, otherwise every
|
||||||
|
* settings page switch would trigger a `.data` round-trip to the server just to
|
||||||
|
* read this cookie.
|
||||||
|
*
|
||||||
|
* The cookie is not `HttpOnly` so it can be read straight from the document.
|
||||||
|
*/
|
||||||
|
export function clientLoader() {
|
||||||
|
return {
|
||||||
|
preferredTeamUrl: extractCookieFromDocument(PREFERRED_TEAM_URL_COOKIE),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export default function OrganisationSettingsLayout({ loaderData }: Route.ComponentProps) {
|
export default function OrganisationSettingsLayout({ loaderData }: Route.ComponentProps) {
|
||||||
return <UnifiedSettingsLayout activeScope="organisation" preferredTeamUrl={loaderData.preferredTeamUrl} />;
|
return <UnifiedSettingsLayout activeScope="organisation" preferredTeamUrl={loaderData.preferredTeamUrl} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation';
|
import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation';
|
||||||
import { useSession } from '@documenso/lib/client-only/providers/session';
|
|
||||||
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
|
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
|
||||||
import { canExecuteOrganisationAction, isPersonalLayout } from '@documenso/lib/utils/organisations';
|
import { canExecuteOrganisationAction } from '@documenso/lib/utils/organisations';
|
||||||
import type { SanitizeBrandingCssWarning } from '@documenso/lib/utils/sanitize-branding-css';
|
import type { SanitizeBrandingCssWarning } from '@documenso/lib/utils/sanitize-branding-css';
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
|
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
|
||||||
@@ -26,16 +25,12 @@ export function meta() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function OrganisationSettingsBrandingPage() {
|
export default function OrganisationSettingsBrandingPage() {
|
||||||
const { organisations } = useSession();
|
|
||||||
|
|
||||||
const organisation = useCurrentOrganisation();
|
const organisation = useCurrentOrganisation();
|
||||||
const team = useOptionalCurrentTeam();
|
const team = useOptionalCurrentTeam();
|
||||||
|
|
||||||
const { t } = useLingui();
|
const { t } = useLingui();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
const isPersonalLayoutMode = isPersonalLayout(organisations);
|
|
||||||
|
|
||||||
const [cssWarnings, setCssWarnings] = useState<SanitizeBrandingCssWarning[]>([]);
|
const [cssWarnings, setCssWarnings] = useState<SanitizeBrandingCssWarning[]>([]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -122,11 +117,9 @@ export default function OrganisationSettingsBrandingPage() {
|
|||||||
|
|
||||||
const settingsHeaderText = t`Branding Preferences`;
|
const settingsHeaderText = t`Branding Preferences`;
|
||||||
|
|
||||||
const settingsHeaderSubtitle = isPersonalLayoutMode
|
const settingsHeaderSubtitle = team
|
||||||
? t`Here you can set your general branding preferences.`
|
? t`Here you can set branding preferences for your team.`
|
||||||
: team
|
: t`Here you can set branding preferences for your organisation. Teams will inherit these settings by default.`;
|
||||||
? t`Here you can set branding preferences for your team.`
|
|
||||||
: t`Here you can set branding preferences for your organisation. Teams will inherit these settings by default.`;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@@ -181,7 +174,7 @@ export default function OrganisationSettingsBrandingPage() {
|
|||||||
|
|
||||||
{canExecuteOrganisationAction('MANAGE_BILLING', organisation.currentOrganisationRole) && (
|
{canExecuteOrganisationAction('MANAGE_BILLING', organisation.currentOrganisationRole) && (
|
||||||
<Button asChild variant="outline">
|
<Button asChild variant="outline">
|
||||||
<Link to={isPersonalLayoutMode ? '/settings/billing' : `/o/${organisation.url}/settings/billing`}>
|
<Link to={`/o/${organisation.url}/settings/billing`}>
|
||||||
<Trans>Update Billing</Trans>
|
<Trans>Update Billing</Trans>
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,14 +1,10 @@
|
|||||||
import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation';
|
import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation';
|
||||||
import { useSession } from '@documenso/lib/client-only/providers/session';
|
|
||||||
import { IS_AI_FEATURES_CONFIGURED } from '@documenso/lib/constants/app';
|
|
||||||
import { DocumentSignatureType } from '@documenso/lib/constants/document';
|
import { DocumentSignatureType } from '@documenso/lib/constants/document';
|
||||||
import { isPersonalLayout } from '@documenso/lib/utils/organisations';
|
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
import { msg } from '@lingui/core/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react/macro';
|
import { useLingui } from '@lingui/react/macro';
|
||||||
import { Loader } from 'lucide-react';
|
import { Loader } from 'lucide-react';
|
||||||
import { useLoaderData } from 'react-router';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DocumentPreferencesForm,
|
DocumentPreferencesForm,
|
||||||
@@ -21,23 +17,12 @@ export function meta() {
|
|||||||
return appMetaTags(msg`Document Preferences`);
|
return appMetaTags(msg`Document Preferences`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const loader = () => {
|
|
||||||
return {
|
|
||||||
isAiFeaturesConfigured: IS_AI_FEATURES_CONFIGURED(),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function OrganisationSettingsDocumentPage() {
|
export default function OrganisationSettingsDocumentPage() {
|
||||||
const { isAiFeaturesConfigured } = useLoaderData<typeof loader>();
|
|
||||||
|
|
||||||
const { organisations } = useSession();
|
|
||||||
const organisation = useCurrentOrganisation();
|
const organisation = useCurrentOrganisation();
|
||||||
|
|
||||||
const { t } = useLingui();
|
const { t } = useLingui();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
const isPersonalLayoutMode = isPersonalLayout(organisations);
|
|
||||||
|
|
||||||
const { data: organisationWithSettings, isLoading: isLoadingOrganisation } = trpc.organisation.get.useQuery({
|
const { data: organisationWithSettings, isLoading: isLoadingOrganisation } = trpc.organisation.get.useQuery({
|
||||||
organisationReference: organisation.url,
|
organisationReference: organisation.url,
|
||||||
});
|
});
|
||||||
@@ -106,9 +91,7 @@ export default function OrganisationSettingsDocumentPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const settingsHeaderText = t`Document Preferences`;
|
const settingsHeaderText = t`Document Preferences`;
|
||||||
const settingsHeaderSubtitle = isPersonalLayoutMode
|
const settingsHeaderSubtitle = t`Here you can set document preferences for your organisation. Teams will inherit these settings by default.`;
|
||||||
? t`Here you can set your general document preferences.`
|
|
||||||
: t`Here you can set document preferences for your organisation. Teams will inherit these settings by default.`;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@@ -117,7 +100,6 @@ export default function OrganisationSettingsDocumentPage() {
|
|||||||
<section>
|
<section>
|
||||||
<DocumentPreferencesForm
|
<DocumentPreferencesForm
|
||||||
canInherit={false}
|
canInherit={false}
|
||||||
isAiFeaturesConfigured={isAiFeaturesConfigured}
|
|
||||||
settings={organisationWithSettings.organisationGlobalSettings}
|
settings={organisationWithSettings.organisationGlobalSettings}
|
||||||
onFormSubmit={onDocumentPreferencesFormSubmit}
|
onFormSubmit={onDocumentPreferencesFormSubmit}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
import { redirect } from 'react-router';
|
|
||||||
|
|
||||||
export function loader() {
|
|
||||||
throw redirect('/settings/profile');
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { extractCookieFromHeaders } from '@documenso/auth/server/lib/utils/cookies';
|
import { extractCookieFromHeaders } from '@documenso/auth/server/lib/utils/cookies';
|
||||||
|
import { extractCookieFromDocument } from '@documenso/lib/client-only/cookies';
|
||||||
import type { RouteHandle } from '@documenso/lib/client-only/hooks/use-child-route-flags';
|
import type { RouteHandle } from '@documenso/lib/client-only/hooks/use-child-route-flags';
|
||||||
import { PREFERRED_TEAM_URL_COOKIE } from '@documenso/lib/constants/cookies';
|
import { PREFERRED_TEAM_URL_COOKIE } from '@documenso/lib/constants/cookies';
|
||||||
import { msg } from '@lingui/core/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
@@ -16,12 +17,29 @@ export const handle: RouteHandle = {
|
|||||||
layoutMode: 'settings',
|
layoutMode: 'settings',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Only runs on the initial document request (SSR) so the first paint has the
|
||||||
|
* correct preferred team without a hydration mismatch.
|
||||||
|
*/
|
||||||
export function loader({ request }: Route.LoaderArgs) {
|
export function loader({ request }: Route.LoaderArgs) {
|
||||||
return {
|
return {
|
||||||
preferredTeamUrl: extractCookieFromHeaders(PREFERRED_TEAM_URL_COOKIE, request.headers),
|
preferredTeamUrl: extractCookieFromHeaders(PREFERRED_TEAM_URL_COOKIE, request.headers),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs instead of the server loader on client-side navigations, otherwise every
|
||||||
|
* settings page switch would trigger a `.data` round-trip to the server just to
|
||||||
|
* read this cookie.
|
||||||
|
*
|
||||||
|
* The cookie is not `HttpOnly` so it can be read straight from the document.
|
||||||
|
*/
|
||||||
|
export function clientLoader() {
|
||||||
|
return {
|
||||||
|
preferredTeamUrl: extractCookieFromDocument(PREFERRED_TEAM_URL_COOKIE),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export default function SettingsLayout({ loaderData }: Route.ComponentProps) {
|
export default function SettingsLayout({ loaderData }: Route.ComponentProps) {
|
||||||
return <UnifiedSettingsLayout activeScope="account" preferredTeamUrl={loaderData.preferredTeamUrl} />;
|
return <UnifiedSettingsLayout activeScope="account" preferredTeamUrl={loaderData.preferredTeamUrl} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
import { redirect } from 'react-router';
|
|
||||||
|
|
||||||
import type { Route } from './+types/settings._index';
|
|
||||||
|
|
||||||
export function loader({ params }: Route.LoaderArgs) {
|
|
||||||
if (params.teamUrl) {
|
|
||||||
throw redirect(`/t/${params.teamUrl}/settings/general`);
|
|
||||||
}
|
|
||||||
|
|
||||||
throw redirect('/');
|
|
||||||
}
|
|
||||||
@@ -1,10 +1,8 @@
|
|||||||
import { IS_AI_FEATURES_CONFIGURED } from '@documenso/lib/constants/app';
|
|
||||||
import { DocumentSignatureType } from '@documenso/lib/constants/document';
|
import { DocumentSignatureType } from '@documenso/lib/constants/document';
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
import { useLingui } from '@lingui/react/macro';
|
import { useLingui } from '@lingui/react/macro';
|
||||||
import { Loader } from 'lucide-react';
|
import { Loader } from 'lucide-react';
|
||||||
import { useLoaderData } from 'react-router';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DocumentPreferencesForm,
|
DocumentPreferencesForm,
|
||||||
@@ -13,15 +11,7 @@ import {
|
|||||||
import { SettingsHeader } from '~/components/general/settings-header';
|
import { SettingsHeader } from '~/components/general/settings-header';
|
||||||
import { useCurrentTeam } from '~/providers/team';
|
import { useCurrentTeam } from '~/providers/team';
|
||||||
|
|
||||||
export const loader = () => {
|
|
||||||
return {
|
|
||||||
isAiFeaturesConfigured: IS_AI_FEATURES_CONFIGURED(),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function TeamsSettingsPage() {
|
export default function TeamsSettingsPage() {
|
||||||
const { isAiFeaturesConfigured } = useLoaderData<typeof loader>();
|
|
||||||
|
|
||||||
const team = useCurrentTeam();
|
const team = useCurrentTeam();
|
||||||
|
|
||||||
const { t } = useLingui();
|
const { t } = useLingui();
|
||||||
@@ -103,7 +93,6 @@ export default function TeamsSettingsPage() {
|
|||||||
<section>
|
<section>
|
||||||
<DocumentPreferencesForm
|
<DocumentPreferencesForm
|
||||||
canInherit={true}
|
canInherit={true}
|
||||||
isAiFeaturesConfigured={isAiFeaturesConfigured}
|
|
||||||
settings={teamWithSettings.teamSettings}
|
settings={teamWithSettings.teamSettings}
|
||||||
onFormSubmit={onDocumentPreferencesSubmit}
|
onFormSubmit={onDocumentPreferencesSubmit}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export const appMiddleware = async (c: Context, next: Next) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// PRE-HANDLER CODE: Place code here to execute BEFORE the route handler runs.
|
// PRE-HANDLER CODE: Place code here to execute BEFORE the route handler runs.
|
||||||
const redirectPath = await handleRedirects(c);
|
const redirectPath = handleRedirects(c);
|
||||||
|
|
||||||
if (redirectPath) {
|
if (redirectPath) {
|
||||||
debug.log('Redirecting from', path);
|
debug.log('Redirecting from', path);
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import type { Context } from 'hono';
|
import type { Context } from 'hono';
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/require-await
|
export const handleRedirects = (c: Context): string | null => {
|
||||||
export const handleRedirects = async (c: Context): Promise<string | null> => {
|
|
||||||
const { req } = c;
|
const { req } = c;
|
||||||
const path = req.path;
|
const path = req.path;
|
||||||
|
|
||||||
@@ -15,5 +14,23 @@ export const handleRedirects = async (c: Context): Promise<string | null> => {
|
|||||||
return '/';
|
return '/';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The settings paths below have no index routes, land on their first page instead.
|
||||||
|
// In-app links point directly at the subpages, these only catch direct visits.
|
||||||
|
if (path === '/settings' || path === '/settings/') {
|
||||||
|
return '/settings/profile';
|
||||||
|
}
|
||||||
|
|
||||||
|
const orgSettingsMatch = path.match(/^\/o\/([^/]+)\/settings\/?$/);
|
||||||
|
|
||||||
|
if (orgSettingsMatch) {
|
||||||
|
return `/o/${orgSettingsMatch[1]}/settings/general`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const teamSettingsMatch = path.match(/^\/t\/([^/]+)\/settings\/?$/);
|
||||||
|
|
||||||
|
if (teamSettingsMatch) {
|
||||||
|
return `/t/${teamSettingsMatch[1]}/settings/general`;
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
/**
|
/**
|
||||||
* Todo: Use library for cookies instead.
|
* Todo: Use library for cookies instead.
|
||||||
|
*
|
||||||
|
* Server-side counterpart of `extractCookieFromDocument`, keep their matching
|
||||||
|
* semantics in step.
|
||||||
*/
|
*/
|
||||||
export const extractCookieFromHeaders = (cookieName: string, headers: Headers): string | null => {
|
export const extractCookieFromHeaders = (cookieName: string, headers: Headers): string | null => {
|
||||||
const cookieHeader = headers.get('cookie') || '';
|
const cookieHeader = headers.get('cookie') || '';
|
||||||
const cookiePairs = cookieHeader.split(';');
|
const cookiePairs = cookieHeader.split(';');
|
||||||
const cookie = cookiePairs.find((pair) => pair.trim().startsWith(cookieName));
|
const cookie = cookiePairs.find((pair) => pair.trim().startsWith(`${cookieName}=`));
|
||||||
|
|
||||||
if (!cookie) {
|
if (!cookie) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
/**
|
||||||
|
* Read a cookie value from `document.cookie`.
|
||||||
|
*
|
||||||
|
* Client-side counterpart of `extractCookieFromHeaders`. Only works for cookies that
|
||||||
|
* are not `HttpOnly`, such as the preferred team URL cookie.
|
||||||
|
*/
|
||||||
|
export const extractCookieFromDocument = (cookieName: string): string | null => {
|
||||||
|
const cookiePairs = document.cookie.split(';');
|
||||||
|
|
||||||
|
const cookie = cookiePairs.find((pair) => pair.trim().startsWith(`${cookieName}=`));
|
||||||
|
|
||||||
|
if (!cookie) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return cookie.split('=')[1].trim();
|
||||||
|
};
|
||||||
@@ -24,7 +24,20 @@ export const SUPPORT_EMAIL = env('NEXT_PUBLIC_SUPPORT_EMAIL') ?? 'support@docume
|
|||||||
|
|
||||||
export const USE_INTERNAL_URL_BROWSERLESS = () => env('NEXT_PUBLIC_USE_INTERNAL_URL_BROWSERLESS') === 'true';
|
export const USE_INTERNAL_URL_BROWSERLESS = () => env('NEXT_PUBLIC_USE_INTERNAL_URL_BROWSERLESS') === 'true';
|
||||||
|
|
||||||
export const IS_AI_FEATURES_CONFIGURED = () => !!env('GOOGLE_VERTEX_PROJECT_ID') && !!env('GOOGLE_VERTEX_API_KEY');
|
/**
|
||||||
|
* Returns whether AI features are configured for this instance.
|
||||||
|
*
|
||||||
|
* Platform-aware:
|
||||||
|
* - On the server, checks the private Vertex credentials are configured.
|
||||||
|
* - On the client, reads the derived public flag injected via `window.__ENV__`.
|
||||||
|
*/
|
||||||
|
export const IS_AI_FEATURES_CONFIGURED = (): boolean => {
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
return !!env('GOOGLE_VERTEX_PROJECT_ID') && !!env('GOOGLE_VERTEX_API_KEY');
|
||||||
|
}
|
||||||
|
|
||||||
|
return env('NEXT_PUBLIC_AI_FEATURES_ENABLED') === 'true';
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Temporary flag to toggle between Playwright-based and Konva-based PDF generation
|
* Temporary flag to toggle between Playwright-based and Konva-based PDF generation
|
||||||
|
|||||||
@@ -56,4 +56,8 @@ export const createPublicEnv = () => ({
|
|||||||
// Derived from the private transport so the client can detect CSC mode for
|
// Derived from the private transport so the client can detect CSC mode for
|
||||||
// authoring UI gating without exposing the raw transport value.
|
// authoring UI gating without exposing the raw transport value.
|
||||||
NEXT_PUBLIC_SIGNING_TRANSPORT_IS_CSC: process.env.NEXT_PRIVATE_SIGNING_TRANSPORT === 'csc' ? 'true' : 'false',
|
NEXT_PUBLIC_SIGNING_TRANSPORT_IS_CSC: process.env.NEXT_PRIVATE_SIGNING_TRANSPORT === 'csc' ? 'true' : 'false',
|
||||||
|
// Derived from the private Vertex credentials so the client can gate AI
|
||||||
|
// feature UI on a boolean.
|
||||||
|
NEXT_PUBLIC_AI_FEATURES_ENABLED:
|
||||||
|
process.env.GOOGLE_VERTEX_PROJECT_ID && process.env.GOOGLE_VERTEX_API_KEY ? 'true' : 'false',
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user