@@ -14,7 +14,7 @@ import {
1414import { toast } from 'sonner' ;
1515import { useMutation , useQueryClient } from '@tanstack/react-query' ;
1616import { FileText , Save , Trash2 } from 'lucide-react' ;
17- import React , { useState , useMemo , useDeferredValue , useCallback , startTransition } from 'react' ;
17+ import React , { useState , useMemo , useDeferredValue , useCallback } from 'react' ;
1818import {
1919 Dialog ,
2020 DialogContent ,
@@ -25,21 +25,20 @@ import {
2525import { Input } from '@/components/ui/input' ;
2626import { TRPCClientError } from '@trpc/client' ;
2727
28- type RecipientField = 'to' | 'cc' | 'bcc' ;
29-
30- type Template = {
28+ type EmailTemplate = {
3129 id : string ;
30+ userId : string ;
3231 name : string ;
33- subject ?: string | null ;
34- body ?: string | null ;
35- to ?: string [ ] | null ;
36- cc ?: string [ ] | null ;
37- bcc ?: string [ ] | null ;
32+ subject : string | null ;
33+ body : string | null ;
34+ to : string [ ] | null ;
35+ cc : string [ ] | null ;
36+ bcc : string [ ] | null ;
37+ createdAt : Date ;
38+ updatedAt : Date ;
3839} ;
3940
40- type TemplatesQueryData = {
41- templates : Template [ ] ;
42- } | undefined ;
41+ type RecipientField = 'to' | 'cc' | 'bcc' ;
4342
4443interface TemplateButtonProps {
4544 editor : Editor | null ;
@@ -64,7 +63,7 @@ const TemplateButtonComponent: React.FC<TemplateButtonProps> = ({
6463 const queryClient = useQueryClient ( ) ;
6564 const { data } = useTemplates ( ) ;
6665
67- const templates : Template [ ] = data ?. templates ?? [ ] ;
66+ const templates = ( data ?. templates ?? [ ] ) as EmailTemplate [ ] ;
6867
6968 const [ menuOpen , setMenuOpen ] = useState ( false ) ;
7069 const [ isSaving , setIsSaving ] = useState ( false ) ;
@@ -81,6 +80,10 @@ const TemplateButtonComponent: React.FC<TemplateButtonProps> = ({
8180 ) ;
8281 } , [ deferredSearch , templates ] ) ;
8382
83+ const templatesById = useMemo ( ( ) => {
84+ return new Map ( templates . map ( ( t ) => [ t . id , t ] as const ) ) ;
85+ } , [ templates ] ) ;
86+
8487 const { mutateAsync : createTemplate } = useMutation ( trpc . templates . create . mutationOptions ( ) ) ;
8588 const { mutateAsync : deleteTemplateMutation } = useMutation (
8689 trpc . templates . delete . mutationOptions ( ) ,
@@ -95,19 +98,17 @@ const TemplateButtonComponent: React.FC<TemplateButtonProps> = ({
9598
9699 setIsSaving ( true ) ;
97100 try {
98- const newTemplate = await createTemplate ( {
101+ const normalizedSubject = subject . trim ( ) ? subject : null ;
102+ await createTemplate ( {
99103 name : templateName . trim ( ) ,
100- subject : subject || '' ,
101104 body : editor . getHTML ( ) ,
102105 to : to . length ? to : undefined ,
103106 cc : cc . length ? cc : undefined ,
104107 bcc : bcc . length ? bcc : undefined ,
108+ ...( normalizedSubject !== null ? { subject : normalizedSubject } : { } ) ,
105109 } ) ;
106- queryClient . setQueryData ( trpc . templates . list . queryKey ( ) , ( old : TemplatesQueryData ) => {
107- if ( ! old ?. templates ) return old ;
108- return {
109- templates : [ newTemplate . template , ...old . templates ] ,
110- } ;
110+ await queryClient . invalidateQueries ( {
111+ queryKey : trpc . templates . list . queryKey ( ) ,
111112 } ) ;
112113 toast . success ( 'Template saved' ) ;
113114 setTemplateName ( '' ) ;
@@ -123,15 +124,18 @@ const TemplateButtonComponent: React.FC<TemplateButtonProps> = ({
123124 }
124125 } ;
125126
126- const handleApplyTemplate = useCallback ( ( template : Template ) => {
127+ const handleApplyTemplate = useCallback ( ( template : EmailTemplate ) => {
127128 if ( ! editor ) return ;
128- startTransition ( ( ) => {
129- if ( template . subject ) setSubject ( template . subject ) ;
130- if ( template . body ) editor . commands . setContent ( template . body , false ) ;
131- if ( template . to ) setRecipients ( 'to' , template . to ) ;
132- if ( template . cc ) setRecipients ( 'cc' , template . cc ) ;
133- if ( template . bcc ) setRecipients ( 'bcc' , template . bcc ) ;
134- } ) ;
129+
130+ if ( template . subject ) setSubject ( template . subject ) ;
131+ if ( template . body ) editor . commands . setContent ( template . body , false ) ;
132+ if ( template . to ) setRecipients ( 'to' , template . to ) ;
133+ if ( template . cc ) setRecipients ( 'cc' , template . cc ) ;
134+ if ( template . bcc ) setRecipients ( 'bcc' , template . bcc ) ;
135+
136+ setTimeout ( ( ) => {
137+ editor . chain ( ) . focus ( 'end' ) . run ( ) ;
138+ } , 200 ) ;
135139 } , [ editor , setSubject , setRecipients ] ) ;
136140
137141 const handleDeleteTemplate = useCallback (
@@ -153,6 +157,41 @@ const TemplateButtonComponent: React.FC<TemplateButtonProps> = ({
153157 [ deleteTemplateMutation , queryClient , trpc . templates . list ] ,
154158 ) ;
155159
160+ const handleTemplateItemClick = useCallback (
161+ ( e : React . MouseEvent < HTMLElement > ) => {
162+ const templateId = ( e . currentTarget as HTMLElement ) . dataset . templateId ;
163+ if ( ! templateId ) return ;
164+ const template = templatesById . get ( templateId ) ;
165+ if ( ! template ) return ;
166+ handleApplyTemplate ( template ) ;
167+ setMenuOpen ( false ) ;
168+ } ,
169+ [ handleApplyTemplate , templatesById ] ,
170+ ) ;
171+
172+ const handleDeleteButtonClick = useCallback (
173+ ( e : React . MouseEvent < HTMLButtonElement > ) => {
174+ e . stopPropagation ( ) ;
175+ setMenuOpen ( false ) ;
176+ const templateId = ( e . currentTarget as HTMLButtonElement ) . dataset . templateId ;
177+ if ( ! templateId ) return ;
178+ const template = templatesById . get ( templateId ) ;
179+ const templateName = template ?. name ?? 'this template' ;
180+ toast ( `Delete template "${ templateName } "?` , {
181+ duration : 10000 ,
182+ action : {
183+ label : 'Delete' ,
184+ onClick : ( ) => handleDeleteTemplate ( templateId ) ,
185+ } ,
186+ className : 'pointer-events-auto' ,
187+ style : {
188+ pointerEvents : 'auto' ,
189+ } ,
190+ } ) ;
191+ } ,
192+ [ templatesById , handleDeleteTemplate ] ,
193+ ) ;
194+
156195 return (
157196 < >
158197 < DropdownMenu open = { menuOpen } onOpenChange = { setMenuOpen } >
@@ -171,7 +210,7 @@ const TemplateButtonComponent: React.FC<TemplateButtonProps> = ({
171210 >
172211 < Save className = "mr-2 h-3.5 w-3.5" /> Save current as template
173212 </ DropdownMenuItem >
174- { templates . length > 0 ? (
213+ { templates . length > 0 ? (
175214 < DropdownMenuSub >
176215 < DropdownMenuSubTrigger >
177216 < FileText className = "mr-2 h-3.5 w-3.5" /> Use template
@@ -187,30 +226,18 @@ const TemplateButtonComponent: React.FC<TemplateButtonProps> = ({
187226 />
188227 </ div >
189228 < div className = "max-h-30 overflow-y-auto" >
190- { filteredTemplates . map ( ( t : Template ) => (
229+ { filteredTemplates . map ( ( t ) => (
191230 < DropdownMenuItem
192231 key = { t . id }
232+ data-template-id = { t . id }
193233 className = "flex items-center justify-between gap-2"
194- onClick = { ( ) => handleApplyTemplate ( t ) }
234+ onClick = { handleTemplateItemClick }
195235 >
196236 < span className = "flex-1 truncate text-left" > { t . name } </ span >
197237 < button
198238 className = "p-0.5 text-muted-foreground hover:text-destructive"
199- onClick = { ( e ) => {
200- e . stopPropagation ( ) ;
201- setMenuOpen ( false ) ;
202- toast ( `Delete template "${ t . name } "?` , {
203- duration : 10000 ,
204- action : {
205- label : 'Delete' ,
206- onClick : ( ) => handleDeleteTemplate ( t . id ) ,
207- } ,
208- className : 'pointer-events-auto' ,
209- style : {
210- pointerEvents : 'auto' ,
211- } ,
212- } ) ;
213- } }
239+ data-template-id = { t . id }
240+ onClick = { handleDeleteButtonClick }
214241 >
215242 < Trash2 className = "h-3.5 w-3.5" />
216243 </ button >
0 commit comments