11"use client"
22
33import { useEffect , useState } from "react"
4+ import { z } from "zod"
45import {
56 CloseButton ,
67 DialogActions ,
@@ -22,6 +23,7 @@ type EditServiceModalProps = {
2223 service : Partial < ServiceWithRelations > ,
2324 prevService : Partial < ServiceWithRelations >
2425 ) => Promise < ServiceWithRelations | null >
26+ doesServiceCodeExist : ( code : string ) => Promise < boolean >
2527 revalidateTable : ( ) => Promise < void >
2628}
2729
@@ -31,11 +33,37 @@ export const EditServiceModal = ({
3133 service,
3234 offices,
3335 updateService,
36+ doesServiceCodeExist,
3437 revalidateTable,
3538} : EditServiceModalProps ) => {
3639 const [ isSaving , setIsSaving ] = useState ( false )
3740 const [ formData , setFormData ] = useState < ServiceWithRelations | null > ( null )
3841 const [ previousService , setPreviousService ] = useState < ServiceWithRelations | null > ( null )
42+ const [ isFormValidState , setIsFormValidState ] = useState < boolean > ( false )
43+ const [ isFormValidating , setIsFormValidating ] = useState < boolean > ( false )
44+
45+ const EditServiceWithRelationsSchema = z . object ( {
46+ name : z . string ( ) . min ( 1 , "Name is required" ) ,
47+ code : z
48+ . string ( )
49+ . min ( 1 , "Code is required" )
50+ . refine (
51+ async ( code ) => {
52+ if ( code === previousService ?. code ) return true
53+ return ! ( await doesServiceCodeExist ( code ) )
54+ } ,
55+ { message : "Code already exists" }
56+ ) ,
57+ description : z . string ( ) ,
58+ publicName : z . string ( ) . min ( 1 , "Public name is required" ) ,
59+ ticketPrefix : z . string ( ) . min ( 1 , "Ticket prefix is required" ) ,
60+ legacyServiceId : z . number ( ) . nullable ( ) ,
61+ backOffice : z . boolean ( ) ,
62+ deletedAt : z . date ( ) . nullable ( ) ,
63+ createdAt : z . date ( ) ,
64+ updatedAt : z . date ( ) ,
65+ locations : z . array ( z . any ( ) ) ,
66+ } )
3967
4068 useEffect ( ( ) => {
4169 if ( open && service ) {
@@ -44,6 +72,34 @@ export const EditServiceModal = ({
4472 }
4573 } , [ open , service ] )
4674
75+ // Validate formData asynchronously and update local state instead of calling async validators during render
76+ // biome-ignore lint/correctness/useExhaustiveDependencies: <>
77+ useEffect ( ( ) => {
78+ if ( ! formData ) {
79+ setIsFormValidState ( false )
80+ setIsFormValidating ( false )
81+ return
82+ }
83+
84+ let active = true
85+ setIsFormValidating ( true )
86+
87+ EditServiceWithRelationsSchema . parseAsync ( formData )
88+ . then ( ( ) => {
89+ if ( active ) setIsFormValidState ( true )
90+ } )
91+ . catch ( ( ) => {
92+ if ( active ) setIsFormValidState ( false )
93+ } )
94+ . finally ( ( ) => {
95+ if ( active ) setIsFormValidating ( false )
96+ } )
97+
98+ return ( ) => {
99+ active = false
100+ }
101+ } , [ formData , previousService , doesServiceCodeExist ] )
102+
47103 if ( ! service || ! formData || ! previousService ) return null
48104
49105 const isArchived = service . deletedAt !== null
@@ -81,6 +137,7 @@ export const EditServiceModal = ({
81137 service = { formData }
82138 offices = { offices }
83139 setFormData = { setFormData }
140+ doesServiceCodeExist = { doesServiceCodeExist }
84141 isReadonly = { isReadonly }
85142 />
86143 </ form >
@@ -94,7 +151,7 @@ export const EditServiceModal = ({
94151 type = "button"
95152 className = "primary"
96153 onClick = { handleSave }
97- disabled = { isReadonly || isSaving }
154+ disabled = { isReadonly || isSaving || isFormValidating || ! isFormValidState }
98155 >
99156 { isSaving ? "Saving..." : "Save Changes" }
100157 </ button >
0 commit comments