mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-10-31 00:12:39 +00:00
fix: ask for the details every sending address needs and preview the signature that is sent
This commit is contained in:
@@ -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(<SignatureDetailsSection />);
|
||||
expect(screen.queryByTestId('signature-detail-title')).toBeNull();
|
||||
@@ -81,6 +81,29 @@ describe('Your details', () => {
|
||||
expect(screen.getByTestId('signature-details-preview').innerHTML).toContain('Alice<br>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(<SignatureDetailsSection />);
|
||||
expect(screen.getByTestId('signature-detail-phone')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('signature-details-preview').innerHTML).toBe('<p class="text-xs font-medium text-muted-foreground mb-1">preview</p><div class="p-3 border border-border rounded-md bg-muted/40 text-sm break-words min-w-0"><div><b>Own</b></div></div>');
|
||||
});
|
||||
|
||||
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(<SignatureDetailsSection />);
|
||||
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(<SignatureDetailsSection />);
|
||||
|
||||
@@ -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<SignatureField, { type: string; autoComplete: string }
|
||||
mobile: { type: 'tel', autoComplete: 'tel' },
|
||||
};
|
||||
|
||||
/** The details the organisation's signatures and disclaimers ask for, over every address the user sends from. */
|
||||
function useFieldsToAsk(): SignatureField[] {
|
||||
const identities = useIdentityStore((s) => 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<SignatureField>();
|
||||
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<SignatureField>([
|
||||
...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 (
|
||||
<SettingsSection id="signature-details" title={t('title')} description={t('description')}>
|
||||
|
||||
Reference in New Issue
Block a user