diff --git a/app/kubernetes/views/applications/create/createApplication.html b/app/kubernetes/views/applications/create/createApplication.html index ca9546c3f..cb77338f0 100644 --- a/app/kubernetes/views/applications/create/createApplication.html +++ b/app/kubernetes/views/applications/create/createApplication.html @@ -1150,7 +1150,7 @@ load-balancer-enabled="ctrl.publishViaLoadBalancerEnabled()" app-name="ctrl.formValues.Name" selector="ctrl.formValues.Selector" - validation-context="{nodePortServices: ctrl.state.nodePortServices}" + validation-data="{nodePortServices: ctrl.state.nodePortServices, formServices: ctrl.formValues.Services}" is-edit-mode="ctrl.state.isEdit" > @@ -1200,7 +1200,7 @@ load-balancer-enabled="ctrl.publishViaLoadBalancerEnabled()" app-name="ctrl.formValues.Name" selector="ctrl.formValues.Selector" - validation-context="{nodePortServices: ctrl.state.nodePortServices}" + validation-data="{nodePortServices: ctrl.state.nodePortServices, formServices: ctrl.formValues.Services}" is-edit-mode="ctrl.state.isEdit" > diff --git a/app/react-tools/withFormValidation.ts b/app/react-tools/withFormValidation.ts index 07d57ad1e..ebfcdd201 100644 --- a/app/react-tools/withFormValidation.ts +++ b/app/react-tools/withFormValidation.ts @@ -13,38 +13,41 @@ interface FormFieldProps { onChange(values: TValue): void; values: TValue; errors?: FormikErrors | ArrayError; - validationContext?: object; // optional context to pass to yup validation, for example, external data } type WithFormFieldProps = TProps & FormFieldProps; /** - * Utility function to use for wrapping react components with form validation - * when used inside an angular form, it will set the form to invalid if the component values are invalid. + * This utility function is used for wrapping React components with form validation. + * When used inside an Angular form, it sets the form to invalid if the component values are invalid. + * This function registers two AngularJS components: + * 1. The React component with r2a wrapping: + * - `onChange` and `values` must be manually passed to the React component from an Angular view. + * - `errors` will be automatically passed to the React component and updated by the validation component. + * 2. An AngularJS component that handles form validation, based on a yup validation schema: + * - `validationData` can optionally be passed to the React component from an Angular view, which can be used in validation. * - * this registers two angularjs components: - * 1. the react component with r2a wrapping - * 2. an angularjs component that handles form validation + * @example + * // Usage in Angular view + * + * */ export function withFormValidation( ngModule: IModule, Component: ComponentType>, componentName: string, propNames: PropNames[], - schemaBuilder: (data?: TData) => SchemaOf + schemaBuilder: (validationData?: TData) => SchemaOf ) { const reactComponentName = `react${_.upperFirst(componentName)}`; ngModule .component( reactComponentName, - r2a(Component, [ - 'errors', - 'onChange', - 'values', - 'validationContext', - ...propNames, - ]) + r2a(Component, ['errors', 'onChange', 'values', ...propNames]) ) .component( componentName, @@ -58,11 +61,11 @@ export function withFormValidation( export function createFormValidationComponent( componentName: string, - props: Array, - schemaBuilder: (data?: TData) => SchemaOf + propNames: Array, + schemaBuilder: (validationData?: TData) => SchemaOf ): IComponentOptions { const kebabName = _.kebabCase(componentName); - const propsWithErrors = [...props, 'errors', 'values']; + const propsWithErrors = [...propNames, 'errors', 'values']; return { template: ` @@ -75,18 +78,13 @@ export function createFormValidationComponent( `, controller: createFormValidatorController(schemaBuilder), bindings: Object.fromEntries( - [ - ...propsWithErrors, - 'validationData', - 'onChange', - 'validationContext', - ].map((p) => [p, '<']) + [...propsWithErrors, 'validationData', 'onChange'].map((p) => [p, '<']) ), }; } -export function createFormValidatorController( - schemaBuilder: (data?: TData) => SchemaOf +function createFormValidatorController( + schemaBuilder: (validationData?: TData) => SchemaOf ) { return class FormValidatorController { errors?: FormikErrors = {}; @@ -99,8 +97,6 @@ export function createFormValidatorController( validationData?: TData; - validationContext?: object; - onChange?: (value: TFormModel) => void; /* @ngInject */ @@ -114,18 +110,17 @@ export function createFormValidatorController( async handleChange(newValues: TFormModel) { return this.$async(async () => { this.onChange?.(newValues); - await this.runValidation(newValues, this.validationContext); + await this.runValidation(newValues); }); } - async runValidation(value: TFormModel, validationContext?: object) { + async runValidation(value: TFormModel) { return this.$async(async () => { this.form?.$setValidity('form', true, this.form); this.errors = await validateForm( () => schemaBuilder(this.validationData), - value, - validationContext + value ); if (this.errors && Object.keys(this.errors).length > 0) { @@ -136,10 +131,7 @@ export function createFormValidatorController( async $onChanges(changes: { values?: { currentValue: TFormModel } }) { if (changes.values) { - await this.runValidation( - changes.values.currentValue, - this.validationContext - ); + await this.runValidation(changes.values.currentValue); } } }; diff --git a/app/react/components/form-components/validate-form.ts b/app/react/components/form-components/validate-form.ts index 3d65d2bee..0d3e54450 100644 --- a/app/react/components/form-components/validate-form.ts +++ b/app/react/components/form-components/validate-form.ts @@ -3,8 +3,7 @@ import { SchemaOf } from 'yup'; export async function validateForm( schemaBuilder: () => SchemaOf, - formValues: T, - validationContext?: object + formValues: T ) { const validationSchema = schemaBuilder(); @@ -12,8 +11,6 @@ export async function validateForm( await validationSchema.validate(formValues, { strict: true, abortEarly: false, - // workaround to access all parents for nested fields. See clusterIpFormValidation for a use case. - context: { formValues, validationContext }, }); return undefined; } catch (error) { diff --git a/app/react/kubernetes/applications/CreateView/application-services/KubeServicesForm.tsx b/app/react/kubernetes/applications/CreateView/application-services/KubeServicesForm.tsx index b78a46795..7b43113cb 100644 --- a/app/react/kubernetes/applications/CreateView/application-services/KubeServicesForm.tsx +++ b/app/react/kubernetes/applications/CreateView/application-services/KubeServicesForm.tsx @@ -340,7 +340,9 @@ type NodePortValidationContext = { formServices: ServiceFormValues[]; }; -export function kubeServicesValidation(): SchemaOf { +export function kubeServicesValidation( + validationData?: NodePortValidationContext +): SchemaOf { return array( object({ Headless: boolean().required(), @@ -367,20 +369,20 @@ export function kubeServicesValidation(): SchemaOf { .test( 'service-port-is-unique', 'Service port number must be unique.', - // eslint-disable-next-line func-names - function (servicePort, context) { + (servicePort, context) => { // test for duplicate service ports within this service. // yup gives access to context.parent which gives one ServicePort object. // yup also gives access to all form values through this.options.context. // Unfortunately, it doesn't give direct access to all Ports within the current service. // To find all ports in the service for validation, I'm filtering the services by the service name, // that's stored in the ServicePort object, then getting all Ports in the service. - if (servicePort === undefined) { + if (servicePort === undefined || validationData === undefined) { return true; } + const { formServices } = validationData; const matchingService = getServiceForPort( context.parent as ServicePort, - this.options.context?.formValues as ServiceFormValues[] + formServices ); if (matchingService === undefined) { return true; @@ -406,14 +408,14 @@ export function kubeServicesValidation(): SchemaOf { .test( 'node-port-is-unique-in-service', 'Node port is already used in this service.', - // eslint-disable-next-line func-names - function (nodePort, context) { - if (nodePort === undefined) { + (nodePort, context) => { + if (nodePort === undefined || validationData === undefined) { return true; } + const { formServices } = validationData; const matchingService = getServiceForPort( context.parent as ServicePort, - this.options.context?.formValues as ServiceFormValues[] + formServices ); if ( matchingService === undefined || @@ -432,15 +434,11 @@ export function kubeServicesValidation(): SchemaOf { .test( 'node-port-is-unique-in-cluster', 'Node port is already used.', - // eslint-disable-next-line func-names - function (nodePort, context) { - if (nodePort === undefined) { + (nodePort, context) => { + if (nodePort === undefined || validationData === undefined) { return true; } - const { nodePortServices } = this.options.context - ?.validationContext as NodePortValidationContext; - const formServices = this.options.context - ?.formValues as ServiceFormValues[]; + const { formServices, nodePortServices } = validationData; const matchingService = getServiceForPort( context.parent as ServicePort, formServices @@ -476,21 +474,21 @@ export function kubeServicesValidation(): SchemaOf { .map((formServicePorts) => formServicePorts.nodePort); return ( !clusterNodePortsWithoutFormServices.includes(nodePort) && // node port is not in the cluster services that aren't in the application form - !formNodePortsWithoutCurrentService.includes(nodePort) // node port is not in the current form, excluding the current service + !formNodePortsWithoutCurrentService.includes(nodePort) // and the node port is not in the current form, excluding the current service ); } ) .test( 'node-port-minimum', 'Nodeport number must be inside the range 30000-32767 or blank for system allocated.', - // eslint-disable-next-line func-names - function (nodePort, context) { - if (nodePort === undefined) { + (nodePort, context) => { + if (nodePort === undefined || validationData === undefined) { return true; } + const { formServices } = validationData; const matchingService = getServiceForPort( context.parent as ServicePort, - this.options.context?.formValues as ServiceFormValues[] + formServices ); if ( !matchingService || @@ -505,14 +503,14 @@ export function kubeServicesValidation(): SchemaOf { .test( 'node-port-maximum', 'Nodeport number must be inside the range 30000-32767 or blank for system allocated.', - // eslint-disable-next-line func-names - function (nodePort, context) { - if (nodePort === undefined) { + (nodePort, context) => { + if (nodePort === undefined || validationData === undefined) { return true; } + const { formServices } = validationData; const matchingService = getServiceForPort( context.parent as ServicePort, - this.options.context?.formValues as ServiceFormValues[] + formServices ); if ( !matchingService ||