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 { 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 { DOCUMENT_SIGNATURE_TYPES, DocumentSignatureType } from '@documenso/lib/constants/document';
|
||||
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 { useForm } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { DocumentPreferencesResetDialog } from '~/components/dialogs/document-preferences-reset-dialog';
|
||||
import { useOptionalCurrentTeam } from '~/providers/team';
|
||||
|
||||
import { DefaultRecipientsMultiSelectCombobox } from '../general/default-recipients-multiselect-combobox';
|
||||
import { FormStickySaveBar } from './form-sticky-save-bar';
|
||||
import { InheritableField } from './inheritable-field';
|
||||
@@ -64,7 +63,6 @@ type SettingsSubset = Pick<
|
||||
export type DocumentPreferencesFormProps = {
|
||||
settings: SettingsSubset;
|
||||
canInherit: boolean;
|
||||
isAiFeaturesConfigured?: boolean;
|
||||
onFormSubmit: (data: TDocumentPreferencesFormSchema) => Promise<void>;
|
||||
};
|
||||
|
||||
@@ -83,17 +81,14 @@ const getDocumentPreferencesFormValues = (settings: SettingsSubset): TDocumentPr
|
||||
};
|
||||
};
|
||||
|
||||
export const DocumentPreferencesForm = ({
|
||||
settings,
|
||||
onFormSubmit,
|
||||
canInherit,
|
||||
isAiFeaturesConfigured = false,
|
||||
}: DocumentPreferencesFormProps) => {
|
||||
export const DocumentPreferencesForm = ({ settings, onFormSubmit, canInherit }: DocumentPreferencesFormProps) => {
|
||||
const { _ } = useLingui();
|
||||
const { organisations } = useSession();
|
||||
const currentOrganisation = useCurrentOrganisation();
|
||||
const optionalTeam = useOptionalCurrentTeam();
|
||||
|
||||
const isAiFeaturesConfigured = IS_AI_FEATURES_CONFIGURED();
|
||||
|
||||
const isPersonalLayoutMode = isPersonalLayout(organisations);
|
||||
|
||||
const ZDocumentPreferencesFormSchema = z.object({
|
||||
|
||||
@@ -56,7 +56,7 @@ export const OrganisationUpdateForm = () => {
|
||||
await refreshSession();
|
||||
|
||||
if (url !== organisation.url) {
|
||||
await navigate(`/o/${url}/settings`);
|
||||
await navigate(`/o/${url}/settings/general`);
|
||||
}
|
||||
|
||||
toast({
|
||||
|
||||
@@ -65,7 +65,7 @@ export const TeamUpdateForm = ({ teamId, teamName, teamUrl }: UpdateTeamDialogPr
|
||||
});
|
||||
|
||||
if (url !== teamUrl) {
|
||||
await navigate(`/t/${url}/settings`);
|
||||
await navigate(`/t/${url}/settings/general`);
|
||||
}
|
||||
} catch (err) {
|
||||
const error = AppError.parseError(err);
|
||||
|
||||
@@ -287,7 +287,7 @@ export const AppCommandMenu = ({ open, onOpenChange }: AppCommandMenuProps) => {
|
||||
{
|
||||
id: 'settings-main',
|
||||
label: msg`Settings`,
|
||||
path: '/settings',
|
||||
path: '/settings/profile',
|
||||
icon: SettingsIcon,
|
||||
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 goToSettings = useCallback(() => push('/settings'), [push]);
|
||||
const goToSettings = useCallback(() => push('/settings/profile'), [push]);
|
||||
const goToDocuments = useCallback(() => {
|
||||
if (teamUrl) {
|
||||
push(`/t/${teamUrl}/documents?status=ALL`);
|
||||
|
||||
@@ -168,7 +168,7 @@ export const OrgMenuSwitcher = () => {
|
||||
{canExecuteOrganisationAction('MANAGE_ORGANISATION', org.currentOrganisationRole) && (
|
||||
<div className="absolute top-0 right-0 bottom-0 flex items-center justify-center">
|
||||
<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"
|
||||
>
|
||||
<Settings2Icon className="h-3.5 w-3.5" />
|
||||
@@ -221,7 +221,7 @@ export const OrgMenuSwitcher = () => {
|
||||
{canExecuteTeamAction('MANAGE_TEAM', team.currentTeamRole) && (
|
||||
<div className="absolute top-0 right-0 bottom-0 flex items-center justify-center">
|
||||
<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"
|
||||
>
|
||||
<Settings2Icon className="h-3.5 w-3.5" />
|
||||
@@ -275,10 +275,10 @@ export const OrgMenuSwitcher = () => {
|
||||
<Link
|
||||
to={
|
||||
canAccessOrganisationSettings
|
||||
? `/o/${currentOrganisation?.url}/settings`
|
||||
? `/o/${currentOrganisation?.url}/settings/general`
|
||||
: canAccessTeamSettings
|
||||
? `/t/${currentTeam?.url}/settings`
|
||||
: '/settings'
|
||||
? `/t/${currentTeam?.url}/settings/general`
|
||||
: '/settings/profile'
|
||||
}
|
||||
>
|
||||
<Trans>Settings</Trans>
|
||||
|
||||
@@ -157,7 +157,7 @@ export const UnifiedSettingsLayout = ({ activeScope, preferredTeamUrl = null }:
|
||||
}}
|
||||
primaryButton={
|
||||
<Button asChild>
|
||||
<Link to="/settings">
|
||||
<Link to="/settings/profile">
|
||||
<Trans>Go to your settings</Trans>
|
||||
</Link>
|
||||
</Button>
|
||||
|
||||
@@ -76,7 +76,7 @@ export const OrganisationTeamsTable = () => {
|
||||
cell: ({ row }) => (
|
||||
<div className="flex justify-end space-x-2">
|
||||
<Button variant="outline" asChild>
|
||||
<Link to={`/t/${row.original.url}/settings`}>
|
||||
<Link to={`/t/${row.original.url}/settings/general`}>
|
||||
<Trans>Manage</Trans>
|
||||
</Link>
|
||||
</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 { ORGANISATION_MEMBER_ROLE_MAP } from '@documenso/lib/constants/organisations-translations';
|
||||
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 { AvatarWithText } from '@documenso/ui/primitives/avatar';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
@@ -29,8 +29,6 @@ export const UserOrganisationsTable = () => {
|
||||
})),
|
||||
});
|
||||
|
||||
const isPersonalLayoutMode = isPersonalLayout(data);
|
||||
|
||||
const results = {
|
||||
data: data || [],
|
||||
perPage: 10,
|
||||
@@ -74,7 +72,7 @@ export const UserOrganisationsTable = () => {
|
||||
<div className="flex justify-end space-x-2">
|
||||
{canExecuteOrganisationAction('MANAGE_ORGANISATION', row.original.currentOrganisationRole) && (
|
||||
<Button variant="outline" asChild>
|
||||
<Link to={`/o/${row.original.url}/settings`}>
|
||||
<Link to={`/o/${row.original.url}/settings/general`}>
|
||||
<Trans>Manage</Trans>
|
||||
</Link>
|
||||
</Button>
|
||||
@@ -99,7 +97,7 @@ export const UserOrganisationsTable = () => {
|
||||
),
|
||||
},
|
||||
] satisfies DataTableColumnDef<(typeof results)['data'][number]>[];
|
||||
}, [isPersonalLayoutMode]);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -112,9 +110,6 @@ export const UserOrganisationsTable = () => {
|
||||
error={{
|
||||
enable: isLoadingError,
|
||||
}}
|
||||
columnVisibility={{
|
||||
actions: !isPersonalLayoutMode,
|
||||
}}
|
||||
skeleton={{
|
||||
enable: isLoading,
|
||||
rows: 3,
|
||||
@@ -136,14 +131,12 @@ export const UserOrganisationsTable = () => {
|
||||
<TableCell>
|
||||
<Skeleton className="h-4 w-20 rounded-full" />
|
||||
</TableCell>
|
||||
{!isPersonalLayoutMode && (
|
||||
<TableCell>
|
||||
<div className="flex flex-row justify-end space-x-2">
|
||||
<Skeleton className="h-10 w-20 rounded" />
|
||||
<Skeleton className="h-10 w-16 rounded" />
|
||||
</div>
|
||||
</TableCell>
|
||||
)}
|
||||
</>
|
||||
),
|
||||
}}
|
||||
|
||||
@@ -143,7 +143,7 @@ export default function DashboardPage() {
|
||||
|
||||
{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">
|
||||
<Link to={`/o/${org.url}/settings`}>
|
||||
<Link to={`/o/${org.url}/settings/general`}>
|
||||
<SettingsIcon className="h-4 w-4" />
|
||||
</Link>
|
||||
</div>
|
||||
@@ -212,7 +212,7 @@ export default function DashboardPage() {
|
||||
|
||||
{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">
|
||||
<Link to={`/t/${team.url}/settings`}>
|
||||
<Link to={`/t/${team.url}/settings/general`}>
|
||||
<SettingsIcon className="h-4 w-4" />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
@@ -115,7 +115,7 @@ export default function OrganisationSettingsTeamsPage() {
|
||||
</div>
|
||||
|
||||
<Button asChild>
|
||||
<Link to={`/o/${organisation.url}/settings`}>
|
||||
<Link to={`/o/${organisation.url}/settings/general`}>
|
||||
<Trans>Manage Organisation</Trans>
|
||||
</Link>
|
||||
</Button>
|
||||
@@ -184,7 +184,7 @@ const TeamDropdownMenu = ({ team }: { team: TGetOrganisationSessionResponse[0]['
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild>
|
||||
<Link to={`/t/${team.url}/settings`}>
|
||||
<Link to={`/t/${team.url}/settings/general`}>
|
||||
<SettingsIcon className="mr-2 h-4 w-4" />
|
||||
<Trans>Settings</Trans>
|
||||
</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 { extractCookieFromDocument } from '@documenso/lib/client-only/cookies';
|
||||
import type { RouteHandle } from '@documenso/lib/client-only/hooks/use-child-route-flags';
|
||||
import { PREFERRED_TEAM_URL_COOKIE } from '@documenso/lib/constants/cookies';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
@@ -16,12 +17,29 @@ export const handle: RouteHandle = {
|
||||
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) {
|
||||
return {
|
||||
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) {
|
||||
return <UnifiedSettingsLayout activeScope="organisation" preferredTeamUrl={loaderData.preferredTeamUrl} />;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
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 { 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 { trpc } from '@documenso/trpc/react';
|
||||
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
|
||||
@@ -26,16 +25,12 @@ export function meta() {
|
||||
}
|
||||
|
||||
export default function OrganisationSettingsBrandingPage() {
|
||||
const { organisations } = useSession();
|
||||
|
||||
const organisation = useCurrentOrganisation();
|
||||
const team = useOptionalCurrentTeam();
|
||||
|
||||
const { t } = useLingui();
|
||||
const { toast } = useToast();
|
||||
|
||||
const isPersonalLayoutMode = isPersonalLayout(organisations);
|
||||
|
||||
const [cssWarnings, setCssWarnings] = useState<SanitizeBrandingCssWarning[]>([]);
|
||||
|
||||
const {
|
||||
@@ -122,9 +117,7 @@ export default function OrganisationSettingsBrandingPage() {
|
||||
|
||||
const settingsHeaderText = t`Branding Preferences`;
|
||||
|
||||
const settingsHeaderSubtitle = isPersonalLayoutMode
|
||||
? t`Here you can set your general branding preferences.`
|
||||
: team
|
||||
const settingsHeaderSubtitle = team
|
||||
? 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.`;
|
||||
|
||||
@@ -181,7 +174,7 @@ export default function OrganisationSettingsBrandingPage() {
|
||||
|
||||
{canExecuteOrganisationAction('MANAGE_BILLING', organisation.currentOrganisationRole) && (
|
||||
<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>
|
||||
</Link>
|
||||
</Button>
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
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 { isPersonalLayout } from '@documenso/lib/utils/organisations';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { Loader } from 'lucide-react';
|
||||
import { useLoaderData } from 'react-router';
|
||||
|
||||
import {
|
||||
DocumentPreferencesForm,
|
||||
@@ -21,23 +17,12 @@ export function meta() {
|
||||
return appMetaTags(msg`Document Preferences`);
|
||||
}
|
||||
|
||||
export const loader = () => {
|
||||
return {
|
||||
isAiFeaturesConfigured: IS_AI_FEATURES_CONFIGURED(),
|
||||
};
|
||||
};
|
||||
|
||||
export default function OrganisationSettingsDocumentPage() {
|
||||
const { isAiFeaturesConfigured } = useLoaderData<typeof loader>();
|
||||
|
||||
const { organisations } = useSession();
|
||||
const organisation = useCurrentOrganisation();
|
||||
|
||||
const { t } = useLingui();
|
||||
const { toast } = useToast();
|
||||
|
||||
const isPersonalLayoutMode = isPersonalLayout(organisations);
|
||||
|
||||
const { data: organisationWithSettings, isLoading: isLoadingOrganisation } = trpc.organisation.get.useQuery({
|
||||
organisationReference: organisation.url,
|
||||
});
|
||||
@@ -106,9 +91,7 @@ export default function OrganisationSettingsDocumentPage() {
|
||||
}
|
||||
|
||||
const settingsHeaderText = t`Document Preferences`;
|
||||
const settingsHeaderSubtitle = isPersonalLayoutMode
|
||||
? 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.`;
|
||||
const settingsHeaderSubtitle = t`Here you can set document preferences for your organisation. Teams will inherit these settings by default.`;
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -117,7 +100,6 @@ export default function OrganisationSettingsDocumentPage() {
|
||||
<section>
|
||||
<DocumentPreferencesForm
|
||||
canInherit={false}
|
||||
isAiFeaturesConfigured={isAiFeaturesConfigured}
|
||||
settings={organisationWithSettings.organisationGlobalSettings}
|
||||
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 { extractCookieFromDocument } from '@documenso/lib/client-only/cookies';
|
||||
import type { RouteHandle } from '@documenso/lib/client-only/hooks/use-child-route-flags';
|
||||
import { PREFERRED_TEAM_URL_COOKIE } from '@documenso/lib/constants/cookies';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
@@ -16,12 +17,29 @@ export const handle: RouteHandle = {
|
||||
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) {
|
||||
return {
|
||||
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) {
|
||||
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 { trpc } from '@documenso/trpc/react';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { Loader } from 'lucide-react';
|
||||
import { useLoaderData } from 'react-router';
|
||||
|
||||
import {
|
||||
DocumentPreferencesForm,
|
||||
@@ -13,15 +11,7 @@ import {
|
||||
import { SettingsHeader } from '~/components/general/settings-header';
|
||||
import { useCurrentTeam } from '~/providers/team';
|
||||
|
||||
export const loader = () => {
|
||||
return {
|
||||
isAiFeaturesConfigured: IS_AI_FEATURES_CONFIGURED(),
|
||||
};
|
||||
};
|
||||
|
||||
export default function TeamsSettingsPage() {
|
||||
const { isAiFeaturesConfigured } = useLoaderData<typeof loader>();
|
||||
|
||||
const team = useCurrentTeam();
|
||||
|
||||
const { t } = useLingui();
|
||||
@@ -103,7 +93,6 @@ export default function TeamsSettingsPage() {
|
||||
<section>
|
||||
<DocumentPreferencesForm
|
||||
canInherit={true}
|
||||
isAiFeaturesConfigured={isAiFeaturesConfigured}
|
||||
settings={teamWithSettings.teamSettings}
|
||||
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.
|
||||
const redirectPath = await handleRedirects(c);
|
||||
const redirectPath = handleRedirects(c);
|
||||
|
||||
if (redirectPath) {
|
||||
debug.log('Redirecting from', path);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { Context } from 'hono';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/require-await
|
||||
export const handleRedirects = async (c: Context): Promise<string | null> => {
|
||||
export const handleRedirects = (c: Context): string | null => {
|
||||
const { req } = c;
|
||||
const path = req.path;
|
||||
|
||||
@@ -15,5 +14,23 @@ export const handleRedirects = async (c: Context): Promise<string | null> => {
|
||||
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;
|
||||
};
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
/**
|
||||
* 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 => {
|
||||
const cookieHeader = headers.get('cookie') || '';
|
||||
const cookiePairs = cookieHeader.split(';');
|
||||
const cookie = cookiePairs.find((pair) => pair.trim().startsWith(cookieName));
|
||||
const cookie = cookiePairs.find((pair) => pair.trim().startsWith(`${cookieName}=`));
|
||||
|
||||
if (!cookie) {
|
||||
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 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
|
||||
|
||||
@@ -56,4 +56,8 @@ export const createPublicEnv = () => ({
|
||||
// Derived from the private transport so the client can detect CSC mode for
|
||||
// authoring UI gating without exposing the raw transport value.
|
||||
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