From d6cf3fec4bcbb1bac8608b64c4fcc71659cd68d4 Mon Sep 17 00:00:00 2001 From: David Nguyen Date: Sun, 9 Aug 2026 16:00:55 +1000 Subject: [PATCH 1/9] feat: unify settings (#3128) --- .../document-preferences-reset-dialog.tsx | 19 - .../dialogs/team-email-delete-dialog.tsx | 35 +- .../dialogs/team-email-update-dialog.tsx | 7 +- .../forms/branding-preferences-form.tsx | 57 +- .../forms/certificate-preferences-form.tsx | 169 ++++++ .../forms/document-preferences-form.tsx | 395 +++---------- .../forms/email-preferences-form.tsx | 117 +++- .../components/forms/form-sticky-save-bar.tsx | 26 +- .../components/forms/inheritable-field.tsx | 51 ++ .../forms/reminder-preferences-form.tsx | 134 +++++ .../app/components/general/app-header.tsx | 26 +- .../components/general/app-nav-desktop.tsx | 5 +- .../app/components/general/app-nav-mobile.tsx | 5 +- .../app/components/general/billing-plans.tsx | 1 - .../app/components/general/menu-switcher.tsx | 104 ---- .../components/general/org-menu-switcher.tsx | 48 +- .../organisation-billing-portal-button.tsx | 8 +- .../components/general/settings-header.tsx | 6 +- .../general/settings-nav-desktop.tsx | 137 ----- .../general/settings-nav-mobile.tsx | 144 ----- .../general/settings-org-switcher.tsx | 200 +++++++ .../general/settings-scope-breadcrumb.tsx | 58 ++ .../general/settings-team-switcher.tsx | 166 ++++++ .../general/teams/team-email-dropdown.tsx | 94 --- .../general/unified-settings-layout.tsx | 211 +++++++ .../unified-settings-sidebar-mobile.tsx | 165 ++++++ .../general/unified-settings-sidebar.tsx | 275 +++++++++ .../tables/user-organisations-table.tsx | 24 +- .../app/routes/_authenticated+/_layout.tsx | 30 +- .../routes/_authenticated+/admin+/claims.tsx | 2 +- .../admin+/email-transports._index.tsx | 2 +- .../admin+/organisations.$id.tsx | 10 +- .../_authenticated+/o.$orgUrl._layout.tsx | 12 +- .../o.$orgUrl.settings._layout.tsx | 180 +----- .../o.$orgUrl.settings.branding.tsx | 2 +- .../o.$orgUrl.settings.certificates.tsx | 80 +++ .../o.$orgUrl.settings.document.tsx | 15 +- .../o.$orgUrl.settings.email-domains.$id.tsx | 2 +- ....$orgUrl.settings.email-domains._index.tsx | 14 +- .../o.$orgUrl.settings.email.tsx | 5 +- .../o.$orgUrl.settings.general.tsx | 28 +- .../o.$orgUrl.settings.groups.$id.tsx | 6 +- .../o.$orgUrl.settings.groups._index.tsx | 1 + .../o.$orgUrl.settings.members.tsx | 6 +- .../o.$orgUrl.settings.reminders.tsx | 76 +++ .../o.$orgUrl.settings.sso.tsx | 3 +- .../o.$orgUrl.settings.teams.tsx | 2 +- .../_dynamic_personal_routes+/_layout.tsx | 54 -- .../billing-personal.tsx | 5 - .../_dynamic_personal_routes+/branding.tsx | 5 - .../_dynamic_personal_routes+/document.tsx | 5 - .../_dynamic_personal_routes+/email.tsx | 5 - .../public-profile.tsx | 5 - .../_dynamic_personal_routes+/tokens.tsx | 5 - .../webhooks.$id._index.tsx | 5 - .../webhooks._index.tsx | 5 - .../_authenticated+/settings+/_layout.tsx | 35 +- .../_authenticated+/settings+/billing.tsx | 1 + .../settings+/organisations.tsx | 1 + .../_authenticated+/settings+/profile.tsx | 2 - .../settings+/security._index.tsx | 8 +- .../settings+/security.activity.tsx | 2 +- .../settings+/security.passkeys.tsx | 2 +- .../_authenticated+/t.$teamUrl+/_layout.tsx | 6 +- .../t.$teamUrl+/documents.$id.logs.tsx | 9 +- .../t.$teamUrl+/settings._index.tsx | 163 +----- .../t.$teamUrl+/settings._layout.tsx | 135 +---- .../t.$teamUrl+/settings.branding.tsx | 2 +- .../t.$teamUrl+/settings.certificates.tsx | 76 +++ .../t.$teamUrl+/settings.document.tsx | 18 +- .../t.$teamUrl+/settings.email.tsx | 11 +- .../t.$teamUrl+/settings.general.tsx | 281 +++++++++ .../t.$teamUrl+/settings.groups.tsx | 2 +- .../t.$teamUrl+/settings.members.tsx | 2 +- .../t.$teamUrl+/settings.public-profile.tsx | 5 +- .../t.$teamUrl+/settings.reminders.tsx | 76 +++ .../t.$teamUrl+/settings.tokens.tsx | 1 + .../settings.webhooks.$id._index.tsx | 1 + .../t.$teamUrl+/settings.webhooks._index.tsx | 1 + apps/remix/app/routes/_index.tsx | 3 +- apps/remix/app/routes/_profile+/p.$url.tsx | 19 +- .../organisation.sso.confirmation.$token.tsx | 2 +- .../team.verify.email.$token.tsx | 205 ++++--- apps/remix/app/routes/api+/preferred-team.tsx | 21 + apps/remix/server/middleware.ts | 3 +- .../update-organisation-member-role.spec.ts | 6 +- .../envelope-expiration-settings.spec.ts | 20 +- .../envelope-expiration-signing.spec.ts | 83 ++- .../include-document-certificate.spec.ts | 38 +- .../organisation-team-preferences.spec.ts | 24 +- .../public-profiles/public-profiles.spec.ts | 31 + .../settings/preferred-team-cookie.spec.ts | 82 +++ .../e2e/settings/unified-settings.spec.ts | 554 ++++++++++++++++++ .../app-tests/e2e/teams/manage-team.spec.ts | 3 +- .../app-tests/e2e/teams/team-email.spec.ts | 35 +- .../e2e/teams/team-settings-save-bar.spec.ts | 10 +- .../webhooks/webhook-secret-access.spec.ts | 74 +++ .../hooks/use-child-route-flags.ts | 55 ++ packages/lib/constants/cookies.ts | 1 + .../team/get-team-email-by-email.ts | 37 -- packages/lib/server-only/team/get-teams.ts | 4 +- packages/lib/server-only/user/verify-email.ts | 12 +- .../webhooks/get-webhooks-by-team-id.ts | 34 +- packages/lib/utils/settings-nav.ts | 298 ++++++++++ packages/lib/utils/settings-switcher.ts | 57 ++ packages/lib/vitest.config.ts | 3 + .../trpc/server/document-router/find-inbox.ts | 16 +- .../enterprise-router/create-subscription.ts | 6 +- .../create-subscription.types.ts | 1 - .../enterprise-router/manage-subscription.ts | 6 +- .../manage-subscription.types.ts | 1 - .../envelope-router/sign-envelope-field.ts | 7 + .../update-organisation-settings.ts | 11 +- .../complete-team-email-verification.ts | 87 +++ .../complete-team-email-verification.types.ts | 11 + packages/trpc/server/team-router/router.ts | 30 +- .../team-router/update-team-settings.ts | 11 +- packages/ui/primitives/avatar.tsx | 2 +- .../ui/primitives/document-upload-button.tsx | 8 +- packages/ui/styles/theme.css | 32 + 120 files changed, 4181 insertions(+), 1859 deletions(-) create mode 100644 apps/remix/app/components/forms/certificate-preferences-form.tsx create mode 100644 apps/remix/app/components/forms/inheritable-field.tsx create mode 100644 apps/remix/app/components/forms/reminder-preferences-form.tsx delete mode 100644 apps/remix/app/components/general/menu-switcher.tsx delete mode 100644 apps/remix/app/components/general/settings-nav-desktop.tsx delete mode 100644 apps/remix/app/components/general/settings-nav-mobile.tsx create mode 100644 apps/remix/app/components/general/settings-org-switcher.tsx create mode 100644 apps/remix/app/components/general/settings-scope-breadcrumb.tsx create mode 100644 apps/remix/app/components/general/settings-team-switcher.tsx delete mode 100644 apps/remix/app/components/general/teams/team-email-dropdown.tsx create mode 100644 apps/remix/app/components/general/unified-settings-layout.tsx create mode 100644 apps/remix/app/components/general/unified-settings-sidebar-mobile.tsx create mode 100644 apps/remix/app/components/general/unified-settings-sidebar.tsx create mode 100644 apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.certificates.tsx create mode 100644 apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.reminders.tsx delete mode 100644 apps/remix/app/routes/_authenticated+/settings+/_dynamic_personal_routes+/_layout.tsx delete mode 100644 apps/remix/app/routes/_authenticated+/settings+/_dynamic_personal_routes+/billing-personal.tsx delete mode 100644 apps/remix/app/routes/_authenticated+/settings+/_dynamic_personal_routes+/branding.tsx delete mode 100644 apps/remix/app/routes/_authenticated+/settings+/_dynamic_personal_routes+/document.tsx delete mode 100644 apps/remix/app/routes/_authenticated+/settings+/_dynamic_personal_routes+/email.tsx delete mode 100644 apps/remix/app/routes/_authenticated+/settings+/_dynamic_personal_routes+/public-profile.tsx delete mode 100644 apps/remix/app/routes/_authenticated+/settings+/_dynamic_personal_routes+/tokens.tsx delete mode 100644 apps/remix/app/routes/_authenticated+/settings+/_dynamic_personal_routes+/webhooks.$id._index.tsx delete mode 100644 apps/remix/app/routes/_authenticated+/settings+/_dynamic_personal_routes+/webhooks._index.tsx create mode 100644 apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.certificates.tsx create mode 100644 apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.general.tsx create mode 100644 apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.reminders.tsx create mode 100644 apps/remix/app/routes/api+/preferred-team.tsx create mode 100644 packages/app-tests/e2e/settings/preferred-team-cookie.spec.ts create mode 100644 packages/app-tests/e2e/settings/unified-settings.spec.ts create mode 100644 packages/app-tests/e2e/webhooks/webhook-secret-access.spec.ts create mode 100644 packages/lib/client-only/hooks/use-child-route-flags.ts create mode 100644 packages/lib/constants/cookies.ts delete mode 100644 packages/lib/server-only/team/get-team-email-by-email.ts create mode 100644 packages/lib/utils/settings-nav.ts create mode 100644 packages/lib/utils/settings-switcher.ts create mode 100644 packages/trpc/server/team-router/complete-team-email-verification.ts create mode 100644 packages/trpc/server/team-router/complete-team-email-verification.types.ts diff --git a/apps/remix/app/components/dialogs/document-preferences-reset-dialog.tsx b/apps/remix/app/components/dialogs/document-preferences-reset-dialog.tsx index 9adebb36e..ed7184ef7 100644 --- a/apps/remix/app/components/dialogs/document-preferences-reset-dialog.tsx +++ b/apps/remix/app/components/dialogs/document-preferences-reset-dialog.tsx @@ -18,7 +18,6 @@ export type DocumentPreferencesResetDialogProps = { onReset: () => Promise; showAiFeatures?: boolean; showDocumentVisibility?: boolean; - showIncludeSenderDetails?: boolean; }; export const DocumentPreferencesResetDialog = ({ @@ -26,7 +25,6 @@ export const DocumentPreferencesResetDialog = ({ onReset, showAiFeatures = false, showDocumentVisibility = false, - showIncludeSenderDetails = false, }: DocumentPreferencesResetDialogProps) => { const [open, setOpen] = useState(false); const [isResetting, setIsResetting] = useState(false); @@ -92,29 +90,12 @@ export const DocumentPreferencesResetDialog = ({
  • Default signature settings
  • - {showIncludeSenderDetails && ( -
  • - Send on behalf of team -
  • - )} -
  • - Include the signing certificate in the document -
  • -
  • - Include the audit logs in the document -
  • Default recipients
  • Delegate document ownership
  • -
  • - Default envelope expiration -
  • -
  • - Default signing reminders -
  • {showAiFeatures && (
  • AI features diff --git a/apps/remix/app/components/dialogs/team-email-delete-dialog.tsx b/apps/remix/app/components/dialogs/team-email-delete-dialog.tsx index 7c08cf7d3..147cf3b40 100644 --- a/apps/remix/app/components/dialogs/team-email-delete-dialog.tsx +++ b/apps/remix/app/components/dialogs/team-email-delete-dialog.tsx @@ -17,28 +17,25 @@ import { useToast } from '@documenso/ui/primitives/use-toast'; import { msg } from '@lingui/core/macro'; import { useLingui } from '@lingui/react'; import { Trans } from '@lingui/react/macro'; -import type { Prisma } from '@prisma/client'; +import type { Team, TeamEmail, TeamEmailVerification } from '@prisma/client'; import { useState } from 'react'; import { useRevalidator } from 'react-router'; export type TeamEmailDeleteDialogProps = { trigger?: React.ReactNode; teamName: string; - team: Prisma.TeamGetPayload<{ - include: { - teamEmail: true; - emailVerification: { - select: { - expiresAt: true; - name: true; - email: true; - }; - }; - }; - }>; + team: Pick; + teamEmail: Pick | null; + emailVerification: Pick | null; }; -export const TeamEmailDeleteDialog = ({ trigger, teamName, team }: TeamEmailDeleteDialogProps) => { +export const TeamEmailDeleteDialog = ({ + trigger, + teamName, + team, + teamEmail, + emailVerification, +}: TeamEmailDeleteDialogProps) => { const [open, setOpen] = useState(false); const { _ } = useLingui(); @@ -83,11 +80,11 @@ export const TeamEmailDeleteDialog = ({ trigger, teamName, team }: TeamEmailDele }); const onRemove = async () => { - if (team.teamEmail) { + if (teamEmail) { await deleteTeamEmail({ teamId: team.id }); } - if (team.emailVerification) { + if (emailVerification) { await deleteTeamEmailVerification({ teamId: team.id }); } @@ -121,13 +118,13 @@ export const TeamEmailDeleteDialog = ({ trigger, teamName, team }: TeamEmailDele - {team.teamEmail?.name || team.emailVerification?.name} + {teamEmail?.name || emailVerification?.name} } - secondaryText={{team.teamEmail?.email || team.emailVerification?.email}} + secondaryText={{teamEmail?.email || emailVerification?.email}} /> diff --git a/apps/remix/app/components/dialogs/team-email-update-dialog.tsx b/apps/remix/app/components/dialogs/team-email-update-dialog.tsx index 3fbddc3c2..449d5ec36 100644 --- a/apps/remix/app/components/dialogs/team-email-update-dialog.tsx +++ b/apps/remix/app/components/dialogs/team-email-update-dialog.tsx @@ -23,7 +23,8 @@ import { useRevalidator } from 'react-router'; import type { z } from 'zod'; export type TeamEmailUpdateDialogProps = { - teamEmail: TeamEmail; + teamId: number; + teamEmail: Pick; trigger?: React.ReactNode; } & Omit; @@ -33,7 +34,7 @@ const ZUpdateTeamEmailFormSchema = ZUpdateTeamEmailMutationSchema.pick({ type TUpdateTeamEmailFormSchema = z.infer; -export const TeamEmailUpdateDialog = ({ teamEmail, trigger, ...props }: TeamEmailUpdateDialogProps) => { +export const TeamEmailUpdateDialog = ({ teamId, teamEmail, trigger, ...props }: TeamEmailUpdateDialogProps) => { const [open, setOpen] = useState(false); const { t } = useLingui(); @@ -53,7 +54,7 @@ export const TeamEmailUpdateDialog = ({ teamEmail, trigger, ...props }: TeamEmai const onFormSubmit = async ({ name }: TUpdateTeamEmailFormSchema) => { try { await updateTeamEmail({ - teamId: teamEmail.teamId, + teamId, data: { name, }, diff --git a/apps/remix/app/components/forms/branding-preferences-form.tsx b/apps/remix/app/components/forms/branding-preferences-form.tsx index e556ff6cf..1cb5be35d 100644 --- a/apps/remix/app/components/forms/branding-preferences-form.tsx +++ b/apps/remix/app/components/forms/branding-preferences-form.tsx @@ -29,6 +29,7 @@ import { useOptionalCurrentTeam } from '~/providers/team'; import { useCspNonce } from '~/utils/nonce'; import { FormStickySaveBar } from './form-sticky-save-bar'; +import { InheritableField } from './inheritable-field'; const ZBrandingPreferencesFormSchema = z.object({ brandingEnabled: z.boolean().nullable(), @@ -210,11 +211,13 @@ export function BrandingPreferencesForm({ control={form.control} name="brandingEnabled" render={({ field }) => ( - - - Enable Custom Branding - - + Enable Custom Branding} + testId="branding-enabled" + > @@ -372,7 +379,7 @@ export function BrandingPreferencesForm({ )} - + )} /> @@ -380,11 +387,13 @@ export function BrandingPreferencesForm({ control={form.control} name="brandingCompanyDetails" render={({ field }) => ( - - - Brand Details - - + Brand Details} + testId="branding-company-details" + >