feat: P2.1 extended signatures — multiple per identity + TipTap editor
- 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
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
import { generateUUID } from '@/lib/utils';
|
||||
|
||||
export interface Signature {
|
||||
id: string;
|
||||
name: string;
|
||||
body: string;
|
||||
plainText: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
interface SignatureState {
|
||||
signatures: Signature[];
|
||||
defaultSignatureId: string | null;
|
||||
replySignatureId: string | null;
|
||||
identitySignatureMap: Record<string, { defaultId?: string; replyId?: string }>;
|
||||
addSignature: (sig: Omit<Signature, 'id' | 'createdAt' | 'updatedAt'>) => Signature;
|
||||
updateSignature: (id: string, updates: Partial<Pick<Signature, 'name' | 'body' | 'plainText'>>) => void;
|
||||
deleteSignature: (id: string) => void;
|
||||
duplicateSignature: (id: string) => Signature;
|
||||
setDefaultSignatureId: (id: string | null) => void;
|
||||
setReplySignatureId: (id: string | null) => void;
|
||||
getSignatureById: (id: string) => Signature | undefined;
|
||||
setIdentitySignature: (identityId: string, type: 'default' | 'reply', signatureId: string | null) => void;
|
||||
getIdentityDefaultSignatureId: (identityId: string) => string | null;
|
||||
getIdentityReplySignatureId: (identityId: string) => string | null;
|
||||
}
|
||||
|
||||
export const useSignatureStore = create<SignatureState>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
signatures: [],
|
||||
defaultSignatureId: null,
|
||||
replySignatureId: null,
|
||||
identitySignatureMap: {},
|
||||
|
||||
addSignature: (sig) => {
|
||||
const now = new Date().toISOString();
|
||||
const newSig: Signature = {
|
||||
...sig,
|
||||
id: generateUUID(),
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
set((state) => ({
|
||||
signatures: [...state.signatures, newSig],
|
||||
}));
|
||||
return newSig;
|
||||
},
|
||||
|
||||
updateSignature: (id, updates) => {
|
||||
set((state) => ({
|
||||
signatures: state.signatures.map((s) =>
|
||||
s.id === id
|
||||
? { ...s, ...updates, updatedAt: new Date().toISOString() }
|
||||
: s
|
||||
),
|
||||
}));
|
||||
},
|
||||
|
||||
deleteSignature: (id) => {
|
||||
set((state) => ({
|
||||
signatures: state.signatures.filter((s) => s.id !== id),
|
||||
defaultSignatureId: state.defaultSignatureId === id ? null : state.defaultSignatureId,
|
||||
replySignatureId: state.replySignatureId === id ? null : state.replySignatureId,
|
||||
}));
|
||||
},
|
||||
|
||||
duplicateSignature: (id) => {
|
||||
const original = get().signatures.find((s) => s.id === id);
|
||||
if (!original) {
|
||||
throw new Error(`Signature with id ${id} not found`);
|
||||
}
|
||||
const now = new Date().toISOString();
|
||||
const duplicate: Signature = {
|
||||
...original,
|
||||
id: generateUUID(),
|
||||
name: `${original.name} (copy)`,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
set((state) => ({
|
||||
signatures: [...state.signatures, duplicate],
|
||||
}));
|
||||
return duplicate;
|
||||
},
|
||||
|
||||
setDefaultSignatureId: (id) => {
|
||||
set({ defaultSignatureId: id });
|
||||
},
|
||||
|
||||
setReplySignatureId: (id) => {
|
||||
set({ replySignatureId: id });
|
||||
},
|
||||
|
||||
getSignatureById: (id) => {
|
||||
return get().signatures.find((s) => s.id === id);
|
||||
},
|
||||
|
||||
setIdentitySignature: (identityId, type, signatureId) => {
|
||||
set((state) => {
|
||||
const current = state.identitySignatureMap[identityId] ?? {};
|
||||
const updated = {
|
||||
...current,
|
||||
[type === 'default' ? 'defaultId' : 'replyId']: signatureId ?? undefined,
|
||||
};
|
||||
if (updated.defaultId === undefined && updated.replyId === undefined) {
|
||||
const { [identityId]: _, ...rest } = state.identitySignatureMap;
|
||||
return { identitySignatureMap: rest };
|
||||
}
|
||||
return { identitySignatureMap: { ...state.identitySignatureMap, [identityId]: updated } };
|
||||
});
|
||||
},
|
||||
|
||||
getIdentityDefaultSignatureId: (identityId) => {
|
||||
const entry = get().identitySignatureMap[identityId];
|
||||
return entry?.defaultId ?? get().defaultSignatureId;
|
||||
},
|
||||
|
||||
getIdentityReplySignatureId: (identityId) => {
|
||||
const entry = get().identitySignatureMap[identityId];
|
||||
return entry?.replyId ?? get().replySignatureId ?? get().defaultSignatureId;
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: 'signature-storage',
|
||||
}
|
||||
)
|
||||
);
|
||||
Reference in New Issue
Block a user