@@ -14,6 +14,7 @@ import {
1414 message
1515} from 'antd'
1616import React , { useMemo , useState , useEffect } from 'react'
17+ import { debounce } from 'lodash'
1718import { useParams } from '@tanstack/react-router'
1819import { useAuth } from '../../contexts/AuthContext'
1920import { TreeNodeInput , HasLeaf } from './input'
@@ -26,6 +27,7 @@ import {
2627 createSegment ,
2728 updateSegment ,
2829 previewSegment ,
30+ getSegment ,
2931 CreateSegmentRequest ,
3032 UpdateSegmentRequest ,
3133 PreviewSegmentRequest ,
@@ -125,6 +127,10 @@ const DrawerSegment = (props: {
125127 const [ loadingPreview , setLoadingPreview ] = useState ( false )
126128 const [ previewedData , setPreviewedData ] = useState < string | undefined > ( ) // track the tree hash to avoid re-render
127129 const [ previewResponse , setPreviewResponse ] = useState < PreviewSegmentResponse | undefined > ( )
130+ const [ idValidation , setIdValidation ] = useState < {
131+ status : '' | 'validating' | 'error' | 'success'
132+ message : string
133+ } > ( { status : '' , message : '' } )
128134
129135 // Find the current workspace
130136 const workspace = useMemo ( ( ) => {
@@ -168,6 +174,56 @@ const DrawerSegment = (props: {
168174
169175 const lists = listsData ?. lists || [ ]
170176
177+ // Generate segment ID from name (same logic as in onFinish)
178+ const generateSegmentId = ( name : string ) : string => {
179+ return name
180+ . toLowerCase ( )
181+ . replace ( / [ \s - ] + / g, '_' )
182+ . replace ( / [ ^ a - z 0 - 9 _ ] / g, '' )
183+ . replace ( / ^ _ + | _ + $ / g, '' )
184+ . replace ( / _ + / g, '_' )
185+ }
186+
187+ // Debounced function to check if segment ID exists
188+ const checkIdExists = useMemo (
189+ ( ) =>
190+ debounce ( async ( name : string ) => {
191+ // Skip validation in edit mode or if no workspace
192+ if ( ! name || ! workspaceId || props . segment ) {
193+ setIdValidation ( { status : '' , message : '' } )
194+ return
195+ }
196+
197+ const id = generateSegmentId ( name )
198+ if ( ! id ) {
199+ setIdValidation ( { status : '' , message : '' } )
200+ return
201+ }
202+
203+ setIdValidation ( { status : 'validating' , message : '' } )
204+
205+ try {
206+ await getSegment ( { workspace_id : workspaceId , id } )
207+ // Segment exists (active or deleted) - show error
208+ setIdValidation ( {
209+ status : 'error' ,
210+ message : t `A segment with ID "${ id } " already exists`
211+ } )
212+ } catch {
213+ // Segment not found - ID is available
214+ setIdValidation ( { status : 'success' , message : '' } )
215+ }
216+ } , 500 ) ,
217+ [ workspaceId , props . segment , t ]
218+ )
219+
220+ // Cleanup debounce on unmount
221+ useEffect ( ( ) => {
222+ return ( ) => {
223+ checkIdExists . cancel ( )
224+ }
225+ } , [ checkIdExists ] )
226+
171227 const preview = async ( ) => {
172228 if ( loadingPreview || ! workspaceId ) return
173229 setLoadingPreview ( true )
@@ -211,6 +267,12 @@ const DrawerSegment = (props: {
211267 const onFinish = async ( values : { name : string ; color : string ; tree : TreeNode ; timezone : string } ) => {
212268 if ( loading || ! workspaceId ) return
213269
270+ // Block submission if ID validation failed (only for create mode)
271+ if ( ! props . segment && idValidation . status === 'error' ) {
272+ message . error ( t `Please choose a different segment name` )
273+ return
274+ }
275+
214276 setLoading ( true )
215277
216278 try {
@@ -309,8 +371,26 @@ const DrawerSegment = (props: {
309371 < Col span = { 18 } >
310372 < Form . Item label = { t `Name` } required >
311373 < Space . Compact style = { { width : '100%' } } >
312- < Form . Item name = "name" noStyle rules = { [ { required : true , type : 'string' } ] } >
313- < Input placeholder = { t `i.e: Big spenders...` } style = { { flex : 1 } } />
374+ < Form . Item
375+ name = "name"
376+ rules = { [ { required : true , type : 'string' } ] }
377+ validateStatus = {
378+ idValidation . status === 'validating'
379+ ? 'validating'
380+ : idValidation . status === 'error'
381+ ? 'error'
382+ : idValidation . status === 'success'
383+ ? 'success'
384+ : undefined
385+ }
386+ help = { idValidation . status === 'error' ? idValidation . message : undefined }
387+ hasFeedback = { ! props . segment && idValidation . status !== '' }
388+ style = { { flex : 1 , marginBottom : 0 } }
389+ >
390+ < Input
391+ placeholder = { t `i.e: Big spenders...` }
392+ onChange = { ( e ) => checkIdExists ( e . target . value ) }
393+ />
314394 </ Form . Item >
315395 < Form . Item noStyle name = "color" >
316396 < Select
0 commit comments