- New stores/signature-store.ts: Zustand persist with CRUD, default/reply signature IDs, per-identity signature mapping - New signature-settings.tsx: list management with add/edit/delete/duplicate - New signature-editor-modal.tsx: TipTap rich text editor for signatures - email-composer.tsx: auto-insert signature based on mode (compose/reply) + signature selector dropdown in toolbar - identity-form.tsx: per-identity default/reply signature dropdowns - settings/page.tsx: Signatures tab in Mail settings group
274 lines
9.7 KiB
TypeScript
274 lines
9.7 KiB
TypeScript
'use client';
|
|
|
|
import { useState } from 'react';
|
|
import { useTranslations } from 'next-intl';
|
|
import { Button } from '@/components/ui/button';
|
|
import { ConfirmDialog } from '@/components/ui/confirm-dialog';
|
|
import { SettingsSection, SettingItem, Select } from './settings-section';
|
|
import { SignatureEditorModal } from './signature-editor-modal';
|
|
import { useSignatureStore, type Signature } from '@/stores/signature-store';
|
|
import { useIdentityStore } from '@/stores/identity-store';
|
|
import { truncateText } from '@/lib/utils';
|
|
import {
|
|
Plus,
|
|
Pencil,
|
|
Copy,
|
|
Trash2,
|
|
ChevronRight,
|
|
} from 'lucide-react';
|
|
|
|
export function SignatureSettings() {
|
|
const t = useTranslations('signatures');
|
|
const tCommon = useTranslations('common');
|
|
const {
|
|
signatures,
|
|
defaultSignatureId,
|
|
replySignatureId,
|
|
identitySignatureMap,
|
|
addSignature,
|
|
updateSignature,
|
|
deleteSignature,
|
|
duplicateSignature,
|
|
setDefaultSignatureId,
|
|
setReplySignatureId,
|
|
setIdentitySignature,
|
|
} = useSignatureStore();
|
|
const identities = useIdentityStore((s) => s.identities);
|
|
|
|
const [editingSignature, setEditingSignature] = useState<Signature | null>(null);
|
|
const [showEditor, setShowEditor] = useState(false);
|
|
const [deleteTarget, setDeleteTarget] = useState<Signature | null>(null);
|
|
|
|
const handleAdd = () => {
|
|
setEditingSignature(null);
|
|
setShowEditor(true);
|
|
};
|
|
|
|
const handleEdit = (sig: Signature) => {
|
|
setEditingSignature(sig);
|
|
setShowEditor(true);
|
|
};
|
|
|
|
const handleDuplicate = (id: string) => {
|
|
duplicateSignature(id);
|
|
};
|
|
|
|
const handleDeleteConfirm = () => {
|
|
if (deleteTarget) {
|
|
deleteSignature(deleteTarget.id);
|
|
setDeleteTarget(null);
|
|
}
|
|
};
|
|
|
|
const handleSave = (data: { name: string; body: string; plainText: string }) => {
|
|
if (editingSignature) {
|
|
updateSignature(editingSignature.id, data);
|
|
} else {
|
|
addSignature(data);
|
|
}
|
|
setShowEditor(false);
|
|
setEditingSignature(null);
|
|
};
|
|
|
|
const signatureOptions = [
|
|
{ value: '', label: t('no_signature') },
|
|
...signatures.map((sig) => ({ value: sig.id, label: sig.name })),
|
|
];
|
|
|
|
return (
|
|
<>
|
|
<SettingsSection title={t('title')} description={t('description')}>
|
|
<SettingItem
|
|
label={t('default_signature.label')}
|
|
description={t('default_signature.description')}
|
|
>
|
|
<div className="flex items-center gap-2">
|
|
<Select
|
|
value={defaultSignatureId ?? ''}
|
|
onChange={(value) => setDefaultSignatureId(value || null)}
|
|
options={signatureOptions}
|
|
ariaLabel={t('default_signature.label')}
|
|
/>
|
|
</div>
|
|
</SettingItem>
|
|
|
|
<SettingItem
|
|
label={t('reply_signature.label')}
|
|
description={t('reply_signature.description')}
|
|
>
|
|
<div className="flex items-center gap-2">
|
|
<Select
|
|
value={replySignatureId ?? ''}
|
|
onChange={(value) => setReplySignatureId(value || null)}
|
|
options={signatureOptions}
|
|
ariaLabel={t('reply_signature.label')}
|
|
/>
|
|
</div>
|
|
</SettingItem>
|
|
|
|
{identities.length > 0 && (
|
|
<SettingItem
|
|
label={t('per_identity_signatures.label')}
|
|
description={t('per_identity_signatures.description')}
|
|
>
|
|
<div className="space-y-2 max-w-xs">
|
|
{identities.map((identity) => {
|
|
const mapping = identitySignatureMap[identity.id] ?? {};
|
|
const identitySigOptions = [
|
|
{ value: '', label: t('use_global_default') },
|
|
...signatures.map((sig) => ({ value: sig.id, label: sig.name })),
|
|
];
|
|
return (
|
|
<div key={identity.id} className="border border-border rounded-md p-3 space-y-2">
|
|
<span className="text-sm font-medium block truncate">
|
|
{identity.name ? `${identity.name} <${identity.email}>` : identity.email}
|
|
</span>
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-xs text-muted-foreground w-16 shrink-0">
|
|
{t('default')}
|
|
</span>
|
|
<select
|
|
value={mapping.defaultId ?? ''}
|
|
onChange={(e) =>
|
|
setIdentitySignature(identity.id, 'default', e.target.value || null)
|
|
}
|
|
className="flex-1 px-2 py-1 text-xs rounded-md bg-muted border border-border text-foreground focus:outline-none focus:ring-2 focus:ring-ring"
|
|
>
|
|
{identitySigOptions.map((opt) => (
|
|
<option key={opt.value} value={opt.value}>
|
|
{opt.label}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-xs text-muted-foreground w-16 shrink-0">
|
|
{t('reply')}
|
|
</span>
|
|
<select
|
|
value={mapping.replyId ?? ''}
|
|
onChange={(e) =>
|
|
setIdentitySignature(identity.id, 'reply', e.target.value || null)
|
|
}
|
|
className="flex-1 px-2 py-1 text-xs rounded-md bg-muted border border-border text-foreground focus:outline-none focus:ring-2 focus:ring-ring"
|
|
>
|
|
{identitySigOptions.map((opt) => (
|
|
<option key={opt.value} value={opt.value}>
|
|
{opt.label}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</SettingItem>
|
|
)}
|
|
|
|
<div className="pt-2">
|
|
<div className="flex items-center justify-between mb-3">
|
|
<h4 className="text-sm font-medium text-foreground">
|
|
{t('your_signatures', { count: signatures.length })}
|
|
</h4>
|
|
<Button size="sm" onClick={handleAdd}>
|
|
<Plus className="w-4 h-4 me-1" />
|
|
{t('add_signature')}
|
|
</Button>
|
|
</div>
|
|
|
|
{signatures.length === 0 ? (
|
|
<p className="text-sm text-muted-foreground py-4 text-center">
|
|
{t('no_signatures')}
|
|
</p>
|
|
) : (
|
|
<div className="border border-border rounded-md divide-y divide-border">
|
|
{signatures.map((sig) => (
|
|
<div
|
|
key={sig.id}
|
|
className="flex items-center justify-between px-4 py-3 hover:bg-muted/50 transition-colors"
|
|
>
|
|
<button
|
|
type="button"
|
|
className="flex-1 flex items-center gap-3 min-w-0 text-start"
|
|
onClick={() => handleEdit(sig)}
|
|
>
|
|
<div className="min-w-0 flex-1">
|
|
<div className="text-sm font-medium text-foreground truncate">
|
|
{sig.name}
|
|
</div>
|
|
<div className="text-xs text-muted-foreground truncate mt-0.5">
|
|
{truncateText(sig.plainText, 80)}
|
|
</div>
|
|
</div>
|
|
<ChevronRight className="w-4 h-4 text-muted-foreground shrink-0" />
|
|
</button>
|
|
<div className="flex items-center gap-0.5 ml-2 shrink-0">
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
handleDuplicate(sig.id);
|
|
}}
|
|
title={t('duplicate')}
|
|
className="h-8 w-8 p-0"
|
|
>
|
|
<Copy className="w-4 h-4" />
|
|
</Button>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
handleEdit(sig);
|
|
}}
|
|
title={tCommon('edit')}
|
|
className="h-8 w-8 p-0"
|
|
>
|
|
<Pencil className="w-4 h-4" />
|
|
</Button>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
setDeleteTarget(sig);
|
|
}}
|
|
title={tCommon('delete')}
|
|
className="h-8 w-8 p-0 text-destructive hover:text-destructive"
|
|
>
|
|
<Trash2 className="w-4 h-4" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</SettingsSection>
|
|
|
|
{showEditor && (
|
|
<SignatureEditorModal
|
|
signature={editingSignature}
|
|
onSave={handleSave}
|
|
onClose={() => {
|
|
setShowEditor(false);
|
|
setEditingSignature(null);
|
|
}}
|
|
/>
|
|
)}
|
|
|
|
<ConfirmDialog
|
|
isOpen={!!deleteTarget}
|
|
onClose={() => setDeleteTarget(null)}
|
|
onConfirm={handleDeleteConfirm}
|
|
title={t('delete_title')}
|
|
message={t('delete_message', { name: deleteTarget?.name ?? '' })}
|
|
variant="destructive"
|
|
confirmText={tCommon('delete')}
|
|
/>
|
|
</>
|
|
);
|
|
}
|