@@ -11,6 +11,8 @@ import { convertRemToPixels } from '@scality/core-ui/dist/utils';
1111import {
1212 Controller ,
1313 FormProvider ,
14+ Resolver ,
15+ ResolverOptions ,
1416 SubmitHandler ,
1517 useForm ,
1618} from 'react-hook-form' ;
@@ -39,6 +41,8 @@ import {
3941} from '../next-architecture/domain/business/buckets' ;
4042import { useLocationAndStorageInfos } from '../next-architecture/domain/business/locations' ;
4143import { useAccountsLocationsEndpointsAdapter } from '../next-architecture/ui/AccountsLocationsEndpointsAdapterProvider' ;
44+ import { useAccountsLocationsAndEndpoints } from '../next-architecture/domain/business/accounts' ;
45+ import { getLocationTypeKey } from '../utils/storageOptions' ;
4246import { useInstanceId } from '../next-architecture/ui/AuthProvider' ;
4347import { workflowListQuery } from '../queries' ;
4448import { useQueryParams , useRolePathName } from '../utils/hooks' ;
@@ -51,6 +55,7 @@ import ReplicationForm, {
5155 GeneralReplicationGroup ,
5256 disallowedPrefixes ,
5357 replicationSchema ,
58+ validateCRRFields ,
5459} from './ReplicationForm' ;
5560import {
5661 GeneralTransitionGroup ,
@@ -134,55 +139,136 @@ const CreateWorkflow = () => {
134139 locationInfos . status === 'success' &&
135140 locationInfos . value . location ?. isTransient ;
136141
142+ const { accountsLocationsAndEndpoints } = useAccountsLocationsAndEndpoints ( {
143+ accountsLocationsEndpointsAdapter,
144+ } ) ;
145+
146+ // Get CRR location names
147+ const crrLocationNames =
148+ accountsLocationsAndEndpoints ?. locations
149+ ?. filter (
150+ ( location ) =>
151+ getLocationTypeKey ( location ) === 'location-scality-crr-v1' ,
152+ )
153+ . map ( ( location ) => location . name ) || [ ] ;
154+
155+ // Custom resolver that preserves React Hook Form validation errors and runs Joi validation
156+ const customResolver : Resolver < Record < string , unknown > > = async (
157+ values ,
158+ context ,
159+ options ,
160+ ) => {
161+ // Check for existing React Hook Form validation errors from validate functions
162+ // These are stored in options.fields with their validation results
163+ const existingErrors : Record < string , Record < string , { type : string ; message : string } > > = { } ;
164+ let hasExistingErrors = false ;
165+
166+ // Check for CRR-related validation errors using the shared validation function
167+ const replication = values . replication as {
168+ destinationLocation ?: string [ ] ;
169+ destinationBucketName ?: string ;
170+ destinationRole ?: string ;
171+ } | undefined ;
172+
173+ if ( replication ) {
174+ const crrValidationErrors = validateCRRFields (
175+ replication . destinationLocation ,
176+ crrLocationNames ,
177+ replication . destinationBucketName ,
178+ replication . destinationRole ,
179+ ) ;
180+
181+ if ( crrValidationErrors . bucketNameError ) {
182+ existingErrors . replication = {
183+ ...existingErrors . replication ,
184+ destinationBucketName : {
185+ type : 'validate' ,
186+ message : crrValidationErrors . bucketNameError ,
187+ } ,
188+ } ;
189+ hasExistingErrors = true ;
190+ }
191+
192+ if ( crrValidationErrors . roleError ) {
193+ existingErrors . replication = {
194+ ...existingErrors . replication ,
195+ destinationRole : {
196+ type : 'validate' ,
197+ message : crrValidationErrors . roleError ,
198+ } ,
199+ } ;
200+ hasExistingErrors = true ;
201+ }
202+ }
203+
204+ // If we have existing validation errors, run Joi validation but preserve our errors
205+ const bucketName = ( values . replication as { sourceBucket ?: string } ) ?. sourceBucket ;
206+ const streams = replicationsQuery . data ?? [ ] ;
207+ const unallowedBucketName = streams . flatMap ( ( s ) => {
208+ const { prefix, bucketName } = s . source ;
209+ if ( ! prefix || prefix === '' ) return [ bucketName ] ;
210+ return [ ] ;
211+ } ) ;
212+ const prefixMandatory = ! ! streams . find ( ( s ) => {
213+ const { prefix } = s . source ;
214+ return prefix && prefix !== '' && bucketName === s . source . bucketName ;
215+ } ) ;
216+ const disPrefixes = disallowedPrefixes ( bucketName , streams ) ;
217+
218+ // Build Joi schema
219+ const schema = Joi . object ( {
220+ type : Joi . string ( ) . valid ( 'replication' , 'expiration' , 'transition' ) ,
221+ replication : Joi . when ( 'type' , {
222+ is : Joi . equal ( 'replication' ) ,
223+ then : Joi . object (
224+ replicationSchema (
225+ unallowedBucketName ,
226+ disPrefixes ,
227+ prefixMandatory ,
228+ ! ! isTransient ,
229+ ) ,
230+ ) ,
231+ otherwise : Joi . valid ( ) ,
232+ } ) ,
233+ transition : Joi . when ( 'type' , {
234+ is : Joi . equal ( 'transition' ) ,
235+ then : Joi . object ( transitionSchema ) ,
236+ otherwise : Joi . valid ( ) ,
237+ } ) ,
238+ expiration : Joi . when ( 'type' , {
239+ is : Joi . equal ( 'expiration' ) ,
240+ then : expirationSchema ,
241+ otherwise : Joi . valid ( ) ,
242+ } ) ,
243+ } ) ;
244+
245+ const joiValidator = joiResolver ( schema ) ;
246+ let joiResult ;
247+
248+ if ( [ 'replication' , 'transition' ] . includes ( values . type as string ) ) {
249+ joiResult = await joiValidator ( values , context , options as ResolverOptions < Record < string , unknown > > ) ;
250+ } else {
251+ const expiration = prepareExpirationQuery ( values . expiration as BucketWorkflowExpirationV1 ) ;
252+ joiResult = await joiValidator ( { ...values , expiration } , context , options as ResolverOptions < Record < string , unknown > > ) ;
253+ }
254+
255+ // Merge existing validation errors with Joi errors, giving priority to existing errors
256+ if ( hasExistingErrors ) {
257+ return {
258+ values : joiResult . values ,
259+ errors : {
260+ ...joiResult . errors ,
261+ ...existingErrors ,
262+ } ,
263+ } ;
264+ }
265+
266+ return joiResult ;
267+ } ;
268+
137269 const useFormMethods = useForm ( {
138270 mode : 'onChange' ,
139- resolver : async ( values , context , options ) => {
140- const bucketName = values . replication . sourceBucket ;
141- const streams = replicationsQuery . data ?? [ ] ;
142- const unallowedBucketName = streams . flatMap ( ( s ) => {
143- const { prefix, bucketName } = s . source ;
144- if ( ! prefix || prefix === '' ) return [ bucketName ] ;
145- return [ ] ;
146- } ) ;
147- const prefixMandatory = ! ! streams . find ( ( s ) => {
148- const { prefix } = s . source ;
149- return prefix && prefix !== '' && bucketName === s . source . bucketName ;
150- } ) ;
151- const disPrefixes = disallowedPrefixes ( bucketName , streams ) ;
152- const schema = Joi . object ( {
153- type : Joi . string ( ) . valid ( 'replication' , 'expiration' , 'transition' ) ,
154- replication : Joi . when ( 'type' , {
155- is : Joi . equal ( 'replication' ) ,
156- then : Joi . object (
157- replicationSchema (
158- unallowedBucketName ,
159- disPrefixes ,
160- prefixMandatory ,
161- ! ! isTransient ,
162- ) ,
163- ) ,
164- otherwise : Joi . valid ( ) ,
165- } ) ,
166- transition : Joi . when ( 'type' , {
167- is : Joi . equal ( 'transition' ) ,
168- then : Joi . object ( transitionSchema ) ,
169- otherwise : Joi . valid ( ) ,
170- } ) ,
171- expiration : Joi . when ( 'type' , {
172- is : Joi . equal ( 'expiration' ) ,
173- then : expirationSchema ,
174- otherwise : Joi . valid ( ) ,
175- } ) ,
176- } ) ;
177- const joiValidator = joiResolver ( schema ) ;
178- if ( [ 'replication' , 'transition' ] . includes ( values . type ) ) {
179- const validation = await joiValidator ( values , context , options ) ;
180- return validation ;
181- } else {
182- const expiration = prepareExpirationQuery ( values . expiration ) ;
183- return joiValidator ( { ...values , expiration } , context , options ) ;
184- }
185- } ,
271+ resolver : customResolver ,
186272 defaultValues : defaultFormValues ,
187273 } ) ;
188274
@@ -406,8 +492,8 @@ const CreateWorkflow = () => {
406492 < Select
407493 id = "type"
408494 onBlur = { onBlur }
409- value = { type }
410- onChange = { ( value ) => onChange ( value ) }
495+ value = { type as string }
496+ onChange = { ( value ) => onChange ( value as string ) }
411497 >
412498 < Select . Option
413499 value = "replication"
0 commit comments