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 && (
+
+ )}
+ {i18n._(SUBSCRIPTION_STATUS_MAP[organisation.subscription.status])} subscription found
+
) : (
No subscription found
@@ -333,6 +347,7 @@ export default function OrganisationGroupSettingsPage({ params, loaderData }: Ro