diff --git a/apps/remix/app/components/general/admin-global-settings-section.tsx b/apps/remix/app/components/general/admin-global-settings-section.tsx index 5f21e7900..760684077 100644 --- a/apps/remix/app/components/general/admin-global-settings-section.tsx +++ b/apps/remix/app/components/general/admin-global-settings-section.tsx @@ -5,6 +5,7 @@ import { msg } from '@lingui/core/macro'; import { useLingui } from '@lingui/react'; import { Trans } from '@lingui/react/macro'; import type { OrganisationGlobalSettings, TeamGlobalSettings } from '@prisma/client'; +import type { ReactNode } from 'react'; import { DetailsCard, DetailsValue } from '~/components/general/admin-details'; @@ -25,38 +26,72 @@ const emailSettingsKeys = Object.keys(EMAIL_SETTINGS_LABELS) as (keyof TDocument type AdminGlobalSettingsSectionProps = { settings: TeamGlobalSettings | OrganisationGlobalSettings | null; isTeam?: boolean; + /** When viewing a team, the parent organisation settings the team inherits from. */ + inheritedSettings?: OrganisationGlobalSettings | null; }; -export const AdminGlobalSettingsSection = ({ settings, isTeam = false }: AdminGlobalSettingsSectionProps) => { +export const AdminGlobalSettingsSection = ({ + settings, + isTeam = false, + inheritedSettings, +}: AdminGlobalSettingsSectionProps) => { const { _ } = useLingui(); - const notSetLabel = isTeam ? Inherited : Not set; if (!settings) { return null; } - const textValue = (value: string | null | undefined) => { - if (value === null || value === undefined) { - return notSetLabel; + const notSet = Not set; + + const inheritedValue = (value: ReactNode) => { + if (!isTeam || value === null) { + return notSet; } - return value; + return ( + + + Inherited: + + {value} + + ); }; - const brandingTextValue = (value: string | null | undefined) => { - if (value === null || value === undefined || value.trim() === '') { - return notSetLabel; + const textValue = (value: string | null | undefined, inherited?: string | null) => { + if (value && value.trim() !== '') { + return value; } - return value; + if (inherited && inherited.trim() !== '') { + return inheritedValue(inherited); + } + + return notSet; }; - const booleanValue = (value: boolean | null | undefined) => { - if (value === null || value === undefined) { - return notSetLabel; + const booleanLabel = (value: boolean) => (value ? Enabled : Disabled); + + const booleanValue = (value: boolean | null | undefined, inherited?: boolean | null) => { + if (value !== null && value !== undefined) { + return booleanLabel(value); } - return value ? Enabled : Disabled; + return inherited !== null && inherited !== undefined ? inheritedValue(booleanLabel(inherited)) : notSet; + }; + + const visibilityLabel = (value: string | null | undefined) => { + return value && DOCUMENT_VISIBILITY[value] ? _(DOCUMENT_VISIBILITY[value].value) : null; + }; + + const visibilityValue = (value: string | null | undefined, inherited?: string | null) => { + const label = visibilityLabel(value); + + if (label !== null) { + return label; + } + + return inheritedValue(visibilityLabel(inherited)); }; const parsedEmailSettings = ZDocumentEmailSettingsSchema.safeParse(settings.emailDocumentSettings); @@ -65,70 +100,82 @@ export const AdminGlobalSettingsSection = ({ settings, isTeam = false }: AdminGl
Document visibility}> - {settings.documentVisibility != null - ? _(DOCUMENT_VISIBILITY[settings.documentVisibility].value) - : notSetLabel} + {visibilityValue(settings.documentVisibility, inheritedSettings?.documentVisibility)} Document language}> - {textValue(settings.documentLanguage)} + {textValue(settings.documentLanguage, inheritedSettings?.documentLanguage)} Document timezone}> - {textValue(settings.documentTimezone)} + {textValue(settings.documentTimezone, inheritedSettings?.documentTimezone)} Date format}> - {textValue(settings.documentDateFormat)} + {textValue(settings.documentDateFormat, inheritedSettings?.documentDateFormat)} Include sender details}> - {booleanValue(settings.includeSenderDetails)} + + {booleanValue(settings.includeSenderDetails, inheritedSettings?.includeSenderDetails)} + Include signing certificate}> - {booleanValue(settings.includeSigningCertificate)} + + {booleanValue(settings.includeSigningCertificate, inheritedSettings?.includeSigningCertificate)} + Include audit log}> - {booleanValue(settings.includeAuditLog)} + {booleanValue(settings.includeAuditLog, inheritedSettings?.includeAuditLog)} Delegate document ownership}> - {booleanValue(settings.delegateDocumentOwnership)} + + {booleanValue(settings.delegateDocumentOwnership, inheritedSettings?.delegateDocumentOwnership)} + Typed signature}> - {booleanValue(settings.typedSignatureEnabled)} + + {booleanValue(settings.typedSignatureEnabled, inheritedSettings?.typedSignatureEnabled)} + Upload signature}> - {booleanValue(settings.uploadSignatureEnabled)} + + {booleanValue(settings.uploadSignatureEnabled, inheritedSettings?.uploadSignatureEnabled)} + Draw signature}> - {booleanValue(settings.drawSignatureEnabled)} + + {booleanValue(settings.drawSignatureEnabled, inheritedSettings?.drawSignatureEnabled)} + Branding}> - {booleanValue(settings.brandingEnabled)} + {booleanValue(settings.brandingEnabled, inheritedSettings?.brandingEnabled)} Branding logo}> - {brandingTextValue(settings.brandingLogo)} + {textValue(settings.brandingLogo, inheritedSettings?.brandingLogo)} Branding URL}> - {brandingTextValue(settings.brandingUrl)} + {textValue(settings.brandingUrl, inheritedSettings?.brandingUrl)} Branding company details}> - {brandingTextValue(settings.brandingCompanyDetails)} + + {textValue(settings.brandingCompanyDetails, inheritedSettings?.brandingCompanyDetails)} + Email reply-to}> - {textValue(settings.emailReplyTo)} + {textValue(settings.emailReplyTo, inheritedSettings?.emailReplyTo)} {isTeam && parsedEmailSettings.success && ( @@ -145,7 +192,7 @@ export const AdminGlobalSettingsSection = ({ settings, isTeam = false }: AdminGl )} AI features}> - {booleanValue(settings.aiFeaturesEnabled)} + {booleanValue(settings.aiFeaturesEnabled, inheritedSettings?.aiFeaturesEnabled)}
); diff --git a/apps/remix/app/components/general/claim-limit-fields.tsx b/apps/remix/app/components/general/claim-limit-fields.tsx index 494f18a0f..c90a92430 100644 --- a/apps/remix/app/components/general/claim-limit-fields.tsx +++ b/apps/remix/app/components/general/claim-limit-fields.tsx @@ -6,6 +6,13 @@ import type { Control, FieldValues, Path } from 'react-hook-form'; import { RateLimitArrayInput } from './rate-limit-array-input'; +/** + * The rate-limit editor renders its own per-row inline errors, but a submit + * attempt can still surface array-level Zod issues (e.g. a committed duplicate + * window). Rendering the field's message here guarantees the form never fails + * silently when those errors are not tied to a row the editor is showing. + */ + type ClaimLimitFieldsProps = { control: Control; /** e.g. '' for the claim form, 'claims.' for the org admin form. */ @@ -81,6 +88,7 @@ export const ClaimLimitFields = ({ + )} /> diff --git a/apps/remix/app/routes/_authenticated+/admin+/organisations.$id.tsx b/apps/remix/app/routes/_authenticated+/admin+/organisations.$id.tsx index 10b346083..d895b338d 100644 --- a/apps/remix/app/routes/_authenticated+/admin+/organisations.$id.tsx +++ b/apps/remix/app/routes/_authenticated+/admin+/organisations.$id.tsx @@ -8,6 +8,7 @@ import { getHighestOrganisationRoleInGroup } from '@documenso/lib/utils/organisa import { trpc } from '@documenso/trpc/react'; import type { TGetAdminOrganisationResponse } from '@documenso/trpc/server/admin-router/get-admin-organisation.types'; import { ZUpdateAdminOrganisationRequestSchema } from '@documenso/trpc/server/admin-router/update-admin-organisation.types'; +import { cn } from '@documenso/ui/lib/utils'; import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@documenso/ui/primitives/accordion'; import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert'; import { Badge } from '@documenso/ui/primitives/badge'; @@ -30,7 +31,7 @@ import { useToast } from '@documenso/ui/primitives/use-toast'; import { zodResolver } from '@hookform/resolvers/zod'; import { msg } from '@lingui/core/macro'; import { Trans, useLingui } from '@lingui/react/macro'; -import { OrganisationMemberRole } from '@prisma/client'; +import { OrganisationMemberRole, SubscriptionStatus } from '@prisma/client'; import { ExternalLinkIcon, InfoIcon, Loader } from 'lucide-react'; import { useMemo } from 'react'; import { useForm } from 'react-hook-form'; @@ -312,7 +313,15 @@ export default function OrganisationGroupSettingsPage({ params, loaderData }: Ro className="mt-16" /> - +
Subscription @@ -320,7 +329,12 @@ export default function OrganisationGroupSettingsPage({ params, loaderData }: Ro {organisation.subscription ? ( - {i18n._(SUBSCRIPTION_STATUS_MAP[organisation.subscription.status])} subscription found + + {organisation.subscription.status === SubscriptionStatus.ACTIVE && ( + ) : ( No subscription found @@ -333,6 +347,7 @@ export default function OrganisationGroupSettingsPage({ params, loaderData }: Ro
} /> -