Merge remote-tracking branch 'origin/master' into configure-template

# Conflicts:
#	plugins/scaffolder-react/api-report.md
#	plugins/scaffolder-react/src/next/components/Stepper/Stepper.tsx
This commit is contained in:
Bogdan Nechyporenko
2023-10-23 10:19:10 +02:00
1168 changed files with 23121 additions and 8109 deletions
+1 -1
View File
@@ -15,4 +15,4 @@
*/
export * from './next';
export type { FormProps } from './next';
export * from './legacy';
@@ -0,0 +1,16 @@
/*
* Copyright 2023 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.
*/
export * from './types';
@@ -0,0 +1,55 @@
/*
* Copyright 2023 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 { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common';
import type { FormProps as SchemaFormProps } from '@rjsf/core';
import { UiSchema } from '@rjsf/utils';
import { JsonObject } from '@backstage/types';
/** @public */
export type TemplateGroupFilter = {
title?: React.ReactNode;
filter: (entity: TemplateEntityV1beta3) => boolean;
};
/**
* Any `@rjsf/core` form properties that are publicly exposed to the `ScaffolderPage`
*
* @public
*/
export type FormProps = Pick<
SchemaFormProps,
'transformErrors' | 'noHtml5Validate'
>;
/**
* The props for the Last Step in scaffolder template form.
* Which represents the summary of the input provided by the end user.
*
* @public
*/
export type ReviewStepProps = {
disableButtons: boolean;
formData: JsonObject;
handleBack: () => void;
handleReset: () => void;
handleCreate: () => void;
steps: {
uiSchema: UiSchema;
mergedSchema: JsonObject;
schema: JsonObject;
}[];
};
@@ -14,23 +14,17 @@
* limitations under the License.
*/
import React from 'react';
import {
CustomFieldExtensionSchema,
CustomFieldValidator,
FieldExtensionOptions,
FieldExtensionComponentProps,
FieldExtensionUiSchema,
CustomFieldExtensionSchema,
} from './types';
import { Extension, attachComponentData } from '@backstage/core-plugin-api';
import { UIOptionsType } from '@rjsf/utils';
import { FIELD_EXTENSION_KEY, FIELD_EXTENSION_WRAPPER_KEY } from './keys';
/**
* The type used to wrap up the Layout and embed the input props
*
* @public
*/
export type FieldExtensionComponent<_TReturnValue, _TInputProps> = () => null;
/**
* Method for creating field extensions that can be used in the scaffolder
* frontend form.
@@ -38,7 +32,7 @@ export type FieldExtensionComponent<_TReturnValue, _TInputProps> = () => null;
*/
export function createScaffolderFieldExtension<
TReturnValue = unknown,
TInputProps = unknown,
TInputProps extends UIOptionsType = {},
>(
options: FieldExtensionOptions<TReturnValue, TInputProps>,
): Extension<FieldExtensionComponent<TReturnValue, TInputProps>> {
@@ -72,9 +66,19 @@ attachComponentData(
true,
);
/**
* The type used to wrap up the Layout and embed the input props
*
* @public
*/
export type FieldExtensionComponent<_TReturnValue, _TInputProps> = () => null;
export type {
CustomFieldExtensionSchema,
CustomFieldValidator,
FieldExtensionOptions,
FieldExtensionComponentProps,
FieldExtensionUiSchema,
CustomFieldExtensionSchema,
};
export * from './rjsf';
@@ -0,0 +1,292 @@
/*
* Copyright 2023 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 { ComponentType, ElementType, FormEvent, ReactNode, Ref } from 'react';
import {
ErrorSchema,
FormContextType,
GenericObjectType,
IdSchema,
Registry,
RJSFSchema,
StrictRJSFSchema,
UiSchema,
ValidatorType,
TemplatesType,
RegistryWidgetsType,
RJSFValidationError,
CustomValidator,
Experimental_DefaultFormStateBehavior,
ErrorTransformer,
} from '@rjsf/utils';
import { HTMLAttributes } from 'react';
import Form, { IChangeEvent } from '@rjsf/core';
/**
* The props for the `Field` components
* @public
*/
export interface ScaffolderRJSFFieldProps<
T = any,
S extends StrictRJSFSchema = RJSFSchema,
F extends FormContextType = any,
> extends GenericObjectType,
Pick<
HTMLAttributes<HTMLElement>,
Exclude<
keyof HTMLAttributes<HTMLElement>,
'onBlur' | 'onFocus' | 'onChange'
>
> {
/** The JSON subschema object for this field */
schema: S;
/** The uiSchema for this field */
uiSchema: UiSchema<T, S, F>;
/** The tree of unique ids for every child field */
idSchema: IdSchema<T>;
/** The data for this field */
formData: T;
/** The tree of errors for this field and its children */
errorSchema?: ErrorSchema<T>;
/** The field change event handler; called with the updated form data and an optional `ErrorSchema` */
onChange: (
newFormData: T | undefined,
es?: ErrorSchema<T>,
id?: string,
) => any;
/** The input blur event handler; call it with the field id and value */
onBlur: (id: string, value: any) => void;
/** The input focus event handler; call it with the field id and value */
onFocus: (id: string, value: any) => void;
/** The `formContext` object that you passed to `Form` */
formContext?: F;
/** A boolean value stating if the field should autofocus */
autofocus?: boolean;
/** A boolean value stating if the field is disabled */
disabled: boolean;
/** A boolean value stating if the field is hiding its errors */
hideError?: boolean;
/** A boolean value stating if the field is read-only */
readonly: boolean;
/** The required status of this field */
required?: boolean;
/** The unique name of the field, usually derived from the name of the property in the JSONSchema */
name: string;
/** To avoid collisions with existing ids in the DOM, it is possible to change the prefix used for ids;
* Default is `root`
*/
idPrefix?: string;
/** To avoid using a path separator that is present in field names, it is possible to change the separator used for
* ids (Default is `_`)
*/
idSeparator?: string;
/** An array of strings listing all generated error messages from encountered errors for this field */
rawErrors: string[];
/** The `registry` object */
registry: Registry<T, S, F>;
}
/**
* The properties that are passed to the `Form`
* @public
*/
export interface ScaffolderRJSFFormProps<
T = any,
S extends StrictRJSFSchema = RJSFSchema,
F extends FormContextType = any,
> {
/** The JSON schema object for the form */
schema: S;
/** An implementation of the `ValidatorType` interface that is needed for form validation to work */
validator: ValidatorType<T, S, F>;
/** The optional children for the form, if provided, it will replace the default `SubmitButton` */
children?: ReactNode;
/** The uiSchema for the form */
uiSchema?: UiSchema<T, S, F>;
/** The data for the form, used to prefill a form with existing data */
formData?: T;
/** You can provide a `formContext` object to the form, which is passed down to all fields and widgets. Useful for
* implementing context aware fields and widgets.
*
* NOTE: Setting `{readonlyAsDisabled: false}` on the formContext will make the antd theme treat readOnly fields as
* disabled.
*/
formContext?: F;
/** To avoid collisions with existing ids in the DOM, it is possible to change the prefix used for ids;
* Default is `root`
*/
idPrefix?: string;
/** To avoid using a path separator that is present in field names, it is possible to change the separator used for
* ids (Default is `_`)
*/
idSeparator?: string;
/** It's possible to disable the whole form by setting the `disabled` prop. The `disabled` prop is then forwarded down
* to each field of the form. If you just want to disable some fields, see the `ui:disabled` parameter in `uiSchema`
*/
disabled?: boolean;
/** It's possible to make the whole form read-only by setting the `readonly` prop. The `readonly` prop is then
* forwarded down to each field of the form. If you just want to make some fields read-only, see the `ui:readonly`
* parameter in `uiSchema`
*/
readonly?: boolean;
/** The dictionary of registered fields in the form */
fields?: ScaffolderRJSFRegistryFieldsType<T, S, F>;
/** The dictionary of registered templates in the form; Partial allows a subset to be provided beyond the defaults */
templates?: Partial<Omit<TemplatesType<T, S, F>, 'ButtonTemplates'>> & {
ButtonTemplates?: Partial<TemplatesType<T, S, F>['ButtonTemplates']>;
};
/** The dictionary of registered widgets in the form */
widgets?: RegistryWidgetsType<T, S, F>;
/** If you plan on being notified every time the form data are updated, you can pass an `onChange` handler, which will
* receive the same args as `onSubmit` any time a value is updated in the form. Can also return the `id` of the field
* that caused the change
*/
onChange?: (data: IChangeEvent<T, S, F>, id?: string) => void;
/** To react when submitted form data are invalid, pass an `onError` handler. It will be passed the list of
* encountered errors
*/
onError?: (errors: RJSFValidationError[]) => void;
/** You can pass a function as the `onSubmit` prop of your `Form` component to listen to when the form is submitted
* and its data are valid. It will be passed a result object having a `formData` attribute, which is the valid form
* data you're usually after. The original event will also be passed as a second parameter
*/
onSubmit?: (data: IChangeEvent<T, S, F>, event: FormEvent<any>) => void;
/** Sometimes you may want to trigger events or modify external state when a field has been touched, so you can pass
* an `onBlur` handler, which will receive the id of the input that was blurred and the field value
*/
onBlur?: (id: string, data: any) => void;
/** Sometimes you may want to trigger events or modify external state when a field has been focused, so you can pass
* an `onFocus` handler, which will receive the id of the input that is focused and the field value
*/
onFocus?: (id: string, data: any) => void;
/** The value of this prop will be passed to the `accept-charset` HTML attribute on the form */
acceptcharset?: string;
/** The value of this prop will be passed to the `action` HTML attribute on the form
*
* NOTE: this just renders the `action` attribute in the HTML markup. There is no real network request being sent to
* this `action` on submit. Instead, react-jsonschema-form catches the submit event with `event.preventDefault()`
* and then calls the `onSubmit` function, where you could send a request programmatically with `fetch` or similar.
*/
action?: string;
/** The value of this prop will be passed to the `autocomplete` HTML attribute on the form */
autoComplete?: string;
/** The value of this prop will be passed to the `class` HTML attribute on the form */
className?: string;
/** The value of this prop will be passed to the `enctype` HTML attribute on the form */
enctype?: string;
/** The value of this prop will be passed to the `id` HTML attribute on the form */
id?: string;
/** The value of this prop will be passed to the `name` HTML attribute on the form */
name?: string;
/** The value of this prop will be passed to the `method` HTML attribute on the form */
method?: string;
/** It's possible to change the default `form` tag name to a different HTML tag, which can be helpful if you are
* nesting forms. However, native browser form behaviour, such as submitting when the `Enter` key is pressed, may no
* longer work
*/
tagName?: ElementType;
/** The value of this prop will be passed to the `target` HTML attribute on the form */
target?: string;
/** Formerly the `validate` prop; Takes a function that specifies custom validation rules for the form */
customValidate?: CustomValidator<T, S, F>;
/** This prop allows passing in custom errors that are augmented with the existing JSON Schema errors on the form; it
* can be used to implement asynchronous validation
*/
extraErrors?: ErrorSchema<T>;
/** If set to true, turns off HTML5 validation on the form; Set to `false` by default */
noHtml5Validate?: boolean;
/** If set to true, turns off all validation. Set to `false` by default
*
* @deprecated - In a future release, this switch may be replaced by making `validator` prop optional
*/
noValidate?: boolean;
/** If set to true, the form will perform validation and show any validation errors whenever the form data is changed,
* rather than just on submit
*/
liveValidate?: boolean;
/** If `omitExtraData` and `liveOmit` are both set to true, then extra form data values that are not in any form field
* will be removed whenever `onChange` is called. Set to `false` by default
*/
liveOmit?: boolean;
/** If set to true, then extra form data values that are not in any form field will be removed whenever `onSubmit` is
* called. Set to `false` by default.
*/
omitExtraData?: boolean;
/** When this prop is set to `top` or 'bottom', a list of errors (or the custom error list defined in the `ErrorList`) will also
* show. When set to false, only inline input validation errors will be shown. Set to `top` by default
*/
showErrorList?: false | 'top' | 'bottom';
/** A function can be passed to this prop in order to make modifications to the default errors resulting from JSON
* Schema validation
*/
transformErrors?: ErrorTransformer<T, S, F>;
/** If set to true, then the first field with an error will receive the focus when the form is submitted with errors
*/
focusOnFirstError?: boolean | ((error: RJSFValidationError) => void);
/** Optional string translation function, if provided, allows users to change the translation of the RJSF internal
* strings. Some strings contain replaceable parameter values as indicated by `%1`, `%2`, etc. The number after the
* `%` indicates the order of the parameter. The ordering of parameters is important because some languages may choose
* to put the second parameter before the first in its translation.
*/
translateString?: Registry['translateString'];
/** Optional configuration object with flags, if provided, allows users to override default form state behavior
* Currently only affecting minItems on array fields and handling of setting defaults based on the value of
* `emptyObjectFields`
*/
experimental_defaultFormStateBehavior?: Experimental_DefaultFormStateBehavior;
/**
* _internalFormWrapper is currently used by the semantic-ui theme to provide a custom wrapper around `<Form />`
* that supports the proper rendering of those themes. To use this prop, one must pass a component that takes two
* props: `children` and `as`. That component, at minimum, should render the `children` inside of a <form /> tag
* unless `as` is provided, in which case, use the `as` prop in place of `<form />`.
* i.e.:
* ```
* export default function InternalForm({ children, as }) {
* const FormTag = as || 'form';
* return <FormTag>{children}</FormTag>;
* }
* ```
*
* Use at your own risk as this prop is private and may change at any time without notice.
*/
_internalFormWrapper?: ElementType;
/** Support receiving a React ref to the Form
*/
ref?: Ref<Form<T, S, F>>;
}
/**
* The set of `Fields` stored in the `Registry`
* @public
*/
export type ScaffolderRJSFRegistryFieldsType<
T = any,
S extends StrictRJSFSchema = RJSFSchema,
F extends FormContextType = any,
> = {
/** A `Field` indexed by `name` */
[name: string]: ScaffolderRJSFField<T, S, F>;
};
/**
* The `Field` type for Field Extensions
* @public
*/
export type ScaffolderRJSFField<
T = any,
S extends StrictRJSFSchema = RJSFSchema,
F extends FormContextType = any,
> = ComponentType<ScaffolderRJSFFieldProps<T, S, F>>;
@@ -14,19 +14,23 @@
* limitations under the License.
*/
import { ApiHolder } from '@backstage/core-plugin-api';
import { FieldValidation, FieldProps } from '@rjsf/core';
import { PropsWithChildren } from 'react';
import { JsonObject } from '@backstage/types';
import { JSONSchema7 } from 'json-schema';
import { UiSchema, UIOptionsType, FieldValidation } from '@rjsf/utils';
import { ScaffolderRJSFFieldProps } from './rjsf';
/**
* Field validation type for Custom Field Extensions.
* Type for Field Extension Props for RJSF v5
*
* @public
*/
export type CustomFieldValidator<TFieldReturnValue> = (
data: TFieldReturnValue,
field: FieldValidation,
context: { apiHolder: ApiHolder },
) => void | Promise<void>;
export interface FieldExtensionComponentProps<
TFieldReturnValue,
TUiOptions = {},
> extends PropsWithChildren<ScaffolderRJSFFieldProps<TFieldReturnValue>> {
uiSchema: FieldExtensionUiSchema<TFieldReturnValue, TUiOptions>;
}
/**
* Type for the Custom Field Extension schema.
@@ -38,6 +42,32 @@ export type CustomFieldExtensionSchema = {
uiOptions?: JSONSchema7;
};
/**
* Type for Field Extension UiSchema
*
* @public
*/
export interface FieldExtensionUiSchema<TFieldReturnValue, TUiOptions>
extends UiSchema<TFieldReturnValue> {
'ui:options'?: TUiOptions & UIOptionsType<TFieldReturnValue>;
}
/**
* Field validation type for Custom Field Extensions.
*
* @public
*/
export type CustomFieldValidator<TFieldReturnValue, TUiOptions = unknown> = (
data: TFieldReturnValue,
field: FieldValidation,
context: {
apiHolder: ApiHolder;
formData: JsonObject;
schema: JsonObject;
uiSchema?: FieldExtensionUiSchema<TFieldReturnValue, TUiOptions>;
},
) => void | Promise<void>;
/**
* Type for the Custom Field Extension with the
* name and components and validation function.
@@ -46,27 +76,12 @@ export type CustomFieldExtensionSchema = {
*/
export type FieldExtensionOptions<
TFieldReturnValue = unknown,
TInputProps = unknown,
TUiOptions = unknown,
> = {
name: string;
component: (
props: FieldExtensionComponentProps<TFieldReturnValue, TInputProps>,
props: FieldExtensionComponentProps<TFieldReturnValue, TUiOptions>,
) => JSX.Element | null;
validation?: CustomFieldValidator<TFieldReturnValue>;
validation?: CustomFieldValidator<TFieldReturnValue, TUiOptions>;
schema?: CustomFieldExtensionSchema;
};
/**
* Type for field extensions and being able to type
* incoming props easier.
*
* @public
*/
export interface FieldExtensionComponentProps<
TFieldReturnValue,
TUiOptions = unknown,
> extends FieldProps<TFieldReturnValue> {
uiSchema: FieldProps['uiSchema'] & {
'ui:options'?: TUiOptions;
};
}
+1
View File
@@ -15,6 +15,7 @@
*/
export * from './extensions';
export * from './components';
export * from './types';
export * from './secrets';
export * from './api';
@@ -16,7 +16,7 @@
import { LAYOUTS_KEY, LAYOUTS_WRAPPER_KEY } from './keys';
import { attachComponentData, Extension } from '@backstage/core-plugin-api';
import type { FormProps as SchemaFormProps } from '@rjsf/core-v5';
import type { FormProps as SchemaFormProps } from '@rjsf/core';
import React from 'react';
/**
@@ -15,13 +15,11 @@
*/
import {
NextCustomFieldValidator,
NextFieldExtensionOptions,
NextFieldExtensionComponentProps,
NextFieldExtensionUiSchema,
LegacyCustomFieldValidator,
LegacyFieldExtensionOptions,
LegacyFieldExtensionComponentProps,
} from './types';
import { Extension, attachComponentData } from '@backstage/core-plugin-api';
import { UIOptionsType } from '@rjsf/utils';
import { FIELD_EXTENSION_KEY } from '../../extensions/keys';
import { FieldExtensionComponent } from '@backstage/plugin-scaffolder-react';
@@ -30,11 +28,11 @@ import { FieldExtensionComponent } from '@backstage/plugin-scaffolder-react';
* frontend form.
* @alpha
*/
export function createNextScaffolderFieldExtension<
export function createLegacyScaffolderFieldExtension<
TReturnValue = unknown,
TInputProps extends UIOptionsType = {},
TInputProps = unknown,
>(
options: NextFieldExtensionOptions<TReturnValue, TInputProps>,
options: LegacyFieldExtensionOptions<TReturnValue, TInputProps>,
): Extension<FieldExtensionComponent<TReturnValue, TInputProps>> {
return {
expose() {
@@ -52,8 +50,7 @@ export function createNextScaffolderFieldExtension<
}
export type {
NextCustomFieldValidator,
NextFieldExtensionOptions,
NextFieldExtensionComponentProps,
NextFieldExtensionUiSchema,
LegacyCustomFieldValidator,
LegacyFieldExtensionOptions,
LegacyFieldExtensionComponentProps,
};
@@ -0,0 +1,65 @@
/*
* Copyright 2021 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 { ApiHolder } from '@backstage/core-plugin-api';
import { FieldValidation } from '@rjsf/utils';
import {
CustomFieldExtensionSchema,
ScaffolderRJSFFieldProps,
} from '@backstage/plugin-scaffolder-react';
/**
* Field validation type for Custom Field Extensions.
*
* @alpha
*/
export type LegacyCustomFieldValidator<TFieldReturnValue> = (
data: TFieldReturnValue,
field: FieldValidation,
context: { apiHolder: ApiHolder },
) => void | Promise<void>;
/**
* Type for the Custom Field Extension with the
* name and components and validation function.
*
* @alpha
*/
export type LegacyFieldExtensionOptions<
TFieldReturnValue = unknown,
TInputProps = unknown,
> = {
name: string;
component: (
props: LegacyFieldExtensionComponentProps<TFieldReturnValue, TInputProps>,
) => JSX.Element | null;
validation?: LegacyCustomFieldValidator<TFieldReturnValue>;
schema?: CustomFieldExtensionSchema;
};
/**
* Type for field extensions and being able to type
* incoming props easier.
*
* @alpha
*/
export interface LegacyFieldExtensionComponentProps<
TFieldReturnValue,
TUiOptions = unknown,
> extends ScaffolderRJSFFieldProps<TFieldReturnValue> {
uiSchema: ScaffolderRJSFFieldProps['uiSchema'] & {
'ui:options'?: TUiOptions;
};
}
@@ -0,0 +1,16 @@
/*
* Copyright 2023 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.
*/
export * from './extensions';
@@ -14,26 +14,47 @@
* limitations under the License.
*/
import { FormProps, withTheme } from '@rjsf/core-v5';
import { withTheme } from '@rjsf/core';
import React from 'react';
import { PropsWithChildren } from 'react';
import { FieldTemplate } from './FieldTemplate';
import { DescriptionFieldTemplate } from './DescriptionFieldTemplate';
import { FieldProps } from '@rjsf/utils';
import { ScaffolderRJSFFormProps } from '@backstage/plugin-scaffolder-react';
import { Theme as MuiTheme } from '@rjsf/material-ui';
// 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 WrappedForm = withTheme(require('@rjsf/material-ui-v5').Theme);
const WrappedForm = withTheme(MuiTheme);
/**
* The Form component
* @alpha
*/
export const Form = (props: PropsWithChildren<FormProps>) => {
export const Form = (props: PropsWithChildren<ScaffolderRJSFFormProps>) => {
// This is where we unbreak the changes from RJSF, and make it work with our custom fields so we don't pass on this
// breaking change to our users. We will look more into a better API for this in scaffolderv2.
const wrappedFields = Object.fromEntries(
Object.entries(props.fields ?? {}).map(([key, Component]) => [
key,
(wrapperProps: FieldProps) => {
return (
<Component
{...wrapperProps}
uiSchema={wrapperProps.uiSchema ?? {}}
formData={wrapperProps.formData}
rawErrors={wrapperProps.rawErrors ?? []}
/>
);
},
]),
);
const templates = {
FieldTemplate,
DescriptionFieldTemplate,
...props.templates,
};
return <WrappedForm {...props} templates={templates} />;
return (
<WrappedForm {...props} templates={templates} fields={wrappedFields} />
);
};
@@ -20,7 +20,7 @@ import { renderInTestApp } from '@backstage/test-utils';
import { act, fireEvent } from '@testing-library/react';
import type { RJSFValidationError } from '@rjsf/utils';
import { JsonValue } from '@backstage/types';
import { NextFieldExtensionComponentProps } from '../../extensions';
import { FieldExtensionComponentProps } from '../../../extensions';
import { LayoutTemplate } from '../../../layouts';
describe('Stepper', () => {
@@ -115,7 +115,7 @@ describe('Stepper', () => {
it('should merge nested formData correctly in multiple steps', async () => {
const Repo = ({
onChange,
}: NextFieldExtensionComponentProps<{ repository: string }, any>) => (
}: FieldExtensionComponentProps<{ repository: string }, any>) => (
<input
aria-label="repo"
type="text"
@@ -126,7 +126,7 @@ describe('Stepper', () => {
const Owner = ({
onChange,
}: NextFieldExtensionComponentProps<{ owner: string }, any>) => (
}: FieldExtensionComponentProps<{ owner: string }, any>) => (
<input
aria-label="owner"
type="text"
@@ -273,12 +273,12 @@ describe('Stepper', () => {
/>,
);
await act(async () => {
await fireEvent.click(getByRole('button', { name: 'Review' }));
expect(getByRole('progressbar')).toBeInTheDocument();
expect(getByRole('button', { name: 'Review' })).toBeDisabled();
act(() => {
fireEvent.click(getByRole('button', { name: 'Review' }));
});
expect(getByRole('progressbar')).toBeInTheDocument();
expect(getByRole('button', { name: 'Review' })).toBeDisabled();
});
it('should transform default error message', async () => {
@@ -312,7 +312,7 @@ describe('Stepper', () => {
manifest={manifest}
extensions={[]}
onCreate={jest.fn()}
FormProps={{ transformErrors }}
formProps={{ transformErrors }}
/>,
);
@@ -23,18 +23,23 @@ import {
makeStyles,
LinearProgress,
} from '@material-ui/core';
import { type IChangeEvent } from '@rjsf/core-v5';
import { type IChangeEvent } from '@rjsf/core';
import { ErrorSchema } from '@rjsf/utils';
import React, { useCallback, useMemo, useState, type ReactNode } from 'react';
import { NextFieldExtensionOptions } from '../../extensions';
import React, {
useCallback,
useMemo,
useState,
type ReactNode,
ComponentType,
} from 'react';
import {
createAsyncValidators,
type FormValidation,
} from './createAsyncValidators';
import { ReviewState, type ReviewStateProps } from '../ReviewState';
import { useTemplateSchema, useFormDataFromQuery } from '../../hooks';
import { useTemplateSchema } from '../../hooks/useTemplateSchema';
import validator from '@rjsf/validator-ajv8';
import { FormProps } from '../../types';
import { useFormDataFromQuery } from '../../hooks';
import { useTransformSchemaToProps } from '../../hooks/useTransformSchemaToProps';
import { hasErrors } from './utils';
import * as FieldOverrides from './FieldOverrides';
@@ -42,7 +47,10 @@ import { Form } from '../Form';
import {
TemplateParameterSchema,
LayoutOptions,
FieldExtensionOptions,
FormProps,
} from '@backstage/plugin-scaffolder-react';
import { ReviewStepProps } from '@backstage/plugin-scaffolder-react';
const useStyles = makeStyles(theme => ({
backButton: {
@@ -65,14 +73,14 @@ const useStyles = makeStyles(theme => ({
*/
export type StepperProps = {
manifest: TemplateParameterSchema;
extensions: NextFieldExtensionOptions<any, any>[];
extensions: FieldExtensionOptions<any, any>[];
templateName?: string;
FormProps?: FormProps;
formProps?: FormProps;
initialState?: Record<string, JsonValue>;
onCreate: (values: Record<string, JsonValue>) => Promise<void>;
components?: {
ReviewStepComponent?: ComponentType<ReviewStepProps>;
ReviewStateComponent?: (props: ReviewStateProps) => JSX.Element;
backButtonText?: ReactNode;
createButtonText?: ReactNode;
reviewButtonText?: ReactNode;
};
@@ -87,12 +95,12 @@ export const Stepper = (stepperProps: StepperProps) => {
const { layouts = [], components = {}, ...props } = stepperProps;
const {
ReviewStateComponent = ReviewState,
backButtonText = 'Back',
ReviewStepComponent,
createButtonText = 'Create',
reviewButtonText = 'Review',
} = components;
const analytics = useAnalytics();
const { configurations, steps } = useTemplateSchema(props.manifest);
const { steps } = useTemplateSchema(props.manifest);
const apiHolder = useApiHolder();
const [activeStep, setActiveStep] = useState(0);
const [isValidating, setIsValidating] = useState(false);
@@ -129,6 +137,13 @@ export const Stepper = (stepperProps: StepperProps) => {
[setFormState],
);
const handleCreate = useCallback(() => {
props.onCreate(formState);
const name =
typeof formState.name === 'string' ? formState.name : undefined;
analytics.captureEvent('create', name ?? props.templateName ?? 'unknown');
}, [props, formState, analytics]);
const currentStep = useTransformSchemaToProps(steps[activeStep], { layouts });
const handleNext = async ({
@@ -158,13 +173,6 @@ export const Stepper = (stepperProps: StepperProps) => {
setFormState(current => ({ ...current, ...formData }));
};
const backLabel =
configurations?.buttonLabels?.backButtonText ?? backButtonText;
const createLabel =
configurations?.buttonLabels?.createButtonText ?? createButtonText;
const reviewLabel =
configurations?.buttonLabels?.reviewButtonText ?? reviewButtonText;
return (
<>
{isValidating && <LinearProgress variant="indeterminate" />}
@@ -175,10 +183,11 @@ export const Stepper = (stepperProps: StepperProps) => {
</MuiStep>
))}
<MuiStep>
<MuiStepLabel>${reviewLabel}</MuiStepLabel>
<MuiStepLabel>Review</MuiStepLabel>
</MuiStep>
</MuiStepper>
<div className={styles.formWrapper}>
{/* eslint-disable-next-line no-nested-ternary */}
{activeStep < steps.length ? (
<Form
validator={validator}
@@ -191,7 +200,7 @@ export const Stepper = (stepperProps: StepperProps) => {
fields={{ ...FieldOverrides, ...extensions }}
showErrorList={false}
onChange={handleChange}
{...(props.FormProps ?? {})}
{...(props.formProps ?? {})}
>
<div className={styles.footer}>
<Button
@@ -199,7 +208,7 @@ export const Stepper = (stepperProps: StepperProps) => {
className={styles.backButton}
disabled={activeStep < 1 || isValidating}
>
{backLabel}
Back
</Button>
<Button
variant="contained"
@@ -207,10 +216,20 @@ export const Stepper = (stepperProps: StepperProps) => {
type="submit"
disabled={isValidating}
>
{activeStep === steps.length - 1 ? reviewLabel : 'Next'}
{activeStep === steps.length - 1 ? reviewButtonText : 'Next'}
</Button>
</div>
</Form>
) : // TODO: potentially move away from this pattern, deprecate?
ReviewStepComponent ? (
<ReviewStepComponent
disableButtons={isValidating}
formData={formState}
handleBack={handleBack}
handleReset={() => {}}
steps={steps}
handleCreate={handleCreate}
/>
) : (
<>
<ReviewStateComponent formState={formState} schemas={steps} />
@@ -220,24 +239,14 @@ export const Stepper = (stepperProps: StepperProps) => {
className={styles.backButton}
disabled={activeStep < 1}
>
{configurations?.buttonLabels?.backButtonText ?? backButtonText}
Back
</Button>
<Button
variant="contained"
color="primary"
onClick={() => {
props.onCreate(formState);
const name =
typeof formState.name === 'string'
? formState.name
: undefined;
analytics.captureEvent(
'create',
name ?? props.templateName ?? 'unknown',
);
}}
onClick={handleCreate}
>
{createLabel}
{createButtonText}
</Button>
</div>
</>
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { JsonObject } from '@backstage/types';
import { NextCustomFieldValidator } from '../../extensions';
import { CustomFieldValidator } from '../../../extensions';
import { createAsyncValidators } from './createAsyncValidators';
describe('createAsyncValidators', () => {
@@ -158,16 +158,13 @@ describe('createAsyncValidators', () => {
},
};
const NameField: NextCustomFieldValidator<string> = (
value,
{ addError },
) => {
const NameField: CustomFieldValidator<string> = (value, { addError }) => {
if (!value) {
addError('something is broken here!');
}
};
const AddressField: NextCustomFieldValidator<{
const AddressField: CustomFieldValidator<{
street?: string;
postcode?: string;
}> = (value, { addError }) => {
@@ -183,8 +180,8 @@ describe('createAsyncValidators', () => {
const validate = createAsyncValidators(
schema,
{
NameField: NameField as NextCustomFieldValidator<unknown>,
AddressField: AddressField as NextCustomFieldValidator<unknown>,
NameField: NameField as CustomFieldValidator<unknown>,
AddressField: AddressField as CustomFieldValidator<unknown>,
},
{
apiHolder: { get: jest.fn() },
@@ -298,7 +295,7 @@ describe('createAsyncValidators', () => {
},
};
const AddressField: NextCustomFieldValidator<{
const AddressField: CustomFieldValidator<{
street?: string;
postcode?: string;
}> = (value, { addError }) => {
@@ -311,18 +308,15 @@ describe('createAsyncValidators', () => {
}
};
const NameField: NextCustomFieldValidator<string> = (
value,
{ addError },
) => {
const NameField: CustomFieldValidator<string> = (value, { addError }) => {
if (!value) {
addError('something is broken here!');
}
};
const validators = {
AddressField: AddressField as NextCustomFieldValidator<unknown>,
NameField: NameField as NextCustomFieldValidator<unknown>,
AddressField: AddressField as CustomFieldValidator<unknown>,
NameField: NameField as CustomFieldValidator<unknown>,
};
const validate = createAsyncValidators(schema, validators, {
@@ -19,22 +19,23 @@ import type { JsonObject, JsonValue } from '@backstage/types';
import { ApiHolder } from '@backstage/core-plugin-api';
import { Draft07 as JSONSchema } from 'json-schema-library';
import { createFieldValidation, extractSchemaFromStep } from '../../lib';
import { NextCustomFieldValidator } from '../../extensions';
import {
CustomFieldValidator,
FieldExtensionUiSchema,
} from '@backstage/plugin-scaffolder-react';
import { isObject } from './utils';
import { NextFieldExtensionUiSchema } from '../../extensions/types';
/**
* @internal
*/
/** @alpha */
export type FormValidation = {
[name: string]: FieldValidation | FormValidation;
};
/** @alpha */
export const createAsyncValidators = (
rootSchema: JsonObject,
validators: Record<
string,
undefined | NextCustomFieldValidator<unknown, unknown>
undefined | CustomFieldValidator<unknown, unknown>
>,
context: {
apiHolder: ApiHolder;
@@ -53,7 +54,7 @@ export const createAsyncValidators = (
key: string,
value: JsonValue | undefined,
schema: JsonObject,
uiSchema: NextFieldExtensionUiSchema<unknown, unknown>,
uiSchema: FieldExtensionUiSchema<unknown, unknown>,
) => {
const validator = validators[validatorName];
if (validator) {
@@ -82,7 +83,7 @@ export const createAsyncValidators = (
const doValidateItem = async (
propValue: JsonObject,
itemSchema: JsonObject,
itemUiSchema: NextFieldExtensionUiSchema<unknown, unknown>,
itemUiSchema: FieldExtensionUiSchema<unknown, unknown>,
) => {
await validateForm(
propValue['ui:field'] as string,
@@ -14,3 +14,7 @@
* limitations under the License.
*/
export { Stepper, type StepperProps } from './Stepper';
export {
createAsyncValidators,
type FormValidation,
} from './createAsyncValidators';
@@ -20,6 +20,7 @@ import { TemplateCategoryPicker } from './TemplateCategoryPicker';
import { renderInTestApp, TestApiProvider } from '@backstage/test-utils';
import { alertApiRef } from '@backstage/core-plugin-api';
import { fireEvent } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
jest.mock('@backstage/plugin-catalog-react', () => ({
useEntityTypeFilter: jest.fn(),
@@ -91,7 +92,7 @@ describe('TemplateCategoryPicker', () => {
);
const openButton = getByRole('button', { name: 'Open' });
openButton.click();
await userEvent.click(openButton);
expect(getByRole('checkbox', { name: 'Foo' })).toBeInTheDocument();
expect(getByRole('checkbox', { name: 'Bar' })).toBeInTheDocument();
@@ -18,7 +18,7 @@ jest.mock('@backstage/plugin-catalog-react', () => ({
useEntityList: jest.fn(),
}));
jest.mock('@backstage/plugin-scaffolder-react/alpha', () => ({
jest.mock('../TemplateGroup/TemplateGroup', () => ({
TemplateGroup: jest.fn(() => null),
}));
@@ -27,7 +27,7 @@ import { useEntityList } from '@backstage/plugin-catalog-react';
import { TemplateGroups } from './TemplateGroups';
import { renderInTestApp, TestApiProvider } from '@backstage/test-utils';
import { errorApiRef } from '@backstage/core-plugin-api';
import { TemplateGroup } from '@backstage/plugin-scaffolder-react/alpha';
import { TemplateGroup } from '../TemplateGroup/TemplateGroup';
describe('TemplateGroups', () => {
beforeEach(() => jest.clearAllMocks());
@@ -23,15 +23,8 @@ import {
import { Progress, Link } from '@backstage/core-components';
import { Typography } from '@material-ui/core';
import { errorApiRef, IconComponent, useApi } from '@backstage/core-plugin-api';
import { TemplateGroup } from '@backstage/plugin-scaffolder-react/alpha';
/**
* @alpha
*/
export type TemplateGroupFilter = {
title?: React.ReactNode;
filter: (entity: TemplateEntityV1beta3) => boolean;
};
import { TemplateGroupFilter } from '@backstage/plugin-scaffolder-react';
import { TemplateGroup } from '../TemplateGroup/TemplateGroup';
/**
* @alpha
@@ -27,6 +27,7 @@ import { errorApiRef, useApi } from '@backstage/core-plugin-api';
import { useTemplateParameterSchema } from '../../hooks/useTemplateParameterSchema';
import { Stepper, type StepperProps } from '../Stepper/Stepper';
import { SecretsContextProvider } from '../../../secrets/SecretsContext';
import { ReviewStepProps } from '@backstage/plugin-scaffolder-react';
const useStyles = makeStyles<BackstageTheme>(() => ({
markdown: {
@@ -48,11 +49,14 @@ export type WorkflowProps = {
description?: string;
namespace: string;
templateName: string;
components?: {
ReviewStepComponent?: React.ComponentType<ReviewStepProps>;
};
onError(error: Error | undefined): JSX.Element | null;
} & Pick<
StepperProps,
| 'extensions'
| 'FormProps'
| 'formProps'
| 'components'
| 'onCreate'
| 'initialState'
@@ -1,84 +0,0 @@
/*
* Copyright 2021 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 { ApiHolder } from '@backstage/core-plugin-api';
import {
UIOptionsType,
FieldProps as FieldPropsV5,
UiSchema as UiSchemaV5,
FieldValidation as FieldValidationV5,
} from '@rjsf/utils';
import { PropsWithChildren } from 'react';
import { JsonObject } from '@backstage/types';
import { CustomFieldExtensionSchema } from '@backstage/plugin-scaffolder-react';
/**
* Type for Field Extension Props for RJSF v5
*
* @alpha
*/
export interface NextFieldExtensionComponentProps<
TFieldReturnValue,
TUiOptions = {},
> extends PropsWithChildren<FieldPropsV5<TFieldReturnValue>> {
uiSchema?: NextFieldExtensionUiSchema<TFieldReturnValue, TUiOptions>;
}
/**
* Type for Field Extension UiSchema
*
* @alpha
*/
export interface NextFieldExtensionUiSchema<TFieldReturnValue, TUiOptions>
extends UiSchemaV5<TFieldReturnValue> {
'ui:options'?: TUiOptions & UIOptionsType;
}
/**
* Field validation type for Custom Field Extensions.
*
* @alpha
*/
export type NextCustomFieldValidator<
TFieldReturnValue,
TUiOptions = unknown,
> = (
data: TFieldReturnValue,
field: FieldValidationV5,
context: {
apiHolder: ApiHolder;
formData: JsonObject;
schema: JsonObject;
uiSchema?: NextFieldExtensionUiSchema<TFieldReturnValue, TUiOptions>;
},
) => void | Promise<void>;
/**
* Type for the Custom Field Extension with the
* name and components and validation function.
*
* @alpha
*/
export type NextFieldExtensionOptions<
TFieldReturnValue = unknown,
TUiOptions = unknown,
> = {
name: string;
component: (
props: NextFieldExtensionComponentProps<TFieldReturnValue, TUiOptions>,
) => JSX.Element | null;
validation?: NextCustomFieldValidator<TFieldReturnValue, TUiOptions>;
schema?: CustomFieldExtensionSchema;
};
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { useTemplateSchema } from './useTemplateSchema';
import { renderHook } from '@testing-library/react-hooks';
import { renderHook } from '@testing-library/react';
import { TestApiProvider } from '@backstage/test-utils';
import React from 'react';
import { featureFlagsApiRef } from '@backstage/core-plugin-api';
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import React, { PropsWithChildren } from 'react';
import { renderHook } from '@testing-library/react-hooks';
import { renderHook } from '@testing-library/react';
import { TestApiProvider } from '@backstage/test-utils';
import { type ParsedTemplateSchema } from './useTemplateSchema';
import { useTransformSchemaToProps } from './useTransformSchemaToProps';
@@ -14,7 +14,5 @@
* limitations under the License.
*/
export * from './components';
export * from './extensions';
export * from './types';
export * from './lib';
export * from './hooks';
@@ -1,31 +0,0 @@
/*
* 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 type { FormProps as SchemaFormProps } from '@rjsf/core-v5';
// TODO(Rugvip): The FormProps type is actually supposed to be alpha, but since we want to
// refer to it from @backstage/plugin-scaffolder, it needs to be public for now.
// Once we support internal alpha re-exports this should be switched to an alpha export.
/**
* Any `@rjsf/core` form properties that are publicly exposed to the `NextScaffolderpage`
*
* @alpha
*/
export type FormProps = Pick<
SchemaFormProps,
'transformErrors' | 'noHtml5Validate'
>;
@@ -15,7 +15,7 @@
*/
import React from 'react';
import { useTemplateSecrets, SecretsContextProvider } from './SecretsContext';
import { renderHook, act } from '@testing-library/react-hooks';
import { renderHook, act } from '@testing-library/react';
describe('SecretsContext', () => {
it('should allow the setting of secrets in the context', async () => {