diff --git a/components/identity/__tests__/identity-form-org-signature.test.tsx b/components/identity/__tests__/identity-form-org-signature.test.tsx index 2c7dae84f..c83a580a9 100644 --- a/components/identity/__tests__/identity-form-org-signature.test.tsx +++ b/components/identity/__tests__/identity-form-org-signature.test.tsx @@ -72,7 +72,7 @@ describe('identity signature editor and the organisation signature', () => { describe('Your details', () => { it('asks only for the fields the template uses, and stores them', () => { - setPolicy(); + setPolicy({ locked: true }); useIdentityStore.setState({ identities: [ALICE] }); render(); expect(screen.queryByTestId('signature-detail-title')).toBeNull(); @@ -81,6 +81,29 @@ describe('Your details', () => { expect(screen.getByTestId('signature-details-preview').innerHTML).toContain('Alice
Phone: +49 30 2'); }); + it('previews the signature that is sent: the own one while the organisation one is only the default', () => { + setPolicy(); + useIdentityStore.setState({ identities: [ALICE] }); + render(); + expect(screen.getByTestId('signature-detail-phone')).toBeInTheDocument(); + expect(screen.getByTestId('signature-details-preview').innerHTML).toBe('

preview

Own
'); + }); + + it('asks for what the signature of a group address uses too', () => { + const team = { id: 'id-team', name: 'Team', email: 'team@example.org', mayDelete: false } as Identity; + const policy: SettingsPolicy = { + ...DEFAULT_POLICY, + organisation: { ...DEFAULT_ORGANISATION_POLICY }, + scopes: [{ id: 'team', label: 'Team', match: { groups: ['team@example.org'] }, policy: { organisation: { signature: { text: '{name}\nMobile: {mobile}' } } } }], + }; + const subject = { email: 'alice@example.org', groups: ['team@example.org'] }; + usePolicyStore.setState({ rawPolicy: policy, policy: { ...policy, scopes: undefined }, subject }); + useIdentityStore.setState({ identities: [ALICE, team] }); + render(); + expect(screen.getByTestId('signature-detail-mobile')).toBeInTheDocument(); + expect(screen.queryByTestId('signature-detail-phone')).toBeNull(); + }); + it('shows nothing without an organisation signature', () => { setPolicy(undefined, { html: '', text: '', fields: [] }); const { container } = render(); diff --git a/components/settings/signature-details-section.tsx b/components/settings/signature-details-section.tsx index f0e52b7bc..641bc8e3a 100644 --- a/components/settings/signature-details-section.tsx +++ b/components/settings/signature-details-section.tsx @@ -1,10 +1,12 @@ 'use client'; +import { useMemo } from 'react'; import { useTranslations } from 'next-intl'; import { Input } from '@/components/ui/input'; import { SettingsSection, SettingItem } from './settings-section'; -import { DEFAULT_ORGANISATION_POLICY, type SignatureField } from '@/lib/admin/organisation-policy'; -import { signatureFieldsToAsk, signatureFieldsUsed } from '@/lib/org-signature'; +import { DEFAULT_ORGANISATION_POLICY, SIGNATURE_FIELDS, type SignatureField } from '@/lib/admin/organisation-policy'; +import { effectiveSignatureIdentity, signatureFieldsToAsk, signatureFieldsUsed } from '@/lib/org-signature'; +import { policyForSender } from '@/lib/org/signature-state'; import { sanitizeSignatureHtmlForDisplay } from '@/lib/email-sanitization'; import { getPlainTextSignature } from '@/lib/signature-utils'; import { useOrgSignatureSetup } from '@/hooks/use-org-signature'; @@ -19,33 +21,52 @@ const INPUT_PROPS: Record s.identities); + const policy = usePolicyStore((s) => s.policy); + const rawPolicy = usePolicyStore((s) => s.rawPolicy); + const subject = usePolicyStore((s) => s.subject); + const resolveFor = usePolicyStore((s) => s.resolveFor); + return useMemo(() => { + const view = { policy, rawPolicy, subject, resolveFor }; + // Every identity's policy (a group address may have its group's), and + // the account's own for an account without identities yet. + const policies = [policy, ...identities.map((identity) => policyForSender(view, identity.email))]; + const used = new Set(); + for (const each of policies) { + const organisation = each.organisation ?? DEFAULT_ORGANISATION_POLICY; + for (const field of [...signatureFieldsToAsk(organisation.signature), ...signatureFieldsUsed(organisation.disclaimer)]) { + used.add(field); + } + } + return SIGNATURE_FIELDS.filter((field) => used.has(field)); + }, [identities, policy, rawPolicy, subject, resolveFor]); +} + /** * "Your details": what the organisation signature asks each user for * (title, department, phone, mobile), filled in once and used for every - * address. Shows only the fields the admin's templates use, and nothing - * without an organisation signature. Synced with the other settings; in - * Lite it stays in this browser. + * address. Shows the fields the signatures of any of the user's addresses + * use, and nothing when none does. The preview is the signature the + * primary address sends: the user's own where it still applies. Synced + * with the other settings; in Lite it stays in this browser. */ export function SignatureDetailsSection() { const t = useTranslations('org_signature.details'); - const organisation = usePolicyStore((s) => s.policy.organisation) ?? DEFAULT_ORGANISATION_POLICY; const details = useSettingsStore((s) => s.signatureDetails); const updateSetting = useSettingsStore((s) => s.updateSetting); const primary = useIdentityStore((s) => s.identities[0]); const setup = useOrgSignatureSetup(primary); - - const used = new Set([ - ...signatureFieldsToAsk(organisation.signature), - ...signatureFieldsUsed(organisation.disclaimer), - ]); - const fields = (['title', 'department', 'phone', 'mobile'] as const).filter((field) => used.has(field)); + const fields = useFieldsToAsk(); if (fields.length === 0) return null; - const preview = setup.signature - ? setup.signature.html - ? { html: sanitizeSignatureHtmlForDisplay(setup.signature.html) } - : { text: getPlainTextSignature({ textSignature: setup.signature.text }) } - : null; + const sent = effectiveSignatureIdentity(primary, primary, setup.signature, setup.restriction); + const preview = sent?.htmlSignature?.trim() + ? { html: sanitizeSignatureHtmlForDisplay(sent.htmlSignature) } + : sent?.textSignature?.trim() + ? { text: getPlainTextSignature(sent) } + : null; return (