From 722fef488ec930c09b347a52e5e940f2974cbd5c Mon Sep 17 00:00:00 2001 From: amrit Date: Mon, 11 Aug 2025 23:02:35 +0530 Subject: [PATCH] feat: add autofocus into the composer after a template is selected (#1978) ## Summary by cubic Added autofocus to the email composer after selecting a template, so users can start editing right away. This improves workflow by placing the cursor at the end of the content automatically. ## Summary by CodeRabbit - New Features - Added a delete confirmation toast with an action before removing a template. - Added an empty-state message when no templates are available. - Applying a template now fills subject, body, and recipients, then returns focus to the editor. - The template menu closes immediately after selection. - Smoother searching/filtering when browsing templates. - Bug Fixes - Templates list refreshes after creating a new template to reliably show the latest items. --- .../components/create/template-button.tsx | 119 +++++++++++------- 1 file changed, 73 insertions(+), 46 deletions(-) diff --git a/apps/mail/components/create/template-button.tsx b/apps/mail/components/create/template-button.tsx index 55bb5f861..56528a110 100644 --- a/apps/mail/components/create/template-button.tsx +++ b/apps/mail/components/create/template-button.tsx @@ -14,7 +14,7 @@ import { import { toast } from 'sonner'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import { FileText, Save, Trash2 } from 'lucide-react'; -import React, { useState, useMemo, useDeferredValue, useCallback, startTransition } from 'react'; +import React, { useState, useMemo, useDeferredValue, useCallback } from 'react'; import { Dialog, DialogContent, @@ -25,21 +25,20 @@ import { import { Input } from '@/components/ui/input'; import { TRPCClientError } from '@trpc/client'; -type RecipientField = 'to' | 'cc' | 'bcc'; - -type Template = { +type EmailTemplate = { id: string; + userId: string; name: string; - subject?: string | null; - body?: string | null; - to?: string[] | null; - cc?: string[] | null; - bcc?: string[] | null; + subject: string | null; + body: string | null; + to: string[] | null; + cc: string[] | null; + bcc: string[] | null; + createdAt: Date; + updatedAt: Date; }; -type TemplatesQueryData = { - templates: Template[]; -} | undefined; +type RecipientField = 'to' | 'cc' | 'bcc'; interface TemplateButtonProps { editor: Editor | null; @@ -64,7 +63,7 @@ const TemplateButtonComponent: React.FC = ({ const queryClient = useQueryClient(); const { data } = useTemplates(); - const templates: Template[] = data?.templates ?? []; + const templates = (data?.templates ?? []) as EmailTemplate[]; const [menuOpen, setMenuOpen] = useState(false); const [isSaving, setIsSaving] = useState(false); @@ -81,6 +80,10 @@ const TemplateButtonComponent: React.FC = ({ ); }, [deferredSearch, templates]); + const templatesById = useMemo(() => { + return new Map(templates.map((t) => [t.id, t] as const)); + }, [templates]); + const { mutateAsync: createTemplate } = useMutation(trpc.templates.create.mutationOptions()); const { mutateAsync: deleteTemplateMutation } = useMutation( trpc.templates.delete.mutationOptions(), @@ -95,19 +98,17 @@ const TemplateButtonComponent: React.FC = ({ setIsSaving(true); try { - const newTemplate = await createTemplate({ + const normalizedSubject = subject.trim() ? subject : null; + await createTemplate({ name: templateName.trim(), - subject: subject || '', body: editor.getHTML(), to: to.length ? to : undefined, cc: cc.length ? cc : undefined, bcc: bcc.length ? bcc : undefined, + ...(normalizedSubject !== null ? { subject: normalizedSubject } : {}), }); - queryClient.setQueryData(trpc.templates.list.queryKey(), (old: TemplatesQueryData) => { - if (!old?.templates) return old; - return { - templates: [newTemplate.template, ...old.templates], - }; + await queryClient.invalidateQueries({ + queryKey: trpc.templates.list.queryKey(), }); toast.success('Template saved'); setTemplateName(''); @@ -123,15 +124,18 @@ const TemplateButtonComponent: React.FC = ({ } }; - const handleApplyTemplate = useCallback((template: Template) => { + const handleApplyTemplate = useCallback((template: EmailTemplate) => { if (!editor) return; - startTransition(() => { - if (template.subject) setSubject(template.subject); - if (template.body) editor.commands.setContent(template.body, false); - if (template.to) setRecipients('to', template.to); - if (template.cc) setRecipients('cc', template.cc); - if (template.bcc) setRecipients('bcc', template.bcc); - }); + + if (template.subject) setSubject(template.subject); + if (template.body) editor.commands.setContent(template.body, false); + if (template.to) setRecipients('to', template.to); + if (template.cc) setRecipients('cc', template.cc); + if (template.bcc) setRecipients('bcc', template.bcc); + + setTimeout(() => { + editor.chain().focus('end').run(); + }, 200); }, [editor, setSubject, setRecipients]); const handleDeleteTemplate = useCallback( @@ -153,6 +157,41 @@ const TemplateButtonComponent: React.FC = ({ [deleteTemplateMutation, queryClient, trpc.templates.list], ); + const handleTemplateItemClick = useCallback( + (e: React.MouseEvent) => { + const templateId = (e.currentTarget as HTMLElement).dataset.templateId; + if (!templateId) return; + const template = templatesById.get(templateId); + if (!template) return; + handleApplyTemplate(template); + setMenuOpen(false); + }, + [handleApplyTemplate, templatesById], + ); + + const handleDeleteButtonClick = useCallback( + (e: React.MouseEvent) => { + e.stopPropagation(); + setMenuOpen(false); + const templateId = (e.currentTarget as HTMLButtonElement).dataset.templateId; + if (!templateId) return; + const template = templatesById.get(templateId); + const templateName = template?.name ?? 'this template'; + toast(`Delete template "${templateName}"?`, { + duration: 10000, + action: { + label: 'Delete', + onClick: () => handleDeleteTemplate(templateId), + }, + className: 'pointer-events-auto', + style: { + pointerEvents: 'auto', + }, + }); + }, + [templatesById, handleDeleteTemplate], + ); + return ( <> @@ -171,7 +210,7 @@ const TemplateButtonComponent: React.FC = ({ > Save current as template - {templates.length > 0 ? ( + {templates.length > 0 ? ( Use template @@ -187,30 +226,18 @@ const TemplateButtonComponent: React.FC = ({ />
- {filteredTemplates.map((t: Template) => ( + {filteredTemplates.map((t) => ( handleApplyTemplate(t)} + onClick={handleTemplateItemClick} > {t.name}