Merge pull request #11176 from stephane-mori/customFieldExtensionValidation

Allow custom field extension validation
This commit is contained in:
Ben Lambert
2022-05-02 13:42:51 +02:00
committed by GitHub
5 changed files with 167 additions and 59 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-scaffolder': patch
---
Allow validation for custom field extension with type object
@@ -13,23 +13,22 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { JsonObject, JsonValue } from '@backstage/types';
import { LinearProgress } from '@material-ui/core';
import { FormValidation, IChangeEvent } from '@rjsf/core';
import { IChangeEvent } from '@rjsf/core';
import qs from 'qs';
import React, { useCallback, useContext, useState } from 'react';
import { Navigate, useNavigate } from 'react-router';
import { useParams } from 'react-router-dom';
import useAsync from 'react-use/lib/useAsync';
import { scaffolderApiRef } from '../../api';
import { CustomFieldValidator, FieldExtensionOptions } from '../../extensions';
import { FieldExtensionOptions } from '../../extensions';
import { SecretsContext } from '../secrets/SecretsContext';
import { rootRouteRef, scaffolderTaskRouteRef } from '../../routes';
import { MultistepJsonForm } from '../MultistepJsonForm';
import { createValidator } from './createValidator';
import { Content, Header, InfoCard, Page } from '@backstage/core-components';
import {
ApiHolder,
errorApiRef,
useApi,
useApiHolder,
@@ -46,60 +45,6 @@ const useTemplateParameterSchema = (templateRef: string) => {
return { schema: value, loading, error };
};
function isObject(obj: unknown): obj is JsonObject {
return typeof obj === 'object' && obj !== null && !Array.isArray(obj);
}
export const createValidator = (
rootSchema: JsonObject,
validators: Record<string, undefined | CustomFieldValidator<unknown>>,
context: {
apiHolder: ApiHolder;
},
) => {
function validate(
schema: JsonObject,
formData: JsonObject,
errors: FormValidation,
) {
const schemaProps = schema.properties;
if (!isObject(schemaProps)) {
return;
}
for (const [key, propData] of Object.entries(formData)) {
const propValidation = errors[key];
if (isObject(propData)) {
const propSchemaProps = schemaProps[key];
if (isObject(propSchemaProps)) {
validate(
propSchemaProps,
propData as JsonObject,
propValidation as FormValidation,
);
}
} else {
const propSchema = schemaProps[key];
const fieldName =
isObject(propSchema) && (propSchema['ui:field'] as string);
if (fieldName && typeof validators[fieldName] === 'function') {
validators[fieldName]!(
propData as JsonValue,
propValidation,
context,
);
}
}
}
}
return (formData: JsonObject, errors: FormValidation) => {
validate(rootSchema, formData, errors);
return errors;
};
};
export const TemplatePage = ({
customFieldExtensions = [],
}: {
@@ -0,0 +1,74 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { createValidator } from './createValidator';
import { CustomFieldValidator } from '../../extensions';
import { ApiHolder } from '@backstage/core-plugin-api';
import { FormValidation } from '@rjsf/core';
describe('createValidator', () => {
const validators: Record<string, undefined | CustomFieldValidator<unknown>> =
{
CustomPicker: (value, validation, _context) => {
// @ts-ignore
if (!value || !value.value) {
validation.addError('Error !');
}
},
};
const apiHolderMock: jest.Mocked<ApiHolder> = {
get: jest.fn().mockImplementation(() => {
return null;
}),
};
const context = {
apiHolder: apiHolderMock,
};
it('Should call validator for object property from a custom field extension', () => {
/* GIVEN */
const rootSchema = {
title: 'Title',
properties: {
p1: {
title: 'PropertyOn',
type: 'object',
'ui:field': 'CustomPicker',
},
},
};
const validator = createValidator(rootSchema, validators, context);
const formData = {
p1: {},
};
const errors = {
addError: jest.fn(),
p1: {
addError: jest.fn(),
} as unknown as FormValidation,
} as unknown as FormValidation;
/* WHEN */
const result = validator(formData, errors);
/* THEN */
expect(result).not.toBeNull();
expect(result.p1.addError).toBeCalledTimes(1);
});
});
@@ -0,0 +1,83 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { CustomFieldValidator } from '../../extensions';
import { FormValidation } from '@rjsf/core';
import { JsonObject, JsonValue } from '@backstage/types';
import { ApiHolder } from '@backstage/core-plugin-api';
function isObject(obj: unknown): obj is JsonObject {
return typeof obj === 'object' && obj !== null && !Array.isArray(obj);
}
export const createValidator = (
rootSchema: JsonObject,
validators: Record<string, undefined | CustomFieldValidator<unknown>>,
context: {
apiHolder: ApiHolder;
},
) => {
function validate(
schema: JsonObject,
formData: JsonObject,
errors: FormValidation,
) {
const schemaProps = schema.properties;
const customObject = schema.type === 'object' && schemaProps === undefined;
if (!isObject(schemaProps) && !customObject) {
return;
}
if (schemaProps) {
for (const [key, propData] of Object.entries(formData)) {
const propValidation = errors[key];
if (isObject(propData)) {
const propSchemaProps = schemaProps[key];
if (isObject(propSchemaProps)) {
validate(
propSchemaProps,
propData as JsonObject,
propValidation as FormValidation,
);
}
} else {
const propSchema = schemaProps[key];
const fieldName =
isObject(propSchema) && (propSchema['ui:field'] as string);
if (fieldName && typeof validators[fieldName] === 'function') {
validators[fieldName]!(
propData as JsonValue,
propValidation,
context,
);
}
}
}
} else if (customObject) {
const fieldName = schema['ui:field'] as string;
if (fieldName && typeof validators[fieldName] === 'function') {
validators[fieldName]!(formData, errors, context);
}
}
}
return (formData: JsonObject, errors: FormValidation) => {
validate(rootSchema, formData, errors);
return errors;
};
};
@@ -13,4 +13,5 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { createValidator, TemplatePage } from './TemplatePage';
export { TemplatePage } from './TemplatePage';
export { createValidator } from './createValidator';