@@ -18,10 +18,11 @@ import {
1818 ModalTabsContent ,
1919 ModalTabsList ,
2020 ModalTabsTrigger ,
21+ TagInput ,
22+ type TagItem ,
2123} from '@/components/emcn'
2224import { SlackIcon } from '@/components/icons'
2325import { Skeleton } from '@/components/ui'
24- import { cn } from '@/lib/core/utils/cn'
2526import { quickValidateEmail } from '@/lib/messaging/email/validation'
2627import {
2728 type NotificationSubscription ,
@@ -156,8 +157,7 @@ export function NotificationSettings({
156157 errorCountThreshold : 10 ,
157158 } )
158159
159- const [ emailInputValue , setEmailInputValue ] = useState ( '' )
160- const [ invalidEmails , setInvalidEmails ] = useState < string [ ] > ( [ ] )
160+ const [ emailItems , setEmailItems ] = useState < TagItem [ ] > ( [ ] )
161161
162162 const [ formErrors , setFormErrors ] = useState < Record < string , string > > ( { } )
163163
@@ -225,8 +225,7 @@ export function NotificationSettings({
225225 } )
226226 setFormErrors ( { } )
227227 setEditingId ( null )
228- setEmailInputValue ( '' )
229- setInvalidEmails ( [ ] )
228+ setEmailItems ( [ ] )
230229 } , [ ] )
231230
232231 const handleClose = useCallback ( ( ) => {
@@ -243,81 +242,37 @@ export function NotificationSettings({
243242 const normalized = email . trim ( ) . toLowerCase ( )
244243 const validation = quickValidateEmail ( normalized )
245244
246- if ( formData . emailRecipients . includes ( normalized ) || invalidEmails . includes ( normalized ) ) {
245+ if ( emailItems . some ( ( item ) => item . value === normalized ) ) {
247246 return false
248247 }
249248
250- if ( ! validation . isValid ) {
251- setInvalidEmails ( ( prev ) => [ ...prev , normalized ] )
252- setEmailInputValue ( '' )
253- return false
254- }
255-
256- setFormErrors ( ( prev ) => ( { ...prev , emailRecipients : '' } ) )
257- setFormData ( ( prev ) => ( {
258- ...prev ,
259- emailRecipients : [ ...prev . emailRecipients , normalized ] ,
260- } ) )
261- setEmailInputValue ( '' )
262- return true
263- } ,
264- [ formData . emailRecipients , invalidEmails ]
265- )
266-
267- const handleRemoveEmail = useCallback ( ( emailToRemove : string ) => {
268- setFormData ( ( prev ) => ( {
269- ...prev ,
270- emailRecipients : prev . emailRecipients . filter ( ( e ) => e !== emailToRemove ) ,
271- } ) )
272- } , [ ] )
249+ setEmailItems ( ( prev ) => [ ...prev , { value : normalized , isValid : validation . isValid } ] )
273250
274- const handleRemoveInvalidEmail = useCallback ( ( index : number ) => {
275- setInvalidEmails ( ( prev ) => prev . filter ( ( _ , i ) => i !== index ) )
276- } , [ ] )
277-
278- const handleEmailKeyDown = useCallback (
279- ( e : React . KeyboardEvent < HTMLInputElement > ) => {
280- if ( [ 'Enter' , ',' , ' ' ] . includes ( e . key ) && emailInputValue . trim ( ) ) {
281- e . preventDefault ( )
282- addEmail ( emailInputValue )
251+ if ( validation . isValid ) {
252+ setFormErrors ( ( prev ) => ( { ...prev , emailRecipients : '' } ) )
253+ setFormData ( ( prev ) => ( {
254+ ...prev ,
255+ emailRecipients : [ ...prev . emailRecipients , normalized ] ,
256+ } ) )
283257 }
284258
285- if ( e . key === 'Backspace' && ! emailInputValue ) {
286- if ( invalidEmails . length > 0 ) {
287- handleRemoveInvalidEmail ( invalidEmails . length - 1 )
288- } else if ( formData . emailRecipients . length > 0 ) {
289- handleRemoveEmail ( formData . emailRecipients [ formData . emailRecipients . length - 1 ] )
290- }
291- }
259+ return validation . isValid
292260 } ,
293- [
294- emailInputValue ,
295- addEmail ,
296- invalidEmails ,
297- formData . emailRecipients ,
298- handleRemoveInvalidEmail ,
299- handleRemoveEmail ,
300- ]
261+ [ emailItems ]
301262 )
302263
303- const handleEmailPaste = useCallback (
304- ( e : React . ClipboardEvent < HTMLInputElement > ) => {
305- e . preventDefault ( )
306- const pastedText = e . clipboardData . getData ( 'text' )
307- const pastedEmails = pastedText . split ( / [ \s , ; ] + / ) . filter ( Boolean )
308-
309- let addedCount = 0
310- pastedEmails . forEach ( ( email ) => {
311- if ( addEmail ( email ) ) {
312- addedCount ++
313- }
314- } )
315-
316- if ( addedCount === 0 && pastedEmails . length === 1 ) {
317- setEmailInputValue ( emailInputValue + pastedEmails [ 0 ] )
264+ const handleRemoveEmailItem = useCallback (
265+ ( _value : string , index : number , isValid : boolean ) => {
266+ const itemToRemove = emailItems [ index ]
267+ setEmailItems ( ( prev ) => prev . filter ( ( _ , i ) => i !== index ) )
268+ if ( isValid && itemToRemove ) {
269+ setFormData ( ( prev ) => ( {
270+ ...prev ,
271+ emailRecipients : prev . emailRecipients . filter ( ( e ) => e !== itemToRemove . value ) ,
272+ } ) )
318273 }
319274 } ,
320- [ addEmail , emailInputValue ]
275+ [ emailItems ]
321276 )
322277
323278 const validateForm = ( ) : boolean => {
@@ -356,8 +311,11 @@ export function NotificationSettings({
356311 } else if ( formData . emailRecipients . length > 10 ) {
357312 errors . emailRecipients = 'Maximum 10 email recipients allowed'
358313 }
359- if ( invalidEmails . length > 0 ) {
360- errors . emailRecipients = `Invalid email addresses: ${ invalidEmails . join ( ', ' ) } `
314+ const invalidEmailValues = emailItems
315+ . filter ( ( item ) => ! item . isValid )
316+ . map ( ( item ) => item . value )
317+ if ( invalidEmailValues . length > 0 ) {
318+ errors . emailRecipients = `Invalid email addresses: ${ invalidEmailValues . join ( ', ' ) } `
361319 }
362320 }
363321
@@ -536,8 +494,9 @@ export function NotificationSettings({
536494 inactivityHours : subscription . alertConfig ?. inactivityHours || 24 ,
537495 errorCountThreshold : subscription . alertConfig ?. errorCountThreshold || 10 ,
538496 } )
539- setEmailInputValue ( '' )
540- setInvalidEmails ( [ ] )
497+ setEmailItems (
498+ ( subscription . emailRecipients || [ ] ) . map ( ( email ) => ( { value : email , isValid : true } ) )
499+ )
541500 setShowForm ( true )
542501 }
543502
@@ -692,37 +651,13 @@ export function NotificationSettings({
692651 { activeTab === 'email' && (
693652 < div className = 'flex flex-col gap-[8px]' >
694653 < Label className = 'text-[var(--text-secondary)]' > Email Recipients</ Label >
695- < div className = 'scrollbar-hide flex max-h-32 flex-wrap items-center gap-x-[8px] gap-y-[4px] overflow-y-auto rounded-[4px] border border-[var(--border-1)] bg-[var(--surface-5)] px-[8px] py-[6px] focus-within:outline-none dark:bg-[var(--surface-5)]' >
696- { invalidEmails . map ( ( email , index ) => (
697- < EmailTag
698- key = { `invalid-${ index } ` }
699- email = { email }
700- onRemove = { ( ) => handleRemoveInvalidEmail ( index ) }
701- isInvalid = { true }
702- />
703- ) ) }
704- { formData . emailRecipients . map ( ( email , index ) => (
705- < EmailTag
706- key = { `valid-${ index } ` }
707- email = { email }
708- onRemove = { ( ) => handleRemoveEmail ( email ) }
709- />
710- ) ) }
711- < input
712- type = 'text'
713- value = { emailInputValue }
714- onChange = { ( e ) => setEmailInputValue ( e . target . value ) }
715- onKeyDown = { handleEmailKeyDown }
716- onPaste = { handleEmailPaste }
717- onBlur = { ( ) => emailInputValue . trim ( ) && addEmail ( emailInputValue ) }
718- placeholder = {
719- formData . emailRecipients . length > 0 || invalidEmails . length > 0
720- ? 'Add another email'
721- : 'Enter emails'
722- }
723- className = 'min-w-[180px] flex-1 border-none bg-transparent p-0 font-medium font-sans text-foreground text-sm outline-none placeholder:text-[var(--text-muted)] disabled:cursor-not-allowed disabled:opacity-50'
724- />
725- </ div >
654+ < TagInput
655+ items = { emailItems }
656+ onAdd = { ( value ) => addEmail ( value ) }
657+ onRemove = { handleRemoveEmailItem }
658+ placeholder = 'Enter emails'
659+ placeholderWithTags = 'Add email'
660+ />
726661 { formErrors . emailRecipients && (
727662 < p className = 'text-[11px] text-[var(--text-error)]' > { formErrors . emailRecipients } </ p >
728663 ) }
@@ -1351,37 +1286,3 @@ export function NotificationSettings({
13511286 </ >
13521287 )
13531288}
1354-
1355- interface EmailTagProps {
1356- email : string
1357- onRemove : ( ) => void
1358- isInvalid ?: boolean
1359- }
1360-
1361- function EmailTag ( { email, onRemove, isInvalid } : EmailTagProps ) {
1362- return (
1363- < div
1364- className = { cn (
1365- 'flex w-auto items-center gap-[4px] rounded-[4px] border px-[6px] py-[2px] text-[12px]' ,
1366- isInvalid
1367- ? 'border-[var(--text-error)] bg-[color-mix(in_srgb,var(--text-error)_10%,transparent)] text-[var(--text-error)] dark:bg-[color-mix(in_srgb,var(--text-error)_16%,transparent)]'
1368- : 'border-[var(--border-1)] bg-[var(--surface-4)] text-[var(--text-secondary)] hover:text-[var(--text-primary)]'
1369- ) }
1370- >
1371- < span className = 'max-w-[200px] truncate' > { email } </ span >
1372- < button
1373- type = 'button'
1374- onClick = { onRemove }
1375- className = { cn (
1376- 'flex-shrink-0 transition-colors focus:outline-none' ,
1377- isInvalid
1378- ? 'text-[var(--text-error)] hover:text-[var(--text-error)]'
1379- : 'text-[var(--text-tertiary)] hover:text-[var(--text-primary)]'
1380- ) }
1381- aria-label = { `Remove ${ email } ` }
1382- >
1383- < X className = 'h-[12px] w-[12px] translate-y-[0.2px]' />
1384- </ button >
1385- </ div >
1386- )
1387- }
0 commit comments