Merge pull request #13428 from backstage/blam/rjsfv5

scaffolder: update to `react-jsonschema-form` `v5`
This commit is contained in:
Ben Lambert
2022-10-11 13:03:50 +01:00
committed by GitHub
15 changed files with 260 additions and 45 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-scaffolder': patch
---
Bump to `react-jsonschema-form@v5-beta` for the `NextRouter` under `@alpha` exports
-1
View File
@@ -83,7 +83,6 @@
},
"devDependencies": {
"@backstage/test-utils": "workspace:^",
"@rjsf/core": "^3.2.1",
"@testing-library/cypress": "^8.0.2",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^12.1.3",
+44
View File
@@ -15,7 +15,9 @@ import { Extension } from '@backstage/core-plugin-api';
import { ExternalRouteRef } from '@backstage/core-plugin-api';
import { FetchApi } from '@backstage/core-plugin-api';
import { FieldProps } from '@rjsf/core';
import { FieldProps as FieldProps_2 } from '@rjsf/utils';
import { FieldValidation } from '@rjsf/core';
import { FieldValidation as FieldValidation_2 } from '@rjsf/utils';
import type { FormProps } from '@rjsf/core';
import { IdentityApi } from '@backstage/core-plugin-api';
import { JsonObject } from '@backstage/types';
@@ -29,6 +31,16 @@ import { ScmIntegrationRegistry } from '@backstage/integration';
import { TaskSpec } from '@backstage/plugin-scaffolder-common';
import { TaskStep } from '@backstage/plugin-scaffolder-common';
import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common';
import { UIOptionsType } from '@rjsf/utils';
import { UiSchema } from '@rjsf/utils';
// @alpha
export function createNextScaffolderFieldExtension<
TReturnValue = unknown,
TInputProps extends UIOptionsType = {},
>(
options: NextFieldExtensionOptions<TReturnValue, TInputProps>,
): Extension<NextFieldExtensionComponentProps<TReturnValue, TInputProps>>;
// @public
export function createScaffolderFieldExtension<
@@ -155,6 +167,38 @@ export type LogEvent = {
taskId: string;
};
// @alpha
export type NextCustomFieldValidator<TFieldReturnValue> = (
data: TFieldReturnValue,
field: FieldValidation_2,
context: {
apiHolder: ApiHolder;
},
) => void | Promise<void>;
// @alpha
export interface NextFieldExtensionComponentProps<
TFieldReturnValue,
TUiOptions = {},
> extends PropsWithChildren<FieldProps_2<TFieldReturnValue>> {
// (undocumented)
uiSchema?: UiSchema<TFieldReturnValue> & {
'ui:options'?: TUiOptions & UIOptionsType;
};
}
// @alpha
export type NextFieldExtensionOptions<
TFieldReturnValue = unknown,
TInputProps = unknown,
> = {
name: string;
component: (
props: NextFieldExtensionComponentProps<TFieldReturnValue, TInputProps>,
) => JSX.Element | null;
validation?: NextCustomFieldValidator<TFieldReturnValue>;
};
// @alpha
export type NextRouterProps = {
components?: {
+4
View File
@@ -55,7 +55,11 @@
"@material-ui/lab": "4.0.0-alpha.57",
"@react-hookz/web": "^15.0.0",
"@rjsf/core": "^3.2.1",
"@rjsf/core-v5": "npm:@rjsf/core@^5.0.0-beta.10",
"@rjsf/material-ui": "^3.2.1",
"@rjsf/material-ui-v5": "npm:@rjsf/material-ui@^5.0.0-beta.10",
"@rjsf/utils": "^5.0.0-beta.10",
"@rjsf/validator-ajv8": "^5.0.0-beta.10",
"@types/json-schema": "^7.0.9",
"@uiw/react-codemirror": "^4.9.3",
"classnames": "^2.2.6",
@@ -19,8 +19,12 @@ import {
CustomFieldValidator,
FieldExtensionOptions,
FieldExtensionComponentProps,
NextCustomFieldValidator,
NextFieldExtensionOptions,
NextFieldExtensionComponentProps,
} from './types';
import { Extension, attachComponentData } from '@backstage/core-plugin-api';
import { UIOptionsType } from '@rjsf/utils';
export const FIELD_EXTENSION_WRAPPER_KEY = 'scaffolder.extensions.wrapper.v1';
export const FIELD_EXTENSION_KEY = 'scaffolder.extensions.field.v1';
@@ -58,6 +62,32 @@ export function createScaffolderFieldExtension<
};
}
/**
* Method for creating field extensions that can be used in the scaffolder
* frontend form.
* @alpha
*/
export function createNextScaffolderFieldExtension<
TReturnValue = unknown,
TInputProps extends UIOptionsType = {},
>(
options: NextFieldExtensionOptions<TReturnValue, TInputProps>,
): Extension<NextFieldExtensionComponentProps<TReturnValue, TInputProps>> {
return {
expose() {
const FieldExtensionDataHolder: any = () => null;
attachComponentData(
FieldExtensionDataHolder,
FIELD_EXTENSION_KEY,
options,
);
return FieldExtensionDataHolder;
},
};
}
/**
* The Wrapping component for defining fields extensions inside
*
@@ -76,6 +106,9 @@ export type {
CustomFieldValidator,
FieldExtensionOptions,
FieldExtensionComponentProps,
NextCustomFieldValidator,
NextFieldExtensionOptions,
NextFieldExtensionComponentProps,
};
export { DEFAULT_SCAFFOLDER_FIELD_EXTENSIONS } from './default';
@@ -15,6 +15,14 @@
*/
import { ApiHolder } from '@backstage/core-plugin-api';
import { FieldValidation, FieldProps } from '@rjsf/core';
import { PropsWithChildren } from 'react';
import {
UIOptionsType,
FieldProps as FieldPropsV5,
UiSchema as UiSchemaV5,
FieldValidation as FieldValidationV5,
} from '@rjsf/utils';
/**
* Field validation type for Custom Field Extensions.
@@ -58,3 +66,45 @@ export interface FieldExtensionComponentProps<
'ui:options'?: TUiOptions;
};
}
/**
* Type for Field Extension Props for RJSF v5
*
* @alpha
*/
export interface NextFieldExtensionComponentProps<
TFieldReturnValue,
TUiOptions = {},
> extends PropsWithChildren<FieldPropsV5<TFieldReturnValue>> {
uiSchema?: UiSchemaV5<TFieldReturnValue> & {
'ui:options'?: TUiOptions & UIOptionsType;
};
}
/**
* Field validation type for Custom Field Extensions.
*
* @alpha
*/
export type NextCustomFieldValidator<TFieldReturnValue> = (
data: TFieldReturnValue,
field: FieldValidationV5,
context: { apiHolder: ApiHolder },
) => void | Promise<void>;
/**
* Type for the Custom Field Extension with the
* name and components and validation function.
*
* @alpha
*/
export type NextFieldExtensionOptions<
TFieldReturnValue = unknown,
TInputProps = unknown,
> = {
name: string;
component: (
props: NextFieldExtensionComponentProps<TFieldReturnValue, TInputProps>,
) => JSX.Element | null;
validation?: NextCustomFieldValidator<TFieldReturnValue>;
};
+7 -1
View File
@@ -59,11 +59,17 @@ export {
RepoUrlPickerFieldExtension,
ScaffolderPage,
scaffolderPlugin,
NextScaffolderPage,
} from './plugin';
export * from './components';
export type { TaskPageProps } from './components/TaskPage';
/** next exports */
export { NextScaffolderPage } from './plugin';
export type { NextRouterProps } from './next';
export type { TemplateGroupFilter } from './next';
export {
createNextScaffolderFieldExtension,
type NextCustomFieldValidator,
type NextFieldExtensionOptions,
type NextFieldExtensionComponentProps,
} from './extensions';
@@ -18,10 +18,11 @@ import { Routes, Route, useOutlet } from 'react-router';
import { TemplateListPage } from '../TemplateListPage';
import { TemplateWizardPage } from '../TemplateWizardPage';
import {
FieldExtensionOptions,
FIELD_EXTENSION_WRAPPER_KEY,
FIELD_EXTENSION_KEY,
DEFAULT_SCAFFOLDER_FIELD_EXTENSIONS,
NextFieldExtensionOptions,
FieldExtensionOptions,
} from '../../extensions';
import { useElementFilter } from '@backstage/core-plugin-api';
@@ -60,7 +61,7 @@ export const Router = (props: PropsWithChildren<NextRouterProps>) => {
.selectByComponentData({
key: FIELD_EXTENSION_WRAPPER_KEY,
})
.findComponentData<FieldExtensionOptions>({
.findComponentData<FieldExtensionOptions | NextFieldExtensionOptions>({
key: FIELD_EXTENSION_KEY,
}),
);
@@ -73,7 +74,7 @@ export const Router = (props: PropsWithChildren<NextRouterProps>) => {
customFieldExtension => customFieldExtension.name === name,
),
),
];
] as NextFieldExtensionOptions[];
return (
<Routes>
@@ -22,14 +22,15 @@ import {
Button,
makeStyles,
} from '@material-ui/core';
import { FieldValidation, withTheme } from '@rjsf/core';
import { Theme as MuiTheme } from '@rjsf/material-ui';
import { withTheme } from '@rjsf/core-v5';
import { ErrorSchema, FieldValidation } from '@rjsf/utils';
import React, { useMemo, useState } from 'react';
import { FieldExtensionOptions } from '../../../extensions';
import { NextFieldExtensionOptions } from '../../../extensions';
import { TemplateParameterSchema } from '../../../types';
import { createAsyncValidators } from './createAsyncValidators';
import { useTemplateSchema } from './useTemplateSchema';
import { ReviewState } from './ReviewState';
import validator from '@rjsf/validator-ajv8';
const useStyles = makeStyles(theme => ({
backButton: {
@@ -48,10 +49,13 @@ const useStyles = makeStyles(theme => ({
export interface StepperProps {
manifest: TemplateParameterSchema;
extensions: FieldExtensionOptions<any, any>[];
extensions: NextFieldExtensionOptions<any, any>[];
}
const Form = withTheme(MuiTheme);
// TODO(blam): We require here, as the types in this package depend on @rjsf/core explicitly
// which is what we're using here as the default types, it needs to depend on @rjsf/core-v5 because
// of the re-writing we're doing. Once we've migrated, we can import this the exact same as before.
const Form = withTheme(require('@rjsf/material-ui-v5').Theme);
export const Stepper = (props: StepperProps) => {
const { steps } = useTemplateSchema(props.manifest);
@@ -92,9 +96,9 @@ export const Stepper = (props: StepperProps) => {
const returnedValidation = await validation(formData);
const hasErrors = Object.values(returnedValidation).some(i => {
return i.__errors.length > 0;
});
const hasErrors = Object.values(returnedValidation).some(
i => i.__errors?.length,
);
if (hasErrors) {
setErrors(returnedValidation);
@@ -124,7 +128,8 @@ export const Stepper = (props: StepperProps) => {
<div className={styles.formWrapper}>
{activeStep < steps.length ? (
<Form
extraErrors={errors}
validator={validator}
extraErrors={errors as unknown as ErrorSchema}
formData={formState}
schema={steps[activeStep].schema}
uiSchema={steps[activeStep].uiSchema}
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { JsonObject } from '@backstage/types';
import { CustomFieldValidator } from '../../../extensions';
import { NextCustomFieldValidator } from '../../../extensions';
import { createAsyncValidators } from './createAsyncValidators';
describe('createAsyncValidators', () => {
@@ -79,13 +79,16 @@ describe('createAsyncValidators', () => {
},
};
const NameField: CustomFieldValidator<string> = (value, { addError }) => {
const NameField: NextCustomFieldValidator<string> = (
value,
{ addError },
) => {
if (!value) {
addError('something is broken here!');
}
};
const AddressField: CustomFieldValidator<{
const AddressField: NextCustomFieldValidator<{
street?: string;
postcode?: string;
}> = (value, { addError }) => {
@@ -101,8 +104,8 @@ describe('createAsyncValidators', () => {
const validate = createAsyncValidators(
schema,
{
NameField: NameField as CustomFieldValidator<unknown>,
AddressField: AddressField as CustomFieldValidator<unknown>,
NameField: NameField as NextCustomFieldValidator<unknown>,
AddressField: AddressField as NextCustomFieldValidator<unknown>,
},
{
apiHolder: { get: jest.fn() },
@@ -14,16 +14,16 @@
* limitations under the License.
*/
import { FieldValidation } from '@rjsf/core';
import { FieldValidation } from '@rjsf/utils';
import { JsonObject } from '@backstage/types';
import { ApiHolder } from '@backstage/core-plugin-api';
import { CustomFieldValidator } from '../../../extensions';
import { NextCustomFieldValidator } from '../../../extensions';
import { Draft07 as JSONSchema } from 'json-schema-library';
import { createFieldValidation } from './schema';
export const createAsyncValidators = (
rootSchema: JsonObject,
validators: Record<string, undefined | CustomFieldValidator<unknown>>,
validators: Record<string, undefined | NextCustomFieldValidator<unknown>>,
context: {
apiHolder: ApiHolder;
},
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { JsonObject } from '@backstage/types';
import { FieldValidation, UiSchema } from '@rjsf/core';
import { FieldValidation, UiSchema } from '@rjsf/utils';
function isObject(value: unknown): value is JsonObject {
return typeof value === 'object' && value !== null && !Array.isArray(value);
@@ -119,7 +119,7 @@ export const createFieldValidation = (): FieldValidation => {
const fieldValidation: FieldValidation = {
__errors: [] as string[],
addError: (message: string) => {
fieldValidation.__errors.push(message);
fieldValidation.__errors?.push(message);
},
};
@@ -15,7 +15,7 @@
*/
import { featureFlagsApiRef, useApi } from '@backstage/core-plugin-api';
import { JsonObject } from '@backstage/types';
import { UiSchema } from '@rjsf/core';
import { UiSchema } from '@rjsf/utils';
import { TemplateParameterSchema } from '../../../types';
import { extractSchemaFromStep } from './schema';
@@ -22,7 +22,7 @@ import {
InfoCard,
MarkdownContent,
} from '@backstage/core-components';
import { FieldExtensionOptions } from '../../extensions';
import { NextFieldExtensionOptions } from '../../extensions';
import { Navigate } from 'react-router';
import { stringifyEntityRef } from '@backstage/catalog-model';
import {
@@ -39,7 +39,7 @@ import { BackstageTheme } from '@backstage/theme';
import { nextRouteRef, selectedTemplateRouteRef } from '../../routes';
export interface TemplateWizardPageProps {
customFieldExtensions: FieldExtensionOptions<any, any>[];
customFieldExtensions: NextFieldExtensionOptions<any, any>[];
}
const useStyles = makeStyles<BackstageTheme>(() => ({
+83 -18
View File
@@ -6743,7 +6743,11 @@ __metadata:
"@material-ui/lab": 4.0.0-alpha.57
"@react-hookz/web": ^15.0.0
"@rjsf/core": ^3.2.1
"@rjsf/core-v5": "npm:@rjsf/core@^5.0.0-beta.10"
"@rjsf/material-ui": ^3.2.1
"@rjsf/material-ui-v5": "npm:@rjsf/material-ui@^5.0.0-beta.10"
"@rjsf/utils": ^5.0.0-beta.10
"@rjsf/validator-ajv8": ^5.0.0-beta.10
"@testing-library/jest-dom": ^5.10.1
"@testing-library/react": ^12.1.3
"@testing-library/react-hooks": ^8.0.0
@@ -11940,6 +11944,21 @@ __metadata:
languageName: node
linkType: hard
"@rjsf/core-v5@npm:@rjsf/core@^5.0.0-beta.10":
version: 5.0.0-beta.10
resolution: "@rjsf/core@npm:5.0.0-beta.10"
dependencies:
lodash: ^4.17.15
lodash-es: ^4.17.15
nanoid: ^3.3.4
prop-types: ^15.7.2
peerDependencies:
"@rjsf/utils": ^5.0.0-beta.1
react: ^16.14.0 || >=17
checksum: c3e8852c8fedc6ae23da610033e0bded1bd198cebf7f54a5cdcf3b42d22a3b3864da9c735300cadc7d756f26055a7e62408d51a5ebdb906be9b1bf85bf3712ed
languageName: node
linkType: hard
"@rjsf/core@npm:^3.2.1":
version: 3.2.1
resolution: "@rjsf/core@npm:3.2.1"
@@ -11959,6 +11978,19 @@ __metadata:
languageName: node
linkType: hard
"@rjsf/material-ui-v5@npm:@rjsf/material-ui@^5.0.0-beta.10":
version: 5.0.0-beta.10
resolution: "@rjsf/material-ui@npm:5.0.0-beta.10"
peerDependencies:
"@material-ui/core": ^4.12.3
"@material-ui/icons": ^4.11.2
"@rjsf/core": ^5.0.0-beta.1
"@rjsf/utils": ^5.0.0-beta.1
react: ^16.14.0 || >=17
checksum: c6e602fc331ec37524837123f42e605389c8ea1a9207ed3e9052de380b91fb90365aa962b0020197f0fb2bcbd87baac34bef7dbbb31ea4b57b6587d0fcd81a09
languageName: node
linkType: hard
"@rjsf/material-ui@npm:^3.2.1":
version: 3.2.1
resolution: "@rjsf/material-ui@npm:3.2.1"
@@ -11971,6 +12003,35 @@ __metadata:
languageName: node
linkType: hard
"@rjsf/utils@npm:^5.0.0-beta.10":
version: 5.0.0-beta.10
resolution: "@rjsf/utils@npm:5.0.0-beta.10"
dependencies:
json-schema-merge-allof: ^0.8.1
jsonpointer: ^5.0.1
lodash: ^4.17.15
lodash-es: ^4.17.15
react-is: ^18.2.0
peerDependencies:
react: ^16.14.0 || >=17
checksum: df5dda5a17ce11df2342ceff8d6ad2b3dad3a1f98d44aa7da378e6cd48387e4af023964b6143836e1ad16ff68f6592c0dcf24ff9f3fc859480b42b2c879406bc
languageName: node
linkType: hard
"@rjsf/validator-ajv8@npm:^5.0.0-beta.10":
version: 5.0.0-beta.10
resolution: "@rjsf/validator-ajv8@npm:5.0.0-beta.10"
dependencies:
ajv: ^8.11.0
ajv-formats: ^2.1.1
lodash: ^4.17.15
lodash-es: ^4.17.15
peerDependencies:
"@rjsf/utils": ^5.0.0-beta.1
checksum: faa359c9ec58cf0fff35932d4e08a5c42f8449120cae606ebff108814f974e8db8a62df85e90e1fec7b0345388df09b83359ae1cc09103d41e73f298472b846a
languageName: node
linkType: hard
"@roadiehq/backstage-plugin-buildkite@npm:^2.0.8":
version: 2.0.9
resolution: "@roadiehq/backstage-plugin-buildkite@npm:2.0.9"
@@ -15626,7 +15687,7 @@ __metadata:
languageName: node
linkType: hard
"ajv@npm:^8.0.0, ajv@npm:^8.10.0, ajv@npm:^8.6.3, ajv@npm:^8.8.0":
"ajv@npm:^8.0.0, ajv@npm:^8.10.0, ajv@npm:^8.11.0, ajv@npm:^8.6.3, ajv@npm:^8.8.0":
version: 8.11.0
resolution: "ajv@npm:8.11.0"
dependencies:
@@ -18786,13 +18847,20 @@ __metadata:
languageName: node
linkType: hard
"core-js-pure@npm:^3.20.2, core-js-pure@npm:^3.6.5, core-js-pure@npm:^3.8.1":
"core-js-pure@npm:^3.20.2, core-js-pure@npm:^3.8.1":
version: 3.21.1
resolution: "core-js-pure@npm:3.21.1"
checksum: 00a5dff599b7fb0b30746a638b9d0edbdc0df24ed1580ca56be595fbe3c78c375d37fc4e1bff23627109229702c9ee8ea2587a66b8280eb33b85160aa4e401e9
languageName: node
linkType: hard
"core-js-pure@npm:^3.6.5":
version: 3.25.3
resolution: "core-js-pure@npm:3.25.3"
checksum: be3097b8e2b541977a08004e433b68adb4951cc7694229e695ae407f296d370c1f360e5fd70fc6910c4427e46e48e087ac455d68cebdf4314370907d2e03eef1
languageName: node
linkType: hard
"core-js@npm:^2.4.0, core-js@npm:^2.5.0, core-js@npm:^2.6.10":
version: 2.6.12
resolution: "core-js@npm:2.6.12"
@@ -22077,7 +22145,6 @@ __metadata:
"@material-ui/lab": 4.0.0-alpha.57
"@octokit/rest": ^19.0.3
"@oriflame/backstage-plugin-score-card": ^0.5.1
"@rjsf/core": ^3.2.1
"@roadiehq/backstage-plugin-buildkite": ^2.0.8
"@roadiehq/backstage-plugin-github-insights": ^2.0.5
"@roadiehq/backstage-plugin-github-pull-requests": ^2.2.7
@@ -27359,10 +27426,10 @@ __metadata:
languageName: node
linkType: hard
"jsonpointer@npm:^5.0.0":
version: 5.0.0
resolution: "jsonpointer@npm:5.0.0"
checksum: c7ec0b6bb596b81de687bc12945586bbcdc80dfb54919656d2690d76334f796a936270067ee9f1b5bbc2d9ecc551afb366ac35e6685aa61f07b5b68d1e5e857d
"jsonpointer@npm:^5.0.0, jsonpointer@npm:^5.0.1":
version: 5.0.1
resolution: "jsonpointer@npm:5.0.1"
checksum: 0b40f712900ad0c846681ea2db23b6684b9d5eedf55807b4708c656f5894b63507d0e28ae10aa1bddbea551241035afe62b6df0800fc94c2e2806a7f3adecd7c
languageName: node
linkType: hard
@@ -28097,7 +28164,7 @@ __metadata:
languageName: node
linkType: hard
"lodash-es@npm:^4.17.21":
"lodash-es@npm:^4.17.15, lodash-es@npm:^4.17.21":
version: 4.17.21
resolution: "lodash-es@npm:4.17.21"
checksum: 05cbffad6e2adbb331a4e16fbd826e7faee403a1a04873b82b42c0f22090f280839f85b95393f487c1303c8a3d2a010048bf06151a6cbe03eee4d388fb0a12d2
@@ -30062,16 +30129,7 @@ __metadata:
languageName: node
linkType: hard
"nanoid@npm:^3.1.23":
version: 3.3.1
resolution: "nanoid@npm:3.3.1"
bin:
nanoid: bin/nanoid.cjs
checksum: 4ef0969e1bbe866fc223eb32276cbccb0961900bfe79104fa5abe34361979dead8d0e061410a5c03bc3d47455685adf32c09d6f27790f4a6898fb51f7df7ec86
languageName: node
linkType: hard
"nanoid@npm:^3.3.4":
"nanoid@npm:^3.1.23, nanoid@npm:^3.3.4":
version: 3.3.4
resolution: "nanoid@npm:3.3.4"
bin:
@@ -33589,6 +33647,13 @@ __metadata:
languageName: node
linkType: hard
"react-is@npm:^18.2.0":
version: 18.2.0
resolution: "react-is@npm:18.2.0"
checksum: e72d0ba81b5922759e4aff17e0252bd29988f9642ed817f56b25a3e217e13eea8a7f2322af99a06edb779da12d5d636e9fda473d620df9a3da0df2a74141d53e
languageName: node
linkType: hard
"react-lifecycles-compat@npm:^3.0.4":
version: 3.0.4
resolution: "react-lifecycles-compat@npm:3.0.4"