@@ -8,32 +8,75 @@ import React, { useCallback, useState } from 'react';
88import { v4 as uuidFunc } from 'uuid' ;
99import { Tag } from '../types' ;
1010
11+ const normalizeTagName = ( tagName : string ) => tagName . trim ( ) . toLowerCase ( ) ;
12+
1113export function AddTagDropdown ( {
1214 addTagCallback, currentNames, editTag = false , editableTag,
13- } : { addTagCallback : ( tag : Tag ) => void , currentNames : string [ ] , editTag ?: boolean , editableTag ?: Tag } ) {
15+ } : { addTagCallback : ( tag : Tag ) => void | Promise < void > , currentNames : string [ ] , editTag ?: boolean , editableTag ?: Tag } ) {
1416 const [ name , setName ] = useState < string > ( editTag ? editableTag ! . name : '' ) ;
1517 const [ color , setColor ] = useState < string > ( editTag ? editableTag ! . color : '#fd7e14' ) ;
18+ const [ isSubmitting , setIsSubmitting ] = useState ( false ) ;
19+ const [ submitError , setSubmitError ] = useState < string | null > ( null ) ;
20+
21+ const trimmedName = name . trim ( ) ;
22+ const normalizedName = normalizeTagName ( name ) ;
23+ const normalizedEditableName = editableTag ? normalizeTagName ( editableTag . name ) : null ;
24+ const duplicateName = normalizedName . length > 0 && currentNames . some ( ( currentName ) => {
25+ const normalizedCurrentName = normalizeTagName ( currentName ) ;
26+ return normalizedCurrentName === normalizedName
27+ && ( ! editTag || normalizedCurrentName !== normalizedEditableName ) ;
28+ } ) ;
29+ const unchangedTag = editTag && editableTag
30+ ? trimmedName === editableTag . name && color === editableTag . color
31+ : false ;
32+ const validationError = duplicateName ? 'Tag with this name already exists' : null ;
33+ const submitDisabled = trimmedName . length === 0 || duplicateName || unchangedTag || isSubmitting ;
34+
35+ const createTag = useCallback ( async ( ) => {
36+ if ( submitDisabled ) {
37+ return ;
38+ }
1639
17- const createTag = useCallback ( ( ) => {
1840 const uuid = uuidFunc ( ) ;
1941
20- addTagCallback ( { color, name, id : editTag && editableTag ? editableTag . id : uuid } ) ;
21- } , [ addTagCallback , color , editTag , editableTag , name ] ) ;
42+ setIsSubmitting ( true ) ;
43+ setSubmitError ( null ) ;
44+
45+ try {
46+ await addTagCallback ( { color, name : trimmedName , id : editTag && editableTag ? editableTag . id : uuid } ) ;
47+ } catch {
48+ setSubmitError ( 'Unable to save tag. Try again.' ) ;
49+ } finally {
50+ setIsSubmitting ( false ) ;
51+ }
52+ } , [ addTagCallback , color , editTag , editableTag , submitDisabled , trimmedName ] ) ;
2253
2354 return (
2455 < Stack gap = "xs" >
2556 < Group >
2657 < ColorSwatch color = { color } />
27- < TextInput required placeholder = "Enter tag name" value = { name } onChange = { ( e ) => setName ( e . currentTarget . value ) } error = { currentNames . includes ( name ) && ( ! editTag || editableTag ! . name !== name ) ? 'Tag with this name already exists' : null } />
58+ < TextInput
59+ required
60+ placeholder = "Enter tag name"
61+ value = { name }
62+ onChange = { ( e ) => {
63+ setName ( e . currentTarget . value ) ;
64+ setSubmitError ( null ) ;
65+ } }
66+ error = { validationError || submitError }
67+ />
2868 </ Group >
2969 < ColorPicker
3070 withPicker = { editTag }
3171 style = { { width : '100%' } }
3272 value = { color }
33- onChange = { ( e ) => setColor ( e ) }
73+ onChange = { ( e ) => {
74+ setColor ( e ) ;
75+ setSubmitError ( null ) ;
76+ } }
3477 swatches = { [ '#2e2e2e' , '#868e96' , '#fa5252' , '#e64980' , '#be4bdb' , '#7950f2' , '#4c6ef5' , '#228be6' , '#15aabf' , '#12b886' , '#40c057' , '#82c91e' , '#fab005' , '#fd7e14' ] }
3578 />
36- < Button disabled = { ( editTag && color === editableTag ! . color ) && ( name . length === 0 || currentNames . includes ( name ) ) } size = "compact-sm" onClick = { ( ) => createTag ( ) } > { editTag ? 'Edit Tag' : 'Add Tag' } </ Button >
79+ < Button disabled = { submitDisabled } loading = { isSubmitting } size = "compact-sm" onClick = { createTag } > { editTag ? 'Edit Tag' : 'Add Tag' } </ Button >
3780 </ Stack >
3881 ) ;
3982}
0 commit comments