refactor(r2aform): remove validationData [EE-5559] (#9045)

* refactor(r2aform): remove validationData [EE-5559]

* update doc

---------

Co-authored-by: testa113 <testa113>
pull/9064/head
Ali 2023-06-12 08:48:10 +12:00 committed by GitHub
parent 834ab7c158
commit 4a331b71e1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 53 additions and 66 deletions

View File

@ -1150,7 +1150,7 @@
load-balancer-enabled="ctrl.publishViaLoadBalancerEnabled()" load-balancer-enabled="ctrl.publishViaLoadBalancerEnabled()"
app-name="ctrl.formValues.Name" app-name="ctrl.formValues.Name"
selector="ctrl.formValues.Selector" 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" is-edit-mode="ctrl.state.isEdit"
></kube-services-form> ></kube-services-form>
<!-- kubernetes services options --> <!-- kubernetes services options -->
@ -1200,7 +1200,7 @@
load-balancer-enabled="ctrl.publishViaLoadBalancerEnabled()" load-balancer-enabled="ctrl.publishViaLoadBalancerEnabled()"
app-name="ctrl.formValues.Name" app-name="ctrl.formValues.Name"
selector="ctrl.formValues.Selector" 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" is-edit-mode="ctrl.state.isEdit"
></kube-services-form> ></kube-services-form>
<!-- kubernetes services options --> <!-- kubernetes services options -->

View File

@ -13,38 +13,41 @@ interface FormFieldProps<TValue> {
onChange(values: TValue): void; onChange(values: TValue): void;
values: TValue; values: TValue;
errors?: FormikErrors<TValue> | ArrayError<TValue>; errors?: FormikErrors<TValue> | ArrayError<TValue>;
validationContext?: object; // optional context to pass to yup validation, for example, external data
} }
type WithFormFieldProps<TProps, TValue> = TProps & FormFieldProps<TValue>; type WithFormFieldProps<TProps, TValue> = TProps & FormFieldProps<TValue>;
/** /**
* Utility function to use for wrapping react components with form validation * This utility function is used 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. * 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: * @example
* 1. the react component with r2a wrapping * // Usage in Angular view
* 2. an angularjs component that handles form validation * <component
* values="ctrl.values"
* on-change="ctrl.handleChange"
* validation-data="ctrl.validationData">
* </component>
*/ */
export function withFormValidation<TProps, TValue, TData = never>( export function withFormValidation<TProps, TValue, TData = never>(
ngModule: IModule, ngModule: IModule,
Component: ComponentType<WithFormFieldProps<TProps, TValue>>, Component: ComponentType<WithFormFieldProps<TProps, TValue>>,
componentName: string, componentName: string,
propNames: PropNames<TProps>[], propNames: PropNames<TProps>[],
schemaBuilder: (data?: TData) => SchemaOf<TValue> schemaBuilder: (validationData?: TData) => SchemaOf<TValue>
) { ) {
const reactComponentName = `react${_.upperFirst(componentName)}`; const reactComponentName = `react${_.upperFirst(componentName)}`;
ngModule ngModule
.component( .component(
reactComponentName, reactComponentName,
r2a(Component, [ r2a(Component, ['errors', 'onChange', 'values', ...propNames])
'errors',
'onChange',
'values',
'validationContext',
...propNames,
])
) )
.component( .component(
componentName, componentName,
@ -58,11 +61,11 @@ export function withFormValidation<TProps, TValue, TData = never>(
export function createFormValidationComponent<TFormModel, TData = never>( export function createFormValidationComponent<TFormModel, TData = never>(
componentName: string, componentName: string,
props: Array<string>, propNames: Array<string>,
schemaBuilder: (data?: TData) => SchemaOf<TFormModel> schemaBuilder: (validationData?: TData) => SchemaOf<TFormModel>
): IComponentOptions { ): IComponentOptions {
const kebabName = _.kebabCase(componentName); const kebabName = _.kebabCase(componentName);
const propsWithErrors = [...props, 'errors', 'values']; const propsWithErrors = [...propNames, 'errors', 'values'];
return { return {
template: `<ng-form name="$ctrl.form"> template: `<ng-form name="$ctrl.form">
@ -75,18 +78,13 @@ export function createFormValidationComponent<TFormModel, TData = never>(
</ng-form>`, </ng-form>`,
controller: createFormValidatorController(schemaBuilder), controller: createFormValidatorController(schemaBuilder),
bindings: Object.fromEntries( bindings: Object.fromEntries(
[ [...propsWithErrors, 'validationData', 'onChange'].map((p) => [p, '<'])
...propsWithErrors,
'validationData',
'onChange',
'validationContext',
].map((p) => [p, '<'])
), ),
}; };
} }
export function createFormValidatorController<TFormModel, TData = never>( function createFormValidatorController<TFormModel, TData = never>(
schemaBuilder: (data?: TData) => SchemaOf<TFormModel> schemaBuilder: (validationData?: TData) => SchemaOf<TFormModel>
) { ) {
return class FormValidatorController { return class FormValidatorController {
errors?: FormikErrors<TFormModel> = {}; errors?: FormikErrors<TFormModel> = {};
@ -99,8 +97,6 @@ export function createFormValidatorController<TFormModel, TData = never>(
validationData?: TData; validationData?: TData;
validationContext?: object;
onChange?: (value: TFormModel) => void; onChange?: (value: TFormModel) => void;
/* @ngInject */ /* @ngInject */
@ -114,18 +110,17 @@ export function createFormValidatorController<TFormModel, TData = never>(
async handleChange(newValues: TFormModel) { async handleChange(newValues: TFormModel) {
return this.$async(async () => { return this.$async(async () => {
this.onChange?.(newValues); 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 () => { return this.$async(async () => {
this.form?.$setValidity('form', true, this.form); this.form?.$setValidity('form', true, this.form);
this.errors = await validateForm<TFormModel>( this.errors = await validateForm<TFormModel>(
() => schemaBuilder(this.validationData), () => schemaBuilder(this.validationData),
value, value
validationContext
); );
if (this.errors && Object.keys(this.errors).length > 0) { if (this.errors && Object.keys(this.errors).length > 0) {
@ -136,10 +131,7 @@ export function createFormValidatorController<TFormModel, TData = never>(
async $onChanges(changes: { values?: { currentValue: TFormModel } }) { async $onChanges(changes: { values?: { currentValue: TFormModel } }) {
if (changes.values) { if (changes.values) {
await this.runValidation( await this.runValidation(changes.values.currentValue);
changes.values.currentValue,
this.validationContext
);
} }
} }
}; };

View File

@ -3,8 +3,7 @@ import { SchemaOf } from 'yup';
export async function validateForm<T>( export async function validateForm<T>(
schemaBuilder: () => SchemaOf<T>, schemaBuilder: () => SchemaOf<T>,
formValues: T, formValues: T
validationContext?: object
) { ) {
const validationSchema = schemaBuilder(); const validationSchema = schemaBuilder();
@ -12,8 +11,6 @@ export async function validateForm<T>(
await validationSchema.validate(formValues, { await validationSchema.validate(formValues, {
strict: true, strict: true,
abortEarly: false, abortEarly: false,
// workaround to access all parents for nested fields. See clusterIpFormValidation for a use case.
context: { formValues, validationContext },
}); });
return undefined; return undefined;
} catch (error) { } catch (error) {

View File

@ -340,7 +340,9 @@ type NodePortValidationContext = {
formServices: ServiceFormValues[]; formServices: ServiceFormValues[];
}; };
export function kubeServicesValidation(): SchemaOf<ServiceFormValues[]> { export function kubeServicesValidation(
validationData?: NodePortValidationContext
): SchemaOf<ServiceFormValues[]> {
return array( return array(
object({ object({
Headless: boolean().required(), Headless: boolean().required(),
@ -367,20 +369,20 @@ export function kubeServicesValidation(): SchemaOf<ServiceFormValues[]> {
.test( .test(
'service-port-is-unique', 'service-port-is-unique',
'Service port number must be unique.', 'Service port number must be unique.',
// eslint-disable-next-line func-names (servicePort, context) => {
function (servicePort, context) {
// test for duplicate service ports within this service. // test for duplicate service ports within this service.
// yup gives access to context.parent which gives one ServicePort object. // yup gives access to context.parent which gives one ServicePort object.
// yup also gives access to all form values through this.options.context. // 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. // 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, // 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. // that's stored in the ServicePort object, then getting all Ports in the service.
if (servicePort === undefined) { if (servicePort === undefined || validationData === undefined) {
return true; return true;
} }
const { formServices } = validationData;
const matchingService = getServiceForPort( const matchingService = getServiceForPort(
context.parent as ServicePort, context.parent as ServicePort,
this.options.context?.formValues as ServiceFormValues[] formServices
); );
if (matchingService === undefined) { if (matchingService === undefined) {
return true; return true;
@ -406,14 +408,14 @@ export function kubeServicesValidation(): SchemaOf<ServiceFormValues[]> {
.test( .test(
'node-port-is-unique-in-service', 'node-port-is-unique-in-service',
'Node port is already used in this service.', 'Node port is already used in this service.',
// eslint-disable-next-line func-names (nodePort, context) => {
function (nodePort, context) { if (nodePort === undefined || validationData === undefined) {
if (nodePort === undefined) {
return true; return true;
} }
const { formServices } = validationData;
const matchingService = getServiceForPort( const matchingService = getServiceForPort(
context.parent as ServicePort, context.parent as ServicePort,
this.options.context?.formValues as ServiceFormValues[] formServices
); );
if ( if (
matchingService === undefined || matchingService === undefined ||
@ -432,15 +434,11 @@ export function kubeServicesValidation(): SchemaOf<ServiceFormValues[]> {
.test( .test(
'node-port-is-unique-in-cluster', 'node-port-is-unique-in-cluster',
'Node port is already used.', 'Node port is already used.',
// eslint-disable-next-line func-names (nodePort, context) => {
function (nodePort, context) { if (nodePort === undefined || validationData === undefined) {
if (nodePort === undefined) {
return true; return true;
} }
const { nodePortServices } = this.options.context const { formServices, nodePortServices } = validationData;
?.validationContext as NodePortValidationContext;
const formServices = this.options.context
?.formValues as ServiceFormValues[];
const matchingService = getServiceForPort( const matchingService = getServiceForPort(
context.parent as ServicePort, context.parent as ServicePort,
formServices formServices
@ -476,21 +474,21 @@ export function kubeServicesValidation(): SchemaOf<ServiceFormValues[]> {
.map((formServicePorts) => formServicePorts.nodePort); .map((formServicePorts) => formServicePorts.nodePort);
return ( return (
!clusterNodePortsWithoutFormServices.includes(nodePort) && // node port is not in the cluster services that aren't in the application form !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( .test(
'node-port-minimum', 'node-port-minimum',
'Nodeport number must be inside the range 30000-32767 or blank for system allocated.', 'Nodeport number must be inside the range 30000-32767 or blank for system allocated.',
// eslint-disable-next-line func-names (nodePort, context) => {
function (nodePort, context) { if (nodePort === undefined || validationData === undefined) {
if (nodePort === undefined) {
return true; return true;
} }
const { formServices } = validationData;
const matchingService = getServiceForPort( const matchingService = getServiceForPort(
context.parent as ServicePort, context.parent as ServicePort,
this.options.context?.formValues as ServiceFormValues[] formServices
); );
if ( if (
!matchingService || !matchingService ||
@ -505,14 +503,14 @@ export function kubeServicesValidation(): SchemaOf<ServiceFormValues[]> {
.test( .test(
'node-port-maximum', 'node-port-maximum',
'Nodeport number must be inside the range 30000-32767 or blank for system allocated.', 'Nodeport number must be inside the range 30000-32767 or blank for system allocated.',
// eslint-disable-next-line func-names (nodePort, context) => {
function (nodePort, context) { if (nodePort === undefined || validationData === undefined) {
if (nodePort === undefined) {
return true; return true;
} }
const { formServices } = validationData;
const matchingService = getServiceForPort( const matchingService = getServiceForPort(
context.parent as ServicePort, context.parent as ServicePort,
this.options.context?.formValues as ServiceFormValues[] formServices
); );
if ( if (
!matchingService || !matchingService ||