Merge pull request #20221 from backstage/blam/promote-scaffolder
[scaffolder] Make the `next` `Router` the new default and upgrade to `rjsf@v5`
This commit is contained in:
@@ -64,7 +64,7 @@ import { GcpProjectsPage } from '@backstage/plugin-gcp-projects';
|
||||
import { HomepageCompositionRoot, VisitListener } from '@backstage/plugin-home';
|
||||
import { LighthousePage } from '@backstage/plugin-lighthouse';
|
||||
import { NewRelicPage } from '@backstage/plugin-newrelic';
|
||||
import { NextScaffolderPage } from '@backstage/plugin-scaffolder/alpha';
|
||||
import { LegacyScaffolderPage } from '@backstage/plugin-scaffolder/alpha';
|
||||
import { ScaffolderPage, scaffolderPlugin } from '@backstage/plugin-scaffolder';
|
||||
import {
|
||||
ScaffolderFieldExtensions,
|
||||
@@ -236,11 +236,11 @@ const routes = (
|
||||
<LightBox />
|
||||
</TechDocsAddons>
|
||||
</Route>
|
||||
<FeatureFlagged with="scaffolder-next-preview">
|
||||
<FeatureFlagged with="scaffolder-legacy">
|
||||
<Route
|
||||
path="/create"
|
||||
element={
|
||||
<NextScaffolderPage
|
||||
<LegacyScaffolderPage
|
||||
groups={[
|
||||
{
|
||||
title: 'Recommended',
|
||||
@@ -252,14 +252,14 @@ const routes = (
|
||||
}
|
||||
>
|
||||
<ScaffolderFieldExtensions>
|
||||
<DelayingComponentFieldExtension />
|
||||
<LowerCaseValuePickerFieldExtension />
|
||||
</ScaffolderFieldExtensions>
|
||||
<ScaffolderLayouts>
|
||||
<TwoColumnLayout />
|
||||
</ScaffolderLayouts>
|
||||
</Route>
|
||||
</FeatureFlagged>
|
||||
<FeatureFlagged without="scaffolder-next-preview">
|
||||
<FeatureFlagged without="scaffolder-legacy">
|
||||
<Route
|
||||
path="/create"
|
||||
element={
|
||||
@@ -276,7 +276,7 @@ const routes = (
|
||||
}
|
||||
>
|
||||
<ScaffolderFieldExtensions>
|
||||
<LowerCaseValuePickerFieldExtension />
|
||||
<DelayingComponentFieldExtension />
|
||||
</ScaffolderFieldExtensions>
|
||||
<ScaffolderLayouts>
|
||||
<TwoColumnLayout />
|
||||
|
||||
@@ -17,10 +17,6 @@ import React from 'react';
|
||||
import type { FieldValidation } from '@rjsf/utils';
|
||||
import { scaffolderPlugin } from '@backstage/plugin-scaffolder';
|
||||
import { TextField } from '@material-ui/core';
|
||||
import {
|
||||
NextFieldExtensionComponentProps,
|
||||
createNextScaffolderFieldExtension,
|
||||
} from '@backstage/plugin-scaffolder-react/alpha';
|
||||
import {
|
||||
createScaffolderFieldExtension,
|
||||
FieldExtensionComponentProps,
|
||||
@@ -67,7 +63,7 @@ export const LowerCaseValuePickerFieldExtension = scaffolderPlugin.provide(
|
||||
);
|
||||
|
||||
const MockDelayComponent = (
|
||||
props: NextFieldExtensionComponentProps<{ test?: string }>,
|
||||
props: FieldExtensionComponentProps<{ test?: string }>,
|
||||
) => {
|
||||
const { onChange, formData, rawErrors = [] } = props;
|
||||
return (
|
||||
@@ -83,7 +79,7 @@ const MockDelayComponent = (
|
||||
};
|
||||
|
||||
export const DelayingComponentFieldExtension = scaffolderPlugin.provide(
|
||||
createNextScaffolderFieldExtension({
|
||||
createScaffolderFieldExtension({
|
||||
name: 'DelayingComponent',
|
||||
component: MockDelayComponent,
|
||||
validation: async (
|
||||
|
||||
@@ -46,8 +46,8 @@
|
||||
"@material-ui/core": "^4.12.2",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@material-ui/lab": "4.0.0-alpha.61",
|
||||
"@rjsf/core-v5": "npm:@rjsf/core@5.13.0",
|
||||
"@rjsf/material-ui-v5": "npm:@rjsf/material-ui@5.13.0",
|
||||
"@rjsf/core": "5.13.0",
|
||||
"@rjsf/material-ui": "5.13.0",
|
||||
"@rjsf/utils": "5.13.0",
|
||||
"@rjsf/validator-ajv8": "5.13.0",
|
||||
"@types/react": "^16.13.1 || ^17.0.0",
|
||||
|
||||
@@ -27,10 +27,11 @@ import SettingsIcon from '@material-ui/icons/Settings';
|
||||
import DeleteIcon from '@material-ui/icons/Delete';
|
||||
import React from 'react';
|
||||
import { Widget } from './types';
|
||||
import { withTheme } from '@rjsf/core-v5';
|
||||
import { withTheme } from '@rjsf/core';
|
||||
import { Theme as MuiTheme } from '@rjsf/material-ui';
|
||||
import validator from '@rjsf/validator-ajv8';
|
||||
|
||||
const Form = withTheme(require('@rjsf/material-ui-v5').Theme);
|
||||
const Form = withTheme(MuiTheme);
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
|
||||
@@ -6,13 +6,15 @@
|
||||
/// <reference types="react" />
|
||||
|
||||
import { ApiHolder } from '@backstage/core-plugin-api';
|
||||
import { ComponentType } from 'react';
|
||||
import { CustomFieldExtensionSchema } from '@backstage/plugin-scaffolder-react';
|
||||
import { CustomFieldValidator } from '@backstage/plugin-scaffolder-react';
|
||||
import { Dispatch } from 'react';
|
||||
import { Extension } from '@backstage/core-plugin-api';
|
||||
import { FieldExtensionComponent } from '@backstage/plugin-scaffolder-react';
|
||||
import { FieldProps } from '@rjsf/utils';
|
||||
import { FieldExtensionOptions } from '@backstage/plugin-scaffolder-react';
|
||||
import { FieldValidation } from '@rjsf/utils';
|
||||
import { FormProps as FormProps_2 } from '@rjsf/core-v5';
|
||||
import { FormProps } from '@backstage/plugin-scaffolder-react';
|
||||
import { IconComponent } from '@backstage/core-plugin-api';
|
||||
import { JsonObject } from '@backstage/types';
|
||||
import { JsonValue } from '@backstage/types';
|
||||
@@ -21,24 +23,39 @@ import { PropsWithChildren } from 'react';
|
||||
import { default as React_2 } from 'react';
|
||||
import { ReactElement } from 'react';
|
||||
import { ReactNode } from 'react';
|
||||
import { ReviewStepProps } from '@backstage/plugin-scaffolder-react';
|
||||
import { ScaffolderRJSFFieldProps } from '@backstage/plugin-scaffolder-react';
|
||||
import { ScaffolderRJSFFormProps } from '@backstage/plugin-scaffolder-react';
|
||||
import { ScaffolderStep } from '@backstage/plugin-scaffolder-react';
|
||||
import { ScaffolderTaskOutput } from '@backstage/plugin-scaffolder-react';
|
||||
import { SetStateAction } from 'react';
|
||||
import { TaskStep } from '@backstage/plugin-scaffolder-common';
|
||||
import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common';
|
||||
import { TemplateGroupFilter } from '@backstage/plugin-scaffolder-react';
|
||||
import { TemplateParameterSchema } from '@backstage/plugin-scaffolder-react';
|
||||
import { UIOptionsType } from '@rjsf/utils';
|
||||
import { UiSchema } from '@rjsf/utils';
|
||||
|
||||
// @alpha (undocumented)
|
||||
export const createAsyncValidators: (
|
||||
rootSchema: JsonObject,
|
||||
validators: Record<
|
||||
string,
|
||||
undefined | CustomFieldValidator<unknown, unknown>
|
||||
>,
|
||||
context: {
|
||||
apiHolder: ApiHolder;
|
||||
},
|
||||
) => (formData: JsonObject) => Promise<FormValidation>;
|
||||
|
||||
// @alpha
|
||||
export const createFieldValidation: () => FieldValidation;
|
||||
|
||||
// @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>>;
|
||||
|
||||
// @alpha
|
||||
@@ -57,59 +74,47 @@ export const extractSchemaFromStep: (inputStep: JsonObject) => {
|
||||
|
||||
// @alpha
|
||||
export const Form: (
|
||||
props: PropsWithChildren<FormProps_2>,
|
||||
props: PropsWithChildren<ScaffolderRJSFFormProps>,
|
||||
) => React_2.JSX.Element;
|
||||
|
||||
// @alpha
|
||||
export type FormProps = Pick<
|
||||
FormProps_2,
|
||||
'transformErrors' | 'noHtml5Validate'
|
||||
>;
|
||||
// @alpha (undocumented)
|
||||
export type FormValidation = {
|
||||
[name: string]: FieldValidation | FormValidation;
|
||||
};
|
||||
|
||||
// @alpha
|
||||
export type NextCustomFieldValidator<
|
||||
TFieldReturnValue,
|
||||
TUiOptions = unknown,
|
||||
> = (
|
||||
export type LegacyCustomFieldValidator<TFieldReturnValue> = (
|
||||
data: TFieldReturnValue,
|
||||
field: FieldValidation,
|
||||
context: {
|
||||
apiHolder: ApiHolder;
|
||||
formData: JsonObject;
|
||||
schema: JsonObject;
|
||||
uiSchema?: NextFieldExtensionUiSchema<TFieldReturnValue, TUiOptions>;
|
||||
},
|
||||
) => void | Promise<void>;
|
||||
|
||||
// @alpha
|
||||
export interface NextFieldExtensionComponentProps<
|
||||
export interface LegacyFieldExtensionComponentProps<
|
||||
TFieldReturnValue,
|
||||
TUiOptions = {},
|
||||
> extends PropsWithChildren<FieldProps<TFieldReturnValue>> {
|
||||
TUiOptions = unknown,
|
||||
> extends ScaffolderRJSFFieldProps<TFieldReturnValue> {
|
||||
// (undocumented)
|
||||
uiSchema?: NextFieldExtensionUiSchema<TFieldReturnValue, TUiOptions>;
|
||||
uiSchema: ScaffolderRJSFFieldProps['uiSchema'] & {
|
||||
'ui:options'?: TUiOptions;
|
||||
};
|
||||
}
|
||||
|
||||
// @alpha
|
||||
export type NextFieldExtensionOptions<
|
||||
export type LegacyFieldExtensionOptions<
|
||||
TFieldReturnValue = unknown,
|
||||
TUiOptions = unknown,
|
||||
TInputProps = unknown,
|
||||
> = {
|
||||
name: string;
|
||||
component: (
|
||||
props: NextFieldExtensionComponentProps<TFieldReturnValue, TUiOptions>,
|
||||
props: LegacyFieldExtensionComponentProps<TFieldReturnValue, TInputProps>,
|
||||
) => JSX.Element | null;
|
||||
validation?: NextCustomFieldValidator<TFieldReturnValue, TUiOptions>;
|
||||
validation?: LegacyCustomFieldValidator<TFieldReturnValue>;
|
||||
schema?: CustomFieldExtensionSchema;
|
||||
};
|
||||
|
||||
// @alpha
|
||||
export interface NextFieldExtensionUiSchema<TFieldReturnValue, TUiOptions>
|
||||
extends UiSchema<TFieldReturnValue> {
|
||||
// (undocumented)
|
||||
'ui:options'?: TUiOptions & UIOptionsType;
|
||||
}
|
||||
|
||||
// @alpha
|
||||
export interface ParsedTemplateSchema {
|
||||
// (undocumented)
|
||||
@@ -176,12 +181,13 @@ export const Stepper: (stepperProps: StepperProps) => React_2.JSX.Element;
|
||||
// @alpha
|
||||
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;
|
||||
createButtonText?: ReactNode;
|
||||
reviewButtonText?: ReactNode;
|
||||
@@ -236,12 +242,6 @@ export const TemplateGroup: (
|
||||
props: TemplateGroupProps,
|
||||
) => React_2.JSX.Element | null;
|
||||
|
||||
// @alpha (undocumented)
|
||||
export type TemplateGroupFilter = {
|
||||
title?: React_2.ReactNode;
|
||||
filter: (entity: TemplateEntityV1beta3) => boolean;
|
||||
};
|
||||
|
||||
// @alpha
|
||||
export interface TemplateGroupProps {
|
||||
// (undocumented)
|
||||
@@ -314,11 +314,14 @@ export type WorkflowProps = {
|
||||
description?: string;
|
||||
namespace: string;
|
||||
templateName: string;
|
||||
components?: {
|
||||
ReviewStepComponent?: React_2.ComponentType<ReviewStepProps>;
|
||||
};
|
||||
onError(error: Error | undefined): JSX.Element | null;
|
||||
} & Pick<
|
||||
StepperProps,
|
||||
| 'extensions'
|
||||
| 'FormProps'
|
||||
| 'formProps'
|
||||
| 'components'
|
||||
| 'onCreate'
|
||||
| 'initialState'
|
||||
|
||||
@@ -7,18 +7,42 @@
|
||||
|
||||
import { ApiHolder } from '@backstage/core-plugin-api';
|
||||
import { ApiRef } from '@backstage/core-plugin-api';
|
||||
import { ComponentType } from 'react';
|
||||
import { CustomValidator } from '@rjsf/utils';
|
||||
import { ElementType } from 'react';
|
||||
import { ErrorSchema } from '@rjsf/utils';
|
||||
import { ErrorTransformer } from '@rjsf/utils';
|
||||
import { Experimental_DefaultFormStateBehavior } from '@rjsf/utils';
|
||||
import { Extension } from '@backstage/core-plugin-api';
|
||||
import { FieldProps } from '@rjsf/core';
|
||||
import { FieldValidation } from '@rjsf/core';
|
||||
import type { FormProps } from '@rjsf/core-v5';
|
||||
import { FieldValidation } from '@rjsf/utils';
|
||||
import Form from '@rjsf/core';
|
||||
import { FormContextType } from '@rjsf/utils';
|
||||
import { FormEvent } from 'react';
|
||||
import type { FormProps as FormProps_2 } from '@rjsf/core';
|
||||
import { GenericObjectType } from '@rjsf/utils';
|
||||
import { HTMLAttributes } from 'react';
|
||||
import { IChangeEvent } from '@rjsf/core';
|
||||
import { IdSchema } from '@rjsf/utils';
|
||||
import { JsonObject } from '@backstage/types';
|
||||
import { JSONSchema7 } from 'json-schema';
|
||||
import { JsonValue } from '@backstage/types';
|
||||
import { Observable } from '@backstage/types';
|
||||
import { PropsWithChildren } from 'react';
|
||||
import { default as React_2 } from 'react';
|
||||
import { ReactNode } from 'react';
|
||||
import { Ref } from 'react';
|
||||
import { Registry } from '@rjsf/utils';
|
||||
import { RegistryWidgetsType } from '@rjsf/utils';
|
||||
import { RJSFSchema } from '@rjsf/utils';
|
||||
import { RJSFValidationError } from '@rjsf/utils';
|
||||
import { StrictRJSFSchema } from '@rjsf/utils';
|
||||
import { TaskSpec } from '@backstage/plugin-scaffolder-common';
|
||||
import { TaskStep } from '@backstage/plugin-scaffolder-common';
|
||||
import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common';
|
||||
import { TemplatesType } from '@rjsf/utils';
|
||||
import { UIOptionsType } from '@rjsf/utils';
|
||||
import { UiSchema } from '@rjsf/utils';
|
||||
import { ValidatorType } from '@rjsf/utils';
|
||||
|
||||
// @public
|
||||
export type Action = {
|
||||
@@ -40,7 +64,7 @@ export type ActionExample = {
|
||||
// @public
|
||||
export function createScaffolderFieldExtension<
|
||||
TReturnValue = unknown,
|
||||
TInputProps = unknown,
|
||||
TInputProps extends UIOptionsType = {},
|
||||
>(
|
||||
options: FieldExtensionOptions<TReturnValue, TInputProps>,
|
||||
): Extension<FieldExtensionComponent<TReturnValue, TInputProps>>;
|
||||
@@ -57,11 +81,14 @@ export type CustomFieldExtensionSchema = {
|
||||
};
|
||||
|
||||
// @public
|
||||
export type CustomFieldValidator<TFieldReturnValue> = (
|
||||
export type CustomFieldValidator<TFieldReturnValue, TUiOptions = unknown> = (
|
||||
data: TFieldReturnValue,
|
||||
field: FieldValidation,
|
||||
context: {
|
||||
apiHolder: ApiHolder;
|
||||
formData: JsonObject;
|
||||
schema: JsonObject;
|
||||
uiSchema?: FieldExtensionUiSchema<TFieldReturnValue, TUiOptions>;
|
||||
},
|
||||
) => void | Promise<void>;
|
||||
|
||||
@@ -71,27 +98,38 @@ export type FieldExtensionComponent<_TReturnValue, _TInputProps> = () => null;
|
||||
// @public
|
||||
export interface FieldExtensionComponentProps<
|
||||
TFieldReturnValue,
|
||||
TUiOptions = unknown,
|
||||
> extends FieldProps<TFieldReturnValue> {
|
||||
TUiOptions = {},
|
||||
> extends PropsWithChildren<ScaffolderRJSFFieldProps<TFieldReturnValue>> {
|
||||
// (undocumented)
|
||||
uiSchema: FieldProps['uiSchema'] & {
|
||||
'ui:options'?: TUiOptions;
|
||||
};
|
||||
uiSchema: FieldExtensionUiSchema<TFieldReturnValue, TUiOptions>;
|
||||
}
|
||||
|
||||
// @public
|
||||
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;
|
||||
};
|
||||
|
||||
// @public
|
||||
export interface FieldExtensionUiSchema<TFieldReturnValue, TUiOptions>
|
||||
extends UiSchema<TFieldReturnValue> {
|
||||
// (undocumented)
|
||||
'ui:options'?: TUiOptions & UIOptionsType<TFieldReturnValue>;
|
||||
}
|
||||
|
||||
// @public
|
||||
export type FormProps = Pick<
|
||||
FormProps_2,
|
||||
'transformErrors' | 'noHtml5Validate'
|
||||
>;
|
||||
|
||||
// @public
|
||||
export type LayoutComponent<_TInputProps> = () => null;
|
||||
|
||||
@@ -105,7 +143,7 @@ export interface LayoutOptions<P = any> {
|
||||
|
||||
// @public
|
||||
export type LayoutTemplate<T = any> = NonNullable<
|
||||
FormProps<T>['uiSchema']
|
||||
FormProps_2<T>['uiSchema']
|
||||
>['ui:ObjectFieldTemplate'];
|
||||
|
||||
// @public
|
||||
@@ -124,6 +162,20 @@ export type LogEvent = {
|
||||
taskId: string;
|
||||
};
|
||||
|
||||
// @public
|
||||
export type ReviewStepProps = {
|
||||
disableButtons: boolean;
|
||||
formData: JsonObject;
|
||||
handleBack: () => void;
|
||||
handleReset: () => void;
|
||||
handleCreate: () => void;
|
||||
steps: {
|
||||
uiSchema: UiSchema;
|
||||
mergedSchema: JsonObject;
|
||||
schema: JsonObject;
|
||||
}[];
|
||||
};
|
||||
|
||||
// @public
|
||||
export interface ScaffolderApi {
|
||||
cancelTask(taskId: string): Promise<void>;
|
||||
@@ -186,8 +238,8 @@ export interface ScaffolderDryRunResponse {
|
||||
}
|
||||
|
||||
// @public
|
||||
export const ScaffolderFieldExtensions: React_2.ComponentType<
|
||||
React_2.PropsWithChildren<{}>
|
||||
export const ScaffolderFieldExtensions: React.ComponentType<
|
||||
React.PropsWithChildren<{}>
|
||||
>;
|
||||
|
||||
// @public
|
||||
@@ -226,6 +278,113 @@ export type ScaffolderOutputText = {
|
||||
content?: string;
|
||||
};
|
||||
|
||||
// @public
|
||||
export type ScaffolderRJSFField<
|
||||
T = any,
|
||||
S extends StrictRJSFSchema = RJSFSchema,
|
||||
F extends FormContextType = any,
|
||||
> = ComponentType<ScaffolderRJSFFieldProps<T, S, F>>;
|
||||
|
||||
// @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'
|
||||
>
|
||||
> {
|
||||
autofocus?: boolean;
|
||||
disabled: boolean;
|
||||
errorSchema?: ErrorSchema<T>;
|
||||
formContext?: F;
|
||||
formData: T;
|
||||
hideError?: boolean;
|
||||
idPrefix?: string;
|
||||
idSchema: IdSchema<T>;
|
||||
idSeparator?: string;
|
||||
name: string;
|
||||
onBlur: (id: string, value: any) => void;
|
||||
onChange: (
|
||||
newFormData: T | undefined,
|
||||
es?: ErrorSchema<T>,
|
||||
id?: string,
|
||||
) => any;
|
||||
onFocus: (id: string, value: any) => void;
|
||||
rawErrors: string[];
|
||||
readonly: boolean;
|
||||
registry: Registry<T, S, F>;
|
||||
required?: boolean;
|
||||
schema: S;
|
||||
uiSchema: UiSchema<T, S, F>;
|
||||
}
|
||||
|
||||
// @public
|
||||
export interface ScaffolderRJSFFormProps<
|
||||
T = any,
|
||||
S extends StrictRJSFSchema = RJSFSchema,
|
||||
F extends FormContextType = any,
|
||||
> {
|
||||
acceptcharset?: string;
|
||||
action?: string;
|
||||
autoComplete?: string;
|
||||
children?: ReactNode;
|
||||
className?: string;
|
||||
customValidate?: CustomValidator<T, S, F>;
|
||||
disabled?: boolean;
|
||||
enctype?: string;
|
||||
experimental_defaultFormStateBehavior?: Experimental_DefaultFormStateBehavior;
|
||||
extraErrors?: ErrorSchema<T>;
|
||||
fields?: ScaffolderRJSFRegistryFieldsType<T, S, F>;
|
||||
focusOnFirstError?: boolean | ((error: RJSFValidationError) => void);
|
||||
formContext?: F;
|
||||
formData?: T;
|
||||
id?: string;
|
||||
idPrefix?: string;
|
||||
idSeparator?: string;
|
||||
_internalFormWrapper?: ElementType;
|
||||
liveOmit?: boolean;
|
||||
liveValidate?: boolean;
|
||||
method?: string;
|
||||
name?: string;
|
||||
noHtml5Validate?: boolean;
|
||||
// @deprecated
|
||||
noValidate?: boolean;
|
||||
omitExtraData?: boolean;
|
||||
onBlur?: (id: string, data: any) => void;
|
||||
onChange?: (data: IChangeEvent<T, S, F>, id?: string) => void;
|
||||
onError?: (errors: RJSFValidationError[]) => void;
|
||||
onFocus?: (id: string, data: any) => void;
|
||||
onSubmit?: (data: IChangeEvent<T, S, F>, event: FormEvent<any>) => void;
|
||||
readonly?: boolean;
|
||||
ref?: Ref<Form<T, S, F>>;
|
||||
schema: S;
|
||||
showErrorList?: false | 'top' | 'bottom';
|
||||
tagName?: ElementType;
|
||||
target?: string;
|
||||
templates?: Partial<Omit<TemplatesType<T, S, F>, 'ButtonTemplates'>> & {
|
||||
ButtonTemplates?: Partial<TemplatesType<T, S, F>['ButtonTemplates']>;
|
||||
};
|
||||
transformErrors?: ErrorTransformer<T, S, F>;
|
||||
translateString?: Registry['translateString'];
|
||||
uiSchema?: UiSchema<T, S, F>;
|
||||
validator: ValidatorType<T, S, F>;
|
||||
widgets?: RegistryWidgetsType<T, S, F>;
|
||||
}
|
||||
|
||||
// @public
|
||||
export type ScaffolderRJSFRegistryFieldsType<
|
||||
T = any,
|
||||
S extends StrictRJSFSchema = RJSFSchema,
|
||||
F extends FormContextType = any,
|
||||
> = {
|
||||
[name: string]: ScaffolderRJSFField<T, S, F>;
|
||||
};
|
||||
|
||||
// @public
|
||||
export interface ScaffolderScaffoldOptions {
|
||||
// (undocumented)
|
||||
@@ -313,6 +472,12 @@ export type TaskStream = {
|
||||
output?: ScaffolderTaskOutput;
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
export type TemplateGroupFilter = {
|
||||
title?: React.ReactNode;
|
||||
filter: (entity: TemplateEntityV1beta3) => boolean;
|
||||
};
|
||||
|
||||
// @public
|
||||
export type TemplateParameterSchema = {
|
||||
title: string;
|
||||
|
||||
@@ -60,10 +60,8 @@
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@material-ui/lab": "4.0.0-alpha.61",
|
||||
"@react-hookz/web": "^20.0.0",
|
||||
"@rjsf/core": "^3.2.1",
|
||||
"@rjsf/core-v5": "npm:@rjsf/core@5.13.0",
|
||||
"@rjsf/material-ui": "^3.2.1",
|
||||
"@rjsf/material-ui-v5": "npm:@rjsf/material-ui@5.13.0",
|
||||
"@rjsf/core": "5.13.0",
|
||||
"@rjsf/material-ui": "5.13.0",
|
||||
"@rjsf/utils": "5.13.0",
|
||||
"@rjsf/validator-ajv8": "5.13.0",
|
||||
"@types/json-schema": "^7.0.9",
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
/**
|
||||
|
||||
+9
-12
@@ -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"
|
||||
@@ -312,7 +312,7 @@ describe('Stepper', () => {
|
||||
manifest={manifest}
|
||||
extensions={[]}
|
||||
onCreate={jest.fn()}
|
||||
FormProps={{ transformErrors }}
|
||||
formProps={{ transformErrors }}
|
||||
/>,
|
||||
);
|
||||
|
||||
|
||||
@@ -23,10 +23,15 @@ 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,
|
||||
@@ -35,7 +40,6 @@ import { ReviewState, type ReviewStateProps } from '../ReviewState';
|
||||
import { useTemplateSchema } from '../../hooks/useTemplateSchema';
|
||||
import validator from '@rjsf/validator-ajv8';
|
||||
import { useFormDataFromQuery } from '../../hooks';
|
||||
import { FormProps } from '../../types';
|
||||
import { useTransformSchemaToProps } from '../../hooks/useTransformSchemaToProps';
|
||||
import { hasErrors } from './utils';
|
||||
import * as FieldOverrides from './FieldOverrides';
|
||||
@@ -43,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: {
|
||||
@@ -66,12 +73,13 @@ 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;
|
||||
createButtonText?: ReactNode;
|
||||
reviewButtonText?: ReactNode;
|
||||
@@ -87,6 +95,7 @@ export const Stepper = (stepperProps: StepperProps) => {
|
||||
const { layouts = [], components = {}, ...props } = stepperProps;
|
||||
const {
|
||||
ReviewStateComponent = ReviewState,
|
||||
ReviewStepComponent,
|
||||
createButtonText = 'Create',
|
||||
reviewButtonText = 'Review',
|
||||
} = components;
|
||||
@@ -128,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 ({
|
||||
@@ -171,6 +187,7 @@ export const Stepper = (stepperProps: StepperProps) => {
|
||||
</MuiStep>
|
||||
</MuiStepper>
|
||||
<div className={styles.formWrapper}>
|
||||
{/* eslint-disable-next-line no-nested-ternary */}
|
||||
{activeStep < steps.length ? (
|
||||
<Form
|
||||
validator={validator}
|
||||
@@ -183,7 +200,7 @@ export const Stepper = (stepperProps: StepperProps) => {
|
||||
fields={{ ...FieldOverrides, ...extensions }}
|
||||
showErrorList={false}
|
||||
onChange={handleChange}
|
||||
{...(props.FormProps ?? {})}
|
||||
{...(props.formProps ?? {})}
|
||||
>
|
||||
<div className={styles.footer}>
|
||||
<Button
|
||||
@@ -203,6 +220,16 @@ export const Stepper = (stepperProps: StepperProps) => {
|
||||
</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} />
|
||||
@@ -217,17 +244,7 @@ export const Stepper = (stepperProps: StepperProps) => {
|
||||
<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}
|
||||
>
|
||||
{createButtonText}
|
||||
</Button>
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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,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'
|
||||
>;
|
||||
@@ -5,50 +5,58 @@
|
||||
```ts
|
||||
/// <reference types="react" />
|
||||
|
||||
import { FormProps as FormProps_2 } from '@backstage/plugin-scaffolder-react/alpha';
|
||||
import type { FormProps as FormProps_3 } from '@rjsf/core-v5';
|
||||
import { ComponentType } from 'react';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { FieldExtensionOptions } from '@backstage/plugin-scaffolder-react';
|
||||
import type { FormProps as FormProps_2 } from '@rjsf/core';
|
||||
import { FormProps as FormProps_3 } from '@backstage/plugin-scaffolder-react';
|
||||
import { JSX as JSX_2 } from 'react';
|
||||
import { LayoutOptions } from '@backstage/plugin-scaffolder-react';
|
||||
import { NextFieldExtensionOptions } from '@backstage/plugin-scaffolder-react/alpha';
|
||||
import { PropsWithChildren } from 'react';
|
||||
import { default as React_2 } from 'react';
|
||||
import { ScaffolderTaskOutput } from '@backstage/plugin-scaffolder-react';
|
||||
import { ReviewStepProps } from '@backstage/plugin-scaffolder-react';
|
||||
import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common';
|
||||
import { TemplateGroupFilter } from '@backstage/plugin-scaffolder-react/alpha';
|
||||
import { TemplateGroupFilter } from '@backstage/plugin-scaffolder-react';
|
||||
|
||||
// @alpha @deprecated
|
||||
export type FormProps = Pick<
|
||||
FormProps_3,
|
||||
FormProps_2,
|
||||
'transformErrors' | 'noHtml5Validate'
|
||||
>;
|
||||
|
||||
// @alpha
|
||||
export type NextRouterProps = {
|
||||
export const LegacyRouter: (props: LegacyRouterProps) => React_2.JSX.Element;
|
||||
|
||||
// @alpha
|
||||
export type LegacyRouterProps = {
|
||||
components?: {
|
||||
TemplateCardComponent?: React_2.ComponentType<{
|
||||
template: TemplateEntityV1beta3;
|
||||
}>;
|
||||
TaskPageComponent?: React_2.ComponentType<PropsWithChildren<{}>>;
|
||||
TemplateOutputsComponent?: React_2.ComponentType<{
|
||||
output?: ScaffolderTaskOutput;
|
||||
}>;
|
||||
TemplateListPageComponent?: React_2.ComponentType<TemplateListPageProps>;
|
||||
TemplateWizardPageComponent?: React_2.ComponentType<TemplateWizardPageProps>;
|
||||
ReviewStepComponent?: ComponentType<ReviewStepProps>;
|
||||
TemplateCardComponent?:
|
||||
| ComponentType<{
|
||||
template: TemplateEntityV1beta3;
|
||||
}>
|
||||
| undefined;
|
||||
TaskPageComponent?: ComponentType<PropsWithChildren<{}>>;
|
||||
};
|
||||
groups?: TemplateGroupFilter[];
|
||||
groups?: Array<{
|
||||
title?: React_2.ReactNode;
|
||||
filter: (entity: Entity) => boolean;
|
||||
}>;
|
||||
templateFilter?: (entity: TemplateEntityV1beta3) => boolean;
|
||||
FormProps?: FormProps_2;
|
||||
defaultPreviewTemplate?: string;
|
||||
headerOptions?: {
|
||||
pageTitleOverride?: string;
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
};
|
||||
contextMenu?: {
|
||||
editor?: boolean;
|
||||
actions?: boolean;
|
||||
tasks?: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
// @alpha
|
||||
export const NextScaffolderPage: (
|
||||
props: PropsWithChildren<NextRouterProps>,
|
||||
) => JSX_2.Element;
|
||||
export const LegacyScaffolderPage: (props: LegacyRouterProps) => JSX_2.Element;
|
||||
|
||||
// @alpha (undocumented)
|
||||
export type TemplateListPageProps = {
|
||||
@@ -66,9 +74,12 @@ export type TemplateListPageProps = {
|
||||
|
||||
// @alpha (undocumented)
|
||||
export type TemplateWizardPageProps = {
|
||||
customFieldExtensions: NextFieldExtensionOptions<any, any>[];
|
||||
customFieldExtensions: FieldExtensionOptions<any, any>[];
|
||||
components?: {
|
||||
ReviewStepComponent?: React_2.ComponentType<ReviewStepProps>;
|
||||
};
|
||||
layouts?: LayoutOptions[];
|
||||
FormProps?: FormProps_2;
|
||||
formProps?: FormProps_3;
|
||||
};
|
||||
|
||||
// (No @packageDocumentation comment for this package)
|
||||
|
||||
@@ -14,15 +14,14 @@ import { createScaffolderLayout as createScaffolderLayout_2 } from '@backstage/p
|
||||
import { CustomFieldExtensionSchema as CustomFieldExtensionSchema_2 } from '@backstage/plugin-scaffolder-react';
|
||||
import { CustomFieldValidator as CustomFieldValidator_2 } from '@backstage/plugin-scaffolder-react';
|
||||
import { DiscoveryApi } from '@backstage/core-plugin-api';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { ExternalRouteRef } from '@backstage/core-plugin-api';
|
||||
import { FetchApi } from '@backstage/core-plugin-api';
|
||||
import { FieldExtensionComponent as FieldExtensionComponent_2 } from '@backstage/plugin-scaffolder-react';
|
||||
import { FieldExtensionComponentProps as FieldExtensionComponentProps_2 } from '@backstage/plugin-scaffolder-react';
|
||||
import { FieldExtensionOptions as FieldExtensionOptions_2 } from '@backstage/plugin-scaffolder-react';
|
||||
import { FieldValidation } from '@rjsf/core';
|
||||
import { FieldValidation } from '@rjsf/utils';
|
||||
import { FormProps } from '@backstage/plugin-scaffolder-react';
|
||||
import { IdentityApi } from '@backstage/core-plugin-api';
|
||||
import { JsonObject } from '@backstage/types';
|
||||
import { JSX as JSX_2 } from 'react';
|
||||
import { LayoutOptions as LayoutOptions_2 } from '@backstage/plugin-scaffolder-react';
|
||||
import { LayoutTemplate as LayoutTemplate_2 } from '@backstage/plugin-scaffolder-react';
|
||||
@@ -33,6 +32,7 @@ import { PathParams } from '@backstage/core-plugin-api';
|
||||
import { PropsWithChildren } from 'react';
|
||||
import { default as React_2 } from 'react';
|
||||
import { ReactNode } from 'react';
|
||||
import { ReviewStepProps } from '@backstage/plugin-scaffolder-react';
|
||||
import { RouteRef } from '@backstage/core-plugin-api';
|
||||
import { ScaffolderApi as ScaffolderApi_2 } from '@backstage/plugin-scaffolder-react';
|
||||
import { ScaffolderDryRunOptions as ScaffolderDryRunOptions_2 } from '@backstage/plugin-scaffolder-react';
|
||||
@@ -50,8 +50,10 @@ import { ScaffolderUseTemplateSecrets as ScaffolderUseTemplateSecrets_2 } from '
|
||||
import { ScmIntegrationRegistry } from '@backstage/integration';
|
||||
import { SubRouteRef } from '@backstage/core-plugin-api';
|
||||
import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common';
|
||||
import { TemplateGroupFilter } from '@backstage/plugin-scaffolder-react';
|
||||
import { TemplateListPageProps } from '@backstage/plugin-scaffolder/alpha';
|
||||
import { TemplateParameterSchema as TemplateParameterSchema_2 } from '@backstage/plugin-scaffolder-react';
|
||||
import { UiSchema } from '@rjsf/utils';
|
||||
import { TemplateWizardPageProps } from '@backstage/plugin-scaffolder/alpha';
|
||||
import { z } from 'zod';
|
||||
|
||||
// @public @deprecated (undocumented)
|
||||
@@ -419,19 +421,7 @@ export const RepoUrlPickerFieldSchema: FieldSchema<
|
||||
export type RepoUrlPickerUiOptions =
|
||||
typeof RepoUrlPickerFieldSchema.uiOptionsType;
|
||||
|
||||
// @public
|
||||
export type ReviewStepProps = {
|
||||
disableButtons: boolean;
|
||||
formData: JsonObject;
|
||||
handleBack: () => void;
|
||||
handleReset: () => void;
|
||||
handleCreate: () => void;
|
||||
steps: {
|
||||
uiSchema: UiSchema;
|
||||
mergedSchema: JsonObject;
|
||||
schema: JsonObject;
|
||||
}[];
|
||||
};
|
||||
export { ReviewStepProps };
|
||||
|
||||
// @public @deprecated (undocumented)
|
||||
export const rootRouteRef: RouteRef<undefined>;
|
||||
@@ -439,28 +429,30 @@ export const rootRouteRef: RouteRef<undefined>;
|
||||
// @public
|
||||
export type RouterProps = {
|
||||
components?: {
|
||||
ReviewStepComponent?: ComponentType<ReviewStepProps>;
|
||||
TemplateCardComponent?:
|
||||
| ComponentType<{
|
||||
template: TemplateEntityV1beta3;
|
||||
}>
|
||||
| undefined;
|
||||
TaskPageComponent?: ComponentType<PropsWithChildren<{}>>;
|
||||
ReviewStepComponent?: React_2.ComponentType<ReviewStepProps>;
|
||||
TemplateCardComponent?: React_2.ComponentType<{
|
||||
template: TemplateEntityV1beta3;
|
||||
}>;
|
||||
TaskPageComponent?: React_2.ComponentType<PropsWithChildren<{}>>;
|
||||
EXPERIMENTAL_TemplateOutputsComponent?: React_2.ComponentType<{
|
||||
output?: ScaffolderTaskOutput_2;
|
||||
}>;
|
||||
EXPERIMENTAL_TemplateListPageComponent?: React_2.ComponentType<TemplateListPageProps>;
|
||||
EXPERIMENTAL_TemplateWizardPageComponent?: React_2.ComponentType<TemplateWizardPageProps>;
|
||||
};
|
||||
groups?: Array<{
|
||||
title?: React_2.ReactNode;
|
||||
filter: (entity: Entity) => boolean;
|
||||
}>;
|
||||
groups?: TemplateGroupFilter[];
|
||||
templateFilter?: (entity: TemplateEntityV1beta3) => boolean;
|
||||
defaultPreviewTemplate?: string;
|
||||
headerOptions?: {
|
||||
pageTitleOverride?: string;
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
};
|
||||
defaultPreviewTemplate?: string;
|
||||
formProps?: FormProps;
|
||||
contextMenu?: {
|
||||
editor?: boolean;
|
||||
actions?: boolean;
|
||||
tasks?: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -537,7 +529,9 @@ export const ScaffolderLayouts: ComponentType<{
|
||||
export type ScaffolderOutputlink = ScaffolderOutputLink;
|
||||
|
||||
// @public
|
||||
export const ScaffolderPage: (props: RouterProps) => JSX_2.Element;
|
||||
export const ScaffolderPage: (
|
||||
props: PropsWithChildren<RouterProps>,
|
||||
) => JSX_2.Element;
|
||||
|
||||
// @public
|
||||
export const scaffolderPlugin: BackstagePlugin<
|
||||
@@ -585,10 +579,14 @@ export type ScaffolderTaskStatus = ScaffolderTaskStatus_2;
|
||||
// @public @deprecated (undocumented)
|
||||
export type ScaffolderUseTemplateSecrets = ScaffolderUseTemplateSecrets_2;
|
||||
|
||||
// @public
|
||||
export const TaskPage: (props: TaskPageProps) => React_2.JSX.Element;
|
||||
// @public (undocumented)
|
||||
export const TaskPage: (props: {
|
||||
TemplateOutputsComponent?: React_2.ComponentType<{
|
||||
output?: ScaffolderTaskOutput_2;
|
||||
}>;
|
||||
}) => React_2.JSX.Element;
|
||||
|
||||
// @public
|
||||
// @public @deprecated
|
||||
export type TaskPageProps = {
|
||||
loadingText?: string;
|
||||
};
|
||||
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
} from '@backstage/plugin-catalog-react';
|
||||
import React from 'react';
|
||||
import { scaffolderApiRef, ScaffolderClient } from '../src';
|
||||
import { NextScaffolderPage, ScaffolderPage } from '../src/plugin';
|
||||
import { ScaffolderPage, LegacyScaffolderPage } from '../src/plugin';
|
||||
import {
|
||||
discoveryApiRef,
|
||||
fetchApiRef,
|
||||
@@ -69,9 +69,9 @@ createDevApp()
|
||||
element: <ScaffolderPage />,
|
||||
})
|
||||
.addPage({
|
||||
path: '/next-create',
|
||||
path: '/legacy-create',
|
||||
title: 'Create (next)',
|
||||
element: <NextScaffolderPage />,
|
||||
element: <LegacyScaffolderPage />,
|
||||
})
|
||||
.addPage({
|
||||
path: '/create-groups',
|
||||
@@ -92,10 +92,10 @@ createDevApp()
|
||||
),
|
||||
})
|
||||
.addPage({
|
||||
path: '/next-create-groups',
|
||||
path: '/legacy-create-groups',
|
||||
title: 'Groups (next)',
|
||||
element: (
|
||||
<NextScaffolderPage
|
||||
<LegacyScaffolderPage
|
||||
groups={[
|
||||
{
|
||||
filter: e => e.metadata.tags?.includes('techdocs') || false,
|
||||
|
||||
@@ -68,8 +68,8 @@
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@material-ui/lab": "4.0.0-alpha.61",
|
||||
"@react-hookz/web": "^20.0.0",
|
||||
"@rjsf/core": "^3.2.1",
|
||||
"@rjsf/material-ui": "^3.2.1",
|
||||
"@rjsf/core": "5.13.0",
|
||||
"@rjsf/material-ui": "5.13.0",
|
||||
"@rjsf/utils": "5.13.0",
|
||||
"@rjsf/validator-ajv8": "5.13.0",
|
||||
"@types/react": "^16.13.1 || ^17.0.0",
|
||||
|
||||
@@ -14,10 +14,10 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { NextScaffolderPage } from './plugin';
|
||||
export {
|
||||
type NextRouterProps,
|
||||
type FormProps,
|
||||
type TemplateListPageProps,
|
||||
type TemplateWizardPageProps,
|
||||
} from './next';
|
||||
|
||||
export * from './legacy';
|
||||
|
||||
+3
@@ -51,6 +51,9 @@ const useStyles = makeStyles(theme => ({
|
||||
},
|
||||
}));
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export const OngoingTask = (props: {
|
||||
TemplateOutputsComponent?: React.ComponentType<{
|
||||
output?: ScaffolderTaskOutput;
|
||||
+8
-12
@@ -14,8 +14,6 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { TemplateListPage } from '../TemplateListPage';
|
||||
import { TemplateWizardPage } from '../TemplateWizardPage';
|
||||
import { Router } from './Router';
|
||||
import { renderInTestApp } from '@backstage/test-utils';
|
||||
import {
|
||||
@@ -27,13 +25,11 @@ import {
|
||||
createScaffolderLayout,
|
||||
ScaffolderLayouts,
|
||||
} from '@backstage/plugin-scaffolder-react';
|
||||
import { TemplateListPage, TemplateWizardPage } from '../../next';
|
||||
|
||||
jest.mock('../TemplateListPage', () => ({
|
||||
TemplateListPage: jest.fn(() => null),
|
||||
}));
|
||||
|
||||
jest.mock('../TemplateWizardPage', () => ({
|
||||
jest.mock('../../next', () => ({
|
||||
TemplateWizardPage: jest.fn(() => null),
|
||||
TemplateListPage: jest.fn(() => null),
|
||||
}));
|
||||
|
||||
describe('Router', () => {
|
||||
@@ -52,7 +48,7 @@ describe('Router', () => {
|
||||
const { getByText } = await renderInTestApp(
|
||||
<Router
|
||||
components={{
|
||||
TemplateListPageComponent: () => <>foobar</>,
|
||||
EXPERIMENTAL_TemplateListPageComponent: () => <>foobar</>,
|
||||
}}
|
||||
/>,
|
||||
{
|
||||
@@ -77,7 +73,7 @@ describe('Router', () => {
|
||||
const { getByText } = await renderInTestApp(
|
||||
<Router
|
||||
components={{
|
||||
TemplateWizardPageComponent: () => <>foobar</>,
|
||||
EXPERIMENTAL_TemplateWizardPageComponent: () => <>foobar</>,
|
||||
}}
|
||||
/>,
|
||||
{
|
||||
@@ -93,7 +89,7 @@ describe('Router', () => {
|
||||
|
||||
await renderInTestApp(
|
||||
<Router
|
||||
FormProps={{
|
||||
formProps={{
|
||||
transformErrors: transformErrorsMock,
|
||||
noHtml5Validate: true,
|
||||
}}
|
||||
@@ -105,9 +101,9 @@ describe('Router', () => {
|
||||
|
||||
const mock = TemplateWizardPage as jest.Mock;
|
||||
|
||||
const [{ FormProps }] = mock.mock.calls[0];
|
||||
const [{ formProps }] = mock.mock.calls[0];
|
||||
|
||||
expect(FormProps).toEqual({
|
||||
expect(formProps).toEqual({
|
||||
transformErrors: transformErrorsMock,
|
||||
noHtml5Validate: true,
|
||||
});
|
||||
+38
-24
@@ -15,16 +15,13 @@
|
||||
*/
|
||||
import React, { PropsWithChildren } from 'react';
|
||||
import { Routes, Route, useOutlet } from 'react-router-dom';
|
||||
import { TemplateListPage, TemplateListPageProps } from '../TemplateListPage';
|
||||
|
||||
import {
|
||||
TemplateWizardPage,
|
||||
TemplateWizardPageProps,
|
||||
} from '../TemplateWizardPage';
|
||||
import {
|
||||
NextFieldExtensionOptions,
|
||||
FieldExtensionOptions,
|
||||
FormProps,
|
||||
ReviewStepProps,
|
||||
TemplateGroupFilter,
|
||||
} from '@backstage/plugin-scaffolder-react/alpha';
|
||||
} from '@backstage/plugin-scaffolder-react';
|
||||
import {
|
||||
ScaffolderTaskOutput,
|
||||
SecretsContextProvider,
|
||||
@@ -43,32 +40,45 @@ import {
|
||||
selectedTemplateRouteRef,
|
||||
} from '../../routes';
|
||||
import { ErrorPage } from '@backstage/core-components';
|
||||
import { OngoingTask } from '../OngoingTask';
|
||||
|
||||
import { ActionsPage } from '../../components/ActionsPage';
|
||||
import { ListTasksPage } from '../../components/ListTasksPage';
|
||||
import { TemplateEditorPage } from '../TemplateEditorPage';
|
||||
|
||||
import {
|
||||
TemplateListPageProps,
|
||||
TemplateWizardPageProps,
|
||||
} from '@backstage/plugin-scaffolder/alpha';
|
||||
import { TemplateListPage, TemplateWizardPage } from '../../next';
|
||||
import { OngoingTask } from '../OngoingTask';
|
||||
import { TemplateEditorPage } from '../../next/TemplateEditorPage';
|
||||
|
||||
/**
|
||||
* The Props for the Scaffolder Router
|
||||
*
|
||||
* @alpha
|
||||
* @public
|
||||
*/
|
||||
export type NextRouterProps = {
|
||||
export type RouterProps = {
|
||||
components?: {
|
||||
ReviewStepComponent?: React.ComponentType<ReviewStepProps>;
|
||||
TemplateCardComponent?: React.ComponentType<{
|
||||
template: TemplateEntityV1beta3;
|
||||
}>;
|
||||
TaskPageComponent?: React.ComponentType<PropsWithChildren<{}>>;
|
||||
TemplateOutputsComponent?: React.ComponentType<{
|
||||
EXPERIMENTAL_TemplateOutputsComponent?: React.ComponentType<{
|
||||
output?: ScaffolderTaskOutput;
|
||||
}>;
|
||||
TemplateListPageComponent?: React.ComponentType<TemplateListPageProps>;
|
||||
TemplateWizardPageComponent?: React.ComponentType<TemplateWizardPageProps>;
|
||||
EXPERIMENTAL_TemplateListPageComponent?: React.ComponentType<TemplateListPageProps>;
|
||||
EXPERIMENTAL_TemplateWizardPageComponent?: React.ComponentType<TemplateWizardPageProps>;
|
||||
};
|
||||
groups?: TemplateGroupFilter[];
|
||||
templateFilter?: (entity: TemplateEntityV1beta3) => boolean;
|
||||
// todo(blam): rename this to formProps
|
||||
FormProps?: FormProps;
|
||||
headerOptions?: {
|
||||
pageTitleOverride?: string;
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
};
|
||||
defaultPreviewTemplate?: string;
|
||||
formProps?: FormProps;
|
||||
contextMenu?: {
|
||||
/** Whether to show a link to the template editor */
|
||||
editor?: boolean;
|
||||
@@ -82,21 +92,24 @@ export type NextRouterProps = {
|
||||
/**
|
||||
* The Scaffolder Router
|
||||
*
|
||||
* @alpha
|
||||
* @public
|
||||
*/
|
||||
export const Router = (props: PropsWithChildren<NextRouterProps>) => {
|
||||
export const Router = (props: PropsWithChildren<RouterProps>) => {
|
||||
const {
|
||||
components: {
|
||||
TemplateCardComponent,
|
||||
TemplateOutputsComponent,
|
||||
TaskPageComponent = OngoingTask,
|
||||
TemplateListPageComponent = TemplateListPage,
|
||||
TemplateWizardPageComponent = TemplateWizardPage,
|
||||
ReviewStepComponent,
|
||||
EXPERIMENTAL_TemplateOutputsComponent: TemplateOutputsComponent,
|
||||
EXPERIMENTAL_TemplateListPageComponent:
|
||||
TemplateListPageComponent = TemplateListPage,
|
||||
EXPERIMENTAL_TemplateWizardPageComponent:
|
||||
TemplateWizardPageComponent = TemplateWizardPage,
|
||||
} = {},
|
||||
} = props;
|
||||
const outlet = useOutlet() || props.children;
|
||||
const customFieldExtensions =
|
||||
useCustomFieldExtensions<NextFieldExtensionOptions>(outlet);
|
||||
useCustomFieldExtensions<FieldExtensionOptions>(outlet);
|
||||
|
||||
const fieldExtensions = [
|
||||
...customFieldExtensions,
|
||||
@@ -106,7 +119,7 @@ export const Router = (props: PropsWithChildren<NextRouterProps>) => {
|
||||
customFieldExtension => customFieldExtension.name === name,
|
||||
),
|
||||
),
|
||||
] as NextFieldExtensionOptions[];
|
||||
] as FieldExtensionOptions[];
|
||||
|
||||
const customLayouts = useCustomLayouts(outlet);
|
||||
|
||||
@@ -130,7 +143,8 @@ export const Router = (props: PropsWithChildren<NextRouterProps>) => {
|
||||
<TemplateWizardPageComponent
|
||||
customFieldExtensions={fieldExtensions}
|
||||
layouts={customLayouts}
|
||||
FormProps={props.FormProps}
|
||||
components={{ ReviewStepComponent }}
|
||||
formProps={props.formProps}
|
||||
/>
|
||||
</SecretsContextProvider>
|
||||
}
|
||||
+1
-2
@@ -13,5 +13,4 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
export { Router } from './Router';
|
||||
export type { NextRouterProps } from './Router';
|
||||
export { Router, type RouterProps } from './Router';
|
||||
@@ -1,58 +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 { catalogApiRef } from '@backstage/plugin-catalog-react';
|
||||
import { renderInTestApp, TestApiProvider } from '@backstage/test-utils';
|
||||
import { screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import React from 'react';
|
||||
import { TemplateEditorPage } from './TemplateEditorPage';
|
||||
|
||||
describe('TemplateEditorPage', () => {
|
||||
it('renders without exploding', async () => {
|
||||
await renderInTestApp(<TemplateEditorPage />);
|
||||
|
||||
expect(screen.getByText('Load Template Directory')).toBeInTheDocument();
|
||||
expect(screen.getByText('Edit Template Form')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('template directory loading should not be supported in Jest', async () => {
|
||||
await renderInTestApp(<TemplateEditorPage />);
|
||||
|
||||
expect(
|
||||
screen.getByRole('button', { name: /Load Template Directory/ }),
|
||||
).toBeDisabled();
|
||||
});
|
||||
|
||||
it('should be able to continue to form preview', async () => {
|
||||
await renderInTestApp(
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[
|
||||
catalogApiRef,
|
||||
{ getEntities: jest.fn().mockResolvedValue({ items: [] }) },
|
||||
],
|
||||
]}
|
||||
>
|
||||
<TemplateEditorPage />
|
||||
</TestApiProvider>,
|
||||
);
|
||||
|
||||
await userEvent.click(screen.getByText('Edit Template Form'));
|
||||
|
||||
expect(screen.getByLabelText('Load Existing Template')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { FieldValidation } from '@rjsf/core';
|
||||
import { FieldValidation } from '@rjsf/utils';
|
||||
import { KubernetesValidatorFunctions } from '@backstage/catalog-model';
|
||||
import { entityNamePickerValidation } from './validation';
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { FieldValidation } from '@rjsf/core';
|
||||
import { FieldValidation } from '@rjsf/utils';
|
||||
import { KubernetesValidatorFunctions } from '@backstage/catalog-model';
|
||||
|
||||
export const entityNamePickerValidation = (
|
||||
|
||||
@@ -18,11 +18,12 @@ import { CATALOG_FILTER_EXISTS } from '@backstage/catalog-client';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog-react';
|
||||
import { renderInTestApp, TestApiProvider } from '@backstage/test-utils';
|
||||
import { FieldProps } from '@rjsf/core';
|
||||
|
||||
import { fireEvent, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { EntityPicker } from './EntityPicker';
|
||||
import { EntityPickerProps } from './schema';
|
||||
import { ScaffolderRJSFFieldProps as FieldProps } from '@backstage/plugin-scaffolder-react';
|
||||
|
||||
const makeEntity = (kind: string, namespace: string, name: string): Entity => ({
|
||||
apiVersion: 'scaffolder.backstage.io/v1beta3',
|
||||
@@ -39,7 +40,7 @@ describe('<EntityPicker />', () => {
|
||||
const rawErrors: string[] = [];
|
||||
const formData = undefined;
|
||||
|
||||
let props: FieldProps;
|
||||
let props: FieldProps<string>;
|
||||
|
||||
const catalogApi: jest.Mocked<CatalogApi> = {
|
||||
getLocationById: jest.fn(),
|
||||
@@ -76,7 +77,7 @@ describe('<EntityPicker />', () => {
|
||||
uiSchema,
|
||||
rawErrors,
|
||||
formData,
|
||||
} as unknown as FieldProps<any>;
|
||||
} as unknown as FieldProps;
|
||||
|
||||
catalogApi.getEntities.mockResolvedValue({ items: entities });
|
||||
});
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
import React from 'react';
|
||||
import { render, waitFor } from '@testing-library/react';
|
||||
import { CatalogApi } from '@backstage/catalog-client';
|
||||
import { FieldProps } from '@rjsf/core';
|
||||
import { MyGroupsPicker } from './MyGroupsPicker';
|
||||
import { TestApiProvider } from '@backstage/test-utils';
|
||||
import { catalogApiRef } from '@backstage/plugin-catalog-react';
|
||||
@@ -29,6 +28,7 @@ import {
|
||||
identityApiRef,
|
||||
} from '@backstage/core-plugin-api';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { ScaffolderRJSFFieldProps as FieldProps } from '@backstage/plugin-scaffolder-react';
|
||||
|
||||
// Create a mock IdentityApi
|
||||
const mockIdentityApi: IdentityApi = {
|
||||
@@ -109,7 +109,7 @@ describe('<MyGroupsPicker />', () => {
|
||||
onChange,
|
||||
schema,
|
||||
required,
|
||||
} as unknown as FieldProps<any>;
|
||||
} as unknown as FieldProps<string>;
|
||||
|
||||
render(
|
||||
<TestApiProvider
|
||||
@@ -181,7 +181,7 @@ describe('<MyGroupsPicker />', () => {
|
||||
onChange,
|
||||
schema,
|
||||
required,
|
||||
} as unknown as FieldProps<any>;
|
||||
} as unknown as FieldProps<string>;
|
||||
|
||||
const { queryByText, getByRole } = render(
|
||||
<TestApiProvider
|
||||
@@ -238,7 +238,7 @@ describe('<MyGroupsPicker />', () => {
|
||||
onChange,
|
||||
schema,
|
||||
required,
|
||||
} as unknown as FieldProps<any>;
|
||||
} as unknown as FieldProps<string>;
|
||||
|
||||
const { getByRole } = render(
|
||||
<TestApiProvider
|
||||
|
||||
@@ -18,7 +18,7 @@ import { type EntityFilterQuery } from '@backstage/catalog-client';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog-react';
|
||||
import { renderInTestApp, TestApiProvider } from '@backstage/test-utils';
|
||||
import { FieldProps } from '@rjsf/core';
|
||||
import { ScaffolderRJSFFieldProps as FieldProps } from '@backstage/plugin-scaffolder-react';
|
||||
import React from 'react';
|
||||
import { OwnerPicker } from './OwnerPicker';
|
||||
|
||||
@@ -45,7 +45,7 @@ describe('<OwnerPicker />', () => {
|
||||
const rawErrors: string[] = [];
|
||||
const formData = undefined;
|
||||
|
||||
let props: FieldProps;
|
||||
let props: FieldProps<string>;
|
||||
|
||||
const catalogApi: jest.Mocked<CatalogApi> = {
|
||||
getLocationById: jest.fn(),
|
||||
|
||||
@@ -15,7 +15,8 @@
|
||||
*/
|
||||
import React from 'react';
|
||||
import { RepoUrlPicker } from './RepoUrlPicker';
|
||||
import Form from '@rjsf/core';
|
||||
import { Form } from '@backstage/plugin-scaffolder-react/alpha';
|
||||
import validator from '@rjsf/validator-ajv8';
|
||||
import { renderInTestApp, TestApiProvider } from '@backstage/test-utils';
|
||||
import {
|
||||
scmIntegrationsApiRef,
|
||||
@@ -29,6 +30,7 @@ import {
|
||||
scaffolderApiRef,
|
||||
ScaffolderApi,
|
||||
useTemplateSecrets,
|
||||
ScaffolderRJSFField,
|
||||
} from '@backstage/plugin-scaffolder-react';
|
||||
import { act, fireEvent } from '@testing-library/react';
|
||||
|
||||
@@ -73,9 +75,12 @@ describe('RepoUrlPicker', () => {
|
||||
>
|
||||
<SecretsContextProvider>
|
||||
<Form
|
||||
validator={validator}
|
||||
schema={{ type: 'string' }}
|
||||
uiSchema={{ 'ui:field': 'RepoUrlPicker' }}
|
||||
fields={{ RepoUrlPicker: RepoUrlPicker }}
|
||||
fields={{
|
||||
RepoUrlPicker: RepoUrlPicker as ScaffolderRJSFField<string>,
|
||||
}}
|
||||
onSubmit={onSubmit}
|
||||
/>
|
||||
</SecretsContextProvider>
|
||||
@@ -109,12 +114,15 @@ describe('RepoUrlPicker', () => {
|
||||
>
|
||||
<SecretsContextProvider>
|
||||
<Form
|
||||
validator={validator}
|
||||
schema={{ type: 'string' }}
|
||||
uiSchema={{
|
||||
'ui:field': 'RepoUrlPicker',
|
||||
'ui:options': { allowedHosts: ['dev.azure.com'] },
|
||||
}}
|
||||
fields={{ RepoUrlPicker: RepoUrlPicker }}
|
||||
fields={{
|
||||
RepoUrlPicker: RepoUrlPicker as ScaffolderRJSFField<string>,
|
||||
}}
|
||||
/>
|
||||
</SecretsContextProvider>
|
||||
</TestApiProvider>,
|
||||
@@ -144,6 +152,7 @@ describe('RepoUrlPicker', () => {
|
||||
>
|
||||
<SecretsContextProvider>
|
||||
<Form
|
||||
validator={validator}
|
||||
schema={{ type: 'string' }}
|
||||
uiSchema={{
|
||||
'ui:field': 'RepoUrlPicker',
|
||||
@@ -154,7 +163,9 @@ describe('RepoUrlPicker', () => {
|
||||
},
|
||||
},
|
||||
}}
|
||||
fields={{ RepoUrlPicker: RepoUrlPicker }}
|
||||
fields={{
|
||||
RepoUrlPicker: RepoUrlPicker as ScaffolderRJSFField<string>,
|
||||
}}
|
||||
/>
|
||||
<SecretsComponent />
|
||||
</SecretsContextProvider>
|
||||
@@ -206,6 +217,7 @@ describe('RepoUrlPicker', () => {
|
||||
>
|
||||
<SecretsContextProvider>
|
||||
<Form
|
||||
validator={validator}
|
||||
schema={{ type: 'string' }}
|
||||
uiSchema={{
|
||||
'ui:field': 'RepoUrlPicker',
|
||||
@@ -216,7 +228,9 @@ describe('RepoUrlPicker', () => {
|
||||
},
|
||||
},
|
||||
}}
|
||||
fields={{ RepoUrlPicker: RepoUrlPicker }}
|
||||
fields={{
|
||||
RepoUrlPicker: RepoUrlPicker as ScaffolderRJSFField<string>,
|
||||
}}
|
||||
/>
|
||||
<SecretsComponent />
|
||||
</SecretsContextProvider>
|
||||
@@ -259,6 +273,7 @@ describe('RepoUrlPicker', () => {
|
||||
>
|
||||
<SecretsContextProvider>
|
||||
<Form
|
||||
validator={validator}
|
||||
schema={{ type: 'string' }}
|
||||
uiSchema={{
|
||||
'ui:field': 'RepoUrlPicker',
|
||||
@@ -269,7 +284,9 @@ describe('RepoUrlPicker', () => {
|
||||
},
|
||||
},
|
||||
}}
|
||||
fields={{ RepoUrlPicker: RepoUrlPicker }}
|
||||
fields={{
|
||||
RepoUrlPicker: RepoUrlPicker as ScaffolderRJSFField<string>,
|
||||
}}
|
||||
/>
|
||||
<SecretsComponent />
|
||||
</SecretsContextProvider>
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
|
||||
import { repoPickerValidation } from './validation';
|
||||
import { FieldValidation } from '@rjsf/core';
|
||||
import { FieldValidation } from '@rjsf/utils';
|
||||
import { ScmIntegrations } from '@backstage/integration';
|
||||
import { ConfigReader } from '@backstage/core-app-api';
|
||||
import { ApiHolder } from '@backstage/core-plugin-api';
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { FieldValidation } from '@rjsf/core';
|
||||
import { FieldValidation } from '@rjsf/utils';
|
||||
import { ApiHolder } from '@backstage/core-plugin-api';
|
||||
import { scmIntegrationsApiRef } from '@backstage/integration-react';
|
||||
|
||||
|
||||
@@ -15,7 +15,12 @@
|
||||
*/
|
||||
export * from './fields';
|
||||
export type { RepoUrlPickerUiOptions } from './fields';
|
||||
|
||||
export { TemplateTypePicker } from './TemplateTypePicker';
|
||||
export { TaskPage, type TaskPageProps } from './TaskPage';
|
||||
|
||||
export type { RouterProps } from './Router';
|
||||
export type { ReviewStepProps } from './types';
|
||||
export { OngoingTask as TaskPage } from './OngoingTask';
|
||||
|
||||
export type { ReviewStepProps } from '@backstage/plugin-scaffolder-react';
|
||||
|
||||
export type { TaskPageProps } from '../legacy/TaskPage';
|
||||
|
||||
@@ -13,25 +13,3 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { UiSchema } from '@rjsf/utils';
|
||||
import { JsonObject } from '@backstage/types';
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}[];
|
||||
};
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@
|
||||
|
||||
import React from 'react';
|
||||
import { MarkdownContent } from '@backstage/core-components';
|
||||
import { FieldProps } from '@rjsf/core';
|
||||
import { FieldProps } from '@rjsf/utils';
|
||||
|
||||
export const DescriptionField = ({ description }: FieldProps) =>
|
||||
description && <MarkdownContent content={description} linkTarget="_blank" />;
|
||||
+8
-4
@@ -29,17 +29,20 @@ import {
|
||||
useRouteRefParams,
|
||||
useApi,
|
||||
} from '@backstage/core-plugin-api';
|
||||
import { FormProps, IChangeEvent, ISubmitEvent, withTheme } from '@rjsf/core';
|
||||
import { FormProps, IChangeEvent, withTheme } from '@rjsf/core';
|
||||
import { Theme as MuiTheme } from '@rjsf/material-ui';
|
||||
import React, { ComponentType, useState } from 'react';
|
||||
import { transformSchemaToProps } from './schema';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import * as fieldOverrides from './FieldOverrides';
|
||||
import { ReviewStepProps } from '../types';
|
||||
import { ReviewStep } from './ReviewStep';
|
||||
import validator from '@rjsf/validator-ajv8';
|
||||
import { extractSchemaFromStep } from '@backstage/plugin-scaffolder-react/alpha';
|
||||
import { selectedTemplateRouteRef } from '../../routes';
|
||||
import { LayoutOptions } from '@backstage/plugin-scaffolder-react';
|
||||
import {
|
||||
LayoutOptions,
|
||||
ReviewStepProps,
|
||||
} from '@backstage/plugin-scaffolder-react';
|
||||
|
||||
const Form = withTheme(MuiTheme);
|
||||
|
||||
@@ -178,6 +181,7 @@ export const MultistepJsonForm = (props: MultistepJsonFormProps) => {
|
||||
</StepLabel>
|
||||
<StepContent key={title}>
|
||||
<Form
|
||||
validator={validator}
|
||||
showErrorList={false}
|
||||
fields={{ ...fieldOverrides, ...fields }}
|
||||
widgets={widgets}
|
||||
@@ -185,7 +189,7 @@ export const MultistepJsonForm = (props: MultistepJsonFormProps) => {
|
||||
formData={formData}
|
||||
formContext={{ formData }}
|
||||
onChange={onChange}
|
||||
onSubmit={(e: ISubmitEvent<any>) => {
|
||||
onSubmit={(e: IChangeEvent<any>) => {
|
||||
if (e.errors.length === 0) handleNext();
|
||||
}}
|
||||
{...formProps}
|
||||
+2
-2
@@ -16,9 +16,9 @@
|
||||
import { Box, Button, Paper, Typography } from '@material-ui/core';
|
||||
import React from 'react';
|
||||
import { Content, StructuredMetadataTable } from '@backstage/core-components';
|
||||
import { UiSchema } from '@rjsf/core';
|
||||
import { UiSchema } from '@rjsf/utils';
|
||||
import { JsonObject } from '@backstage/types';
|
||||
import { ReviewStepProps } from '../types';
|
||||
import { ReviewStepProps } from '@backstage/plugin-scaffolder-react';
|
||||
|
||||
export function getReviewData(
|
||||
formData: Record<string, any>,
|
||||
+3
-3
@@ -15,8 +15,9 @@
|
||||
*/
|
||||
|
||||
import { JsonObject } from '@backstage/types';
|
||||
import { FormProps, UiSchema } from '@rjsf/core';
|
||||
import { UiSchema } from '@rjsf/utils';
|
||||
import type { LayoutOptions } from '@backstage/plugin-scaffolder-react';
|
||||
import { FormProps } from '@rjsf/core';
|
||||
|
||||
function isObject(value: unknown): value is JsonObject {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
||||
@@ -120,8 +121,7 @@ export function transformSchemaToProps(
|
||||
)?.component;
|
||||
|
||||
if (Layout) {
|
||||
uiSchema['ui:ObjectFieldTemplate'] =
|
||||
Layout as unknown as FormProps<any>['ObjectFieldTemplate'];
|
||||
uiSchema['ui:ObjectFieldTemplate'] = Layout;
|
||||
}
|
||||
}
|
||||
|
||||
+14
-12
@@ -21,18 +21,17 @@ import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common';
|
||||
import { ScaffolderPage } from './ScaffolderPage';
|
||||
import { TemplatePage } from './TemplatePage';
|
||||
import { TaskPage } from './TaskPage';
|
||||
import { ActionsPage } from './ActionsPage';
|
||||
import { TemplateEditorPage } from './TemplateEditorPage';
|
||||
import { ActionsPage } from '../components/ActionsPage';
|
||||
import { DEFAULT_SCAFFOLDER_FIELD_EXTENSIONS } from '../extensions/default';
|
||||
import { useRouteRef, useRouteRefParams } from '@backstage/core-plugin-api';
|
||||
import { LegacyFieldExtensionOptions } from '@backstage/plugin-scaffolder-react/alpha';
|
||||
import {
|
||||
FieldExtensionOptions,
|
||||
ReviewStepProps,
|
||||
SecretsContextProvider,
|
||||
useCustomFieldExtensions,
|
||||
useCustomLayouts,
|
||||
} from '@backstage/plugin-scaffolder-react';
|
||||
import { ListTasksPage } from './ListTasksPage';
|
||||
import { ReviewStepProps } from './types';
|
||||
import { ListTasksPage } from '../components/ListTasksPage';
|
||||
import {
|
||||
actionsRouteRef,
|
||||
editRouteRef,
|
||||
@@ -41,12 +40,13 @@ import {
|
||||
scaffolderTaskRouteRef,
|
||||
selectedTemplateRouteRef,
|
||||
} from '../routes';
|
||||
import { TemplateEditorPage } from './TemplateEditorPage';
|
||||
|
||||
/**
|
||||
* The props for the entrypoint `ScaffolderPage` component the plugin.
|
||||
* @public
|
||||
* @alpha
|
||||
*/
|
||||
export type RouterProps = {
|
||||
export type LegacyRouterProps = {
|
||||
components?: {
|
||||
ReviewStepComponent?: ComponentType<ReviewStepProps>;
|
||||
TemplateCardComponent?:
|
||||
@@ -77,11 +77,11 @@ export type RouterProps = {
|
||||
};
|
||||
|
||||
/**
|
||||
* The main entrypoint `Router` for the `ScaffolderPlugin`.
|
||||
* The legacy router
|
||||
*
|
||||
* @public
|
||||
* @alpha
|
||||
*/
|
||||
export const Router = (props: RouterProps) => {
|
||||
export const LegacyRouter = (props: LegacyRouterProps) => {
|
||||
const {
|
||||
groups,
|
||||
templateFilter,
|
||||
@@ -95,7 +95,9 @@ export const Router = (props: RouterProps) => {
|
||||
const outlet = useOutlet();
|
||||
const TaskPageElement = TaskPageComponent ?? TaskPage;
|
||||
|
||||
const customFieldExtensions = useCustomFieldExtensions(outlet);
|
||||
const customFieldExtensions =
|
||||
useCustomFieldExtensions<LegacyFieldExtensionOptions>(outlet);
|
||||
|
||||
const fieldExtensions = [
|
||||
...customFieldExtensions,
|
||||
...DEFAULT_SCAFFOLDER_FIELD_EXTENSIONS.filter(
|
||||
@@ -104,7 +106,7 @@ export const Router = (props: RouterProps) => {
|
||||
customFieldExtension => customFieldExtension.name === name,
|
||||
),
|
||||
),
|
||||
] as FieldExtensionOptions[];
|
||||
] as LegacyFieldExtensionOptions[];
|
||||
|
||||
const customLayouts = useCustomLayouts(outlet);
|
||||
|
||||
+1
-1
@@ -34,11 +34,11 @@ import {
|
||||
} from '@backstage/plugin-catalog-react';
|
||||
import React, { ComponentType } from 'react';
|
||||
import { TemplateList } from '../TemplateList';
|
||||
import { TemplateTypePicker } from '../TemplateTypePicker';
|
||||
import { catalogEntityCreatePermission } from '@backstage/plugin-catalog-common/alpha';
|
||||
import { usePermission } from '@backstage/plugin-permission-react';
|
||||
import { ScaffolderPageContextMenu } from './ScaffolderPageContextMenu';
|
||||
import { registerComponentRouteRef } from '../../routes';
|
||||
import { TemplateTypePicker } from '../../components';
|
||||
|
||||
export type ScaffolderPageProps = {
|
||||
TemplateCardComponent?:
|
||||
+2
-2
@@ -236,8 +236,8 @@ const hasLinks = ({ links = [] }: ScaffolderTaskOutput): boolean =>
|
||||
/**
|
||||
* TaskPageProps for constructing a TaskPage
|
||||
* @param loadingText - Optional loading text shown before a task begins executing.
|
||||
*
|
||||
* @public
|
||||
* @deprecated - this is a useless type that is no longer used.
|
||||
*/
|
||||
export type TaskPageProps = {
|
||||
loadingText?: string;
|
||||
@@ -246,7 +246,7 @@ export type TaskPageProps = {
|
||||
/**
|
||||
* TaskPage for showing the status of the taskId provided as a param
|
||||
*
|
||||
* @public
|
||||
* @alpha
|
||||
*/
|
||||
export const TaskPage = (props: TaskPageProps) => {
|
||||
const { loadingText } = props;
|
||||
+14
-21
@@ -28,16 +28,13 @@ import {
|
||||
Select,
|
||||
} from '@material-ui/core';
|
||||
import CloseIcon from '@material-ui/icons/Close';
|
||||
import { ISubmitEvent, withTheme } from '@rjsf/core';
|
||||
import { Theme as MuiTheme } from '@rjsf/material-ui';
|
||||
import CodeMirror from '@uiw/react-codemirror';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import yaml from 'yaml';
|
||||
import { FieldExtensionOptions } from '@backstage/plugin-scaffolder-react';
|
||||
import * as fieldOverrides from '../MultistepJsonForm/FieldOverrides';
|
||||
import { Form } from '@backstage/plugin-scaffolder-react/alpha';
|
||||
import { TemplateEditorForm } from './TemplateEditorForm';
|
||||
|
||||
const Form = withTheme(MuiTheme);
|
||||
import validator from '@rjsf/validator-ajv8';
|
||||
import { LegacyFieldExtensionOptions } from '@backstage/plugin-scaffolder-react/alpha';
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
root: {
|
||||
@@ -69,15 +66,15 @@ export const CustomFieldExplorer = ({
|
||||
customFieldExtensions = [],
|
||||
onClose,
|
||||
}: {
|
||||
customFieldExtensions?: FieldExtensionOptions<any, any>[];
|
||||
customFieldExtensions?: LegacyFieldExtensionOptions<any, any>[];
|
||||
onClose?: () => void;
|
||||
}) => {
|
||||
const classes = useStyles();
|
||||
const fieldOptions = customFieldExtensions.filter(field => !!field.schema);
|
||||
const [selectedField, setSelectedField] = useState(fieldOptions[0]);
|
||||
const [fieldFormState, setFieldFormState] = useState({});
|
||||
const [formState, setFormState] = useState({});
|
||||
const [refreshKey, setRefreshKey] = useState(Date.now());
|
||||
const [formState, setFormState] = useState({});
|
||||
const sampleFieldTemplate = useMemo(
|
||||
() =>
|
||||
yaml.stringify({
|
||||
@@ -104,18 +101,17 @@ export const CustomFieldExplorer = ({
|
||||
}, [customFieldExtensions]);
|
||||
|
||||
const handleSelectionChange = useCallback(
|
||||
(selection: FieldExtensionOptions) => {
|
||||
selection => {
|
||||
setSelectedField(selection);
|
||||
setFieldFormState({});
|
||||
setFormState({});
|
||||
},
|
||||
[setFieldFormState, setFormState, setSelectedField],
|
||||
[setFieldFormState, setSelectedField, setFormState],
|
||||
);
|
||||
|
||||
const handleFieldConfigChange = useCallback(
|
||||
(state: {}) => {
|
||||
state => {
|
||||
setFieldFormState(state);
|
||||
setFormState({});
|
||||
// Force TemplateEditorForm to re-render since some fields
|
||||
// may not be responsive to ui:option changes
|
||||
setRefreshKey(Date.now());
|
||||
@@ -134,9 +130,7 @@ export const CustomFieldExplorer = ({
|
||||
value={selectedField}
|
||||
label="Choose Custom Field Extension"
|
||||
labelId="select-field-label"
|
||||
onChange={e =>
|
||||
handleSelectionChange(e.target.value as FieldExtensionOptions)
|
||||
}
|
||||
onChange={e => handleSelectionChange(e.target.value)}
|
||||
>
|
||||
{fieldOptions.map((option, idx) => (
|
||||
<MenuItem key={idx} value={option as any}>
|
||||
@@ -156,13 +150,12 @@ export const CustomFieldExplorer = ({
|
||||
<CardContent>
|
||||
<Form
|
||||
showErrorList={false}
|
||||
fields={{ ...fieldOverrides, ...fieldComponents }}
|
||||
fields={{ ...fieldComponents }}
|
||||
noHtml5Validate
|
||||
formData={fieldFormState}
|
||||
formContext={{ fieldFormState }}
|
||||
onSubmit={(e: ISubmitEvent<any>) =>
|
||||
handleFieldConfigChange(e.formData)
|
||||
}
|
||||
onSubmit={e => handleFieldConfigChange(e.formData)}
|
||||
validator={validator}
|
||||
schema={selectedField.schema?.uiOptions || {}}
|
||||
>
|
||||
<Button
|
||||
@@ -191,12 +184,12 @@ export const CustomFieldExplorer = ({
|
||||
</CardContent>
|
||||
</Card>
|
||||
<TemplateEditorForm
|
||||
data={formState}
|
||||
onUpdate={setFormState}
|
||||
key={refreshKey}
|
||||
content={sampleFieldTemplate}
|
||||
contentIsSpec
|
||||
fieldExtensions={customFieldExtensions}
|
||||
data={formState}
|
||||
onUpdate={setFormState}
|
||||
setErrorText={() => null}
|
||||
/>
|
||||
</div>
|
||||
+8
-10
@@ -15,17 +15,15 @@
|
||||
*/
|
||||
import { makeStyles } from '@material-ui/core';
|
||||
import React, { useState } from 'react';
|
||||
import type {
|
||||
FieldExtensionOptions,
|
||||
LayoutOptions,
|
||||
} from '@backstage/plugin-scaffolder-react';
|
||||
import { LegacyFieldExtensionOptions } from '@backstage/plugin-scaffolder-react/alpha';
|
||||
import { TemplateDirectoryAccess } from '../../lib/filesystem';
|
||||
import { DirectoryEditorProvider } from './DirectoryEditorContext';
|
||||
import { DryRunProvider } from './DryRunContext';
|
||||
import { DryRunResults } from './DryRunResults';
|
||||
import { TemplateEditorBrowser } from './TemplateEditorBrowser';
|
||||
import { LayoutOptions } from '@backstage/plugin-scaffolder-react';
|
||||
import { DirectoryEditorProvider } from '../../next/TemplateEditorPage/DirectoryEditorContext';
|
||||
import { DryRunProvider } from '../../next/TemplateEditorPage/DryRunContext';
|
||||
import { TemplateEditorBrowser } from '../../next/TemplateEditorPage/TemplateEditorBrowser';
|
||||
import { TemplateEditorTextArea } from '../../next/TemplateEditorPage/TemplateEditorTextArea';
|
||||
import { TemplateEditorForm } from './TemplateEditorForm';
|
||||
import { TemplateEditorTextArea } from './TemplateEditorTextArea';
|
||||
import { DryRunResults } from '../../next/TemplateEditorPage/DryRunResults';
|
||||
|
||||
const useStyles = makeStyles({
|
||||
// Reset and fix sizing to make sure scrolling behaves correctly
|
||||
@@ -59,7 +57,7 @@ const useStyles = makeStyles({
|
||||
|
||||
export const TemplateEditor = (props: {
|
||||
directory: TemplateDirectoryAccess;
|
||||
fieldExtensions?: FieldExtensionOptions<any, any>[];
|
||||
fieldExtensions?: LegacyFieldExtensionOptions<any, any>[];
|
||||
layouts?: LayoutOptions[];
|
||||
onClose?: () => void;
|
||||
}) => {
|
||||
+7
-6
@@ -19,15 +19,16 @@ import { makeStyles } from '@material-ui/core/styles';
|
||||
import React, { Component, ReactNode, useMemo, useState } from 'react';
|
||||
import useDebounce from 'react-use/lib/useDebounce';
|
||||
import yaml from 'yaml';
|
||||
import type {
|
||||
FieldExtensionOptions,
|
||||
import {
|
||||
LayoutOptions,
|
||||
TemplateParameterSchema,
|
||||
} from '@backstage/plugin-scaffolder-react';
|
||||
|
||||
import { useDryRun } from '../../next/TemplateEditorPage/DryRunContext';
|
||||
import { useDirectoryEditor } from '../../next/TemplateEditorPage/DirectoryEditorContext';
|
||||
import { MultistepJsonForm } from '../MultistepJsonForm';
|
||||
import { createValidator } from '../TemplatePage';
|
||||
import { useDirectoryEditor } from './DirectoryEditorContext';
|
||||
import { useDryRun } from './DryRunContext';
|
||||
import { LegacyFieldExtensionOptions } from '@backstage/plugin-scaffolder-react/alpha';
|
||||
|
||||
const useStyles = makeStyles({
|
||||
containerWrapper: {
|
||||
@@ -85,7 +86,7 @@ interface TemplateEditorFormProps {
|
||||
setErrorText: (errorText?: string) => void;
|
||||
|
||||
onDryRun?: (data: JsonObject) => Promise<void>;
|
||||
fieldExtensions?: FieldExtensionOptions<any, any>[];
|
||||
fieldExtensions?: LegacyFieldExtensionOptions<any, any>[];
|
||||
layouts?: LayoutOptions[];
|
||||
}
|
||||
|
||||
@@ -191,7 +192,7 @@ export function TemplateEditorForm(props: TemplateEditorFormProps) {
|
||||
<ErrorBoundary invalidator={steps} setErrorText={setErrorText}>
|
||||
<MultistepJsonForm
|
||||
steps={steps}
|
||||
fields={fields}
|
||||
fields={fields as any}
|
||||
formData={data}
|
||||
onChange={e => onUpdate(e.formData)}
|
||||
onReset={() => onUpdate({})}
|
||||
+5
-7
@@ -19,14 +19,12 @@ import {
|
||||
TemplateDirectoryAccess,
|
||||
WebFileSystemAccess,
|
||||
} from '../../lib/filesystem';
|
||||
import { CustomFieldExplorer } from './CustomFieldExplorer';
|
||||
import { TemplateEditorIntro } from './TemplateEditorIntro';
|
||||
import { type LayoutOptions } from '@backstage/plugin-scaffolder-react';
|
||||
import { LegacyFieldExtensionOptions } from '@backstage/plugin-scaffolder-react/alpha';
|
||||
import { TemplateEditorIntro } from '../../next/TemplateEditorPage/TemplateEditorIntro';
|
||||
import { TemplateEditor } from './TemplateEditor';
|
||||
import { TemplateFormPreviewer } from './TemplateFormPreviewer';
|
||||
import {
|
||||
type FieldExtensionOptions,
|
||||
type LayoutOptions,
|
||||
} from '@backstage/plugin-scaffolder-react';
|
||||
import { CustomFieldExplorer } from './CustomFieldExplorer';
|
||||
|
||||
type Selection =
|
||||
| {
|
||||
@@ -42,7 +40,7 @@ type Selection =
|
||||
|
||||
interface TemplateEditorPageProps {
|
||||
defaultPreviewTemplate?: string;
|
||||
customFieldExtensions?: FieldExtensionOptions<any, any>[];
|
||||
customFieldExtensions?: LegacyFieldExtensionOptions<any, any>[];
|
||||
layouts?: LayoutOptions[];
|
||||
}
|
||||
|
||||
+8
-11
@@ -32,12 +32,10 @@ import CloseIcon from '@material-ui/icons/Close';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import useAsync from 'react-use/lib/useAsync';
|
||||
import yaml from 'yaml';
|
||||
import {
|
||||
type FieldExtensionOptions,
|
||||
type LayoutOptions,
|
||||
} from '@backstage/plugin-scaffolder-react';
|
||||
import { LayoutOptions } from '@backstage/plugin-scaffolder-react';
|
||||
import { LegacyFieldExtensionOptions } from '@backstage/plugin-scaffolder-react/alpha';
|
||||
import { TemplateEditorTextArea } from '../../next/TemplateEditorPage/TemplateEditorTextArea';
|
||||
import { TemplateEditorForm } from './TemplateEditorForm';
|
||||
import { TemplateEditorTextArea } from './TemplateEditorTextArea';
|
||||
|
||||
const EXAMPLE_TEMPLATE_PARAMS_YAML = `# Edit the template parameters below to see how they will render in the scaffolder form UI
|
||||
parameters:
|
||||
@@ -116,7 +114,7 @@ export const TemplateFormPreviewer = ({
|
||||
layouts = [],
|
||||
}: {
|
||||
defaultPreviewTemplate?: string;
|
||||
customFieldExtensions?: FieldExtensionOptions<any, any>[];
|
||||
customFieldExtensions?: LegacyFieldExtensionOptions<any, any>[];
|
||||
onClose?: () => void;
|
||||
layouts?: LayoutOptions[];
|
||||
}) => {
|
||||
@@ -164,8 +162,7 @@ export const TemplateFormPreviewer = ({
|
||||
);
|
||||
|
||||
const handleSelectChange = useCallback(
|
||||
// TODO(Rugvip): Afaik this should be Entity, but didn't want to make runtime changes while fixing types
|
||||
(selected: any) => {
|
||||
selected => {
|
||||
setSelectedTemplate(selected);
|
||||
setTemplateYaml(yaml.stringify(selected.spec));
|
||||
},
|
||||
@@ -195,7 +192,7 @@ export const TemplateFormPreviewer = ({
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<IconButton size="medium" onClick={onClose} aria-label="Close">
|
||||
<IconButton size="medium" onClick={onClose}>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
</div>
|
||||
@@ -208,11 +205,11 @@ export const TemplateFormPreviewer = ({
|
||||
</div>
|
||||
<div className={classes.preview}>
|
||||
<TemplateEditorForm
|
||||
data={formState}
|
||||
onUpdate={setFormState}
|
||||
content={templateYaml}
|
||||
contentIsSpec
|
||||
fieldExtensions={customFieldExtensions}
|
||||
data={formState}
|
||||
onUpdate={setFormState}
|
||||
setErrorText={setErrorText}
|
||||
layouts={layouts}
|
||||
/>
|
||||
+1
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2022 The Backstage Authors
|
||||
* 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.
|
||||
@@ -13,5 +13,4 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { TemplateEditorPage } from './TemplateEditorPage';
|
||||
+4
-4
@@ -20,10 +20,10 @@ import React, { ComponentType, useCallback, useState } from 'react';
|
||||
import { Navigate, useNavigate } from 'react-router-dom';
|
||||
import useAsync from 'react-use/lib/useAsync';
|
||||
import {
|
||||
type FieldExtensionOptions,
|
||||
type LayoutOptions,
|
||||
scaffolderApiRef,
|
||||
useTemplateSecrets,
|
||||
ReviewStepProps,
|
||||
} from '@backstage/plugin-scaffolder-react';
|
||||
import { MultistepJsonForm } from '../MultistepJsonForm';
|
||||
import { createValidator } from './createValidator';
|
||||
@@ -38,12 +38,12 @@ import {
|
||||
useRouteRefParams,
|
||||
} from '@backstage/core-plugin-api';
|
||||
import { stringifyEntityRef } from '@backstage/catalog-model';
|
||||
import { ReviewStepProps } from '../types';
|
||||
import {
|
||||
rootRouteRef,
|
||||
scaffolderTaskRouteRef,
|
||||
selectedTemplateRouteRef,
|
||||
} from '../../routes';
|
||||
import { LegacyFieldExtensionOptions } from '@backstage/plugin-scaffolder-react/alpha';
|
||||
|
||||
const useTemplateParameterSchema = (templateRef: string) => {
|
||||
const scaffolderApi = useApi(scaffolderApiRef);
|
||||
@@ -56,7 +56,7 @@ const useTemplateParameterSchema = (templateRef: string) => {
|
||||
|
||||
type Props = {
|
||||
ReviewStepComponent?: ComponentType<ReviewStepProps>;
|
||||
customFieldExtensions?: FieldExtensionOptions<any, any>[];
|
||||
customFieldExtensions?: LegacyFieldExtensionOptions<any, any>[];
|
||||
layouts?: LayoutOptions[];
|
||||
headerOptions?: {
|
||||
pageTitleOverride?: string;
|
||||
@@ -162,7 +162,7 @@ export const TemplatePage = ({
|
||||
<MultistepJsonForm
|
||||
ReviewStepComponent={ReviewStepComponent}
|
||||
formData={formState}
|
||||
fields={customFieldComponents}
|
||||
fields={customFieldComponents as any}
|
||||
onChange={handleChange}
|
||||
onReset={handleFormReset}
|
||||
onFinish={handleCreate}
|
||||
+47
-45
@@ -15,9 +15,9 @@
|
||||
*/
|
||||
|
||||
import { createValidator } from './createValidator';
|
||||
import { CustomFieldValidator } from '@backstage/plugin-scaffolder-react';
|
||||
import { LegacyCustomFieldValidator } from '@backstage/plugin-scaffolder-react/alpha';
|
||||
import { ApiHolder } from '@backstage/core-plugin-api';
|
||||
import { FieldValidation, FormValidation } from '@rjsf/core';
|
||||
import { FieldValidation, FormValidation } from '@rjsf/utils';
|
||||
|
||||
type CustomLinkType = {
|
||||
url: string;
|
||||
@@ -26,49 +26,51 @@ type CustomLinkType = {
|
||||
};
|
||||
|
||||
describe('createValidator', () => {
|
||||
const validators: Record<string, undefined | CustomFieldValidator<unknown>> =
|
||||
{
|
||||
CustomPicker: (
|
||||
value: unknown,
|
||||
fieldValidation: FieldValidation,
|
||||
_context: { apiHolder: ApiHolder },
|
||||
) => {
|
||||
if (!value || !(value as { value?: unknown }).value) {
|
||||
fieldValidation.addError('Error !');
|
||||
}
|
||||
},
|
||||
CustomLink: (
|
||||
values: unknown,
|
||||
fieldValidation: FieldValidation,
|
||||
_context: { apiHolder: ApiHolder },
|
||||
) => {
|
||||
const input = values as CustomLinkType[];
|
||||
for (const item of input) {
|
||||
const validGitlabUrlRegex =
|
||||
/gitlab\.(?:stg\.)?spotify\.com\?owner=.*&repo=.*/;
|
||||
const validators: Record<
|
||||
string,
|
||||
undefined | LegacyCustomFieldValidator<unknown>
|
||||
> = {
|
||||
CustomPicker: (
|
||||
value: unknown,
|
||||
fieldValidation: FieldValidation,
|
||||
_context: { apiHolder: ApiHolder },
|
||||
) => {
|
||||
if (!value || !(value as { value?: unknown }).value) {
|
||||
fieldValidation.addError('Error !');
|
||||
}
|
||||
},
|
||||
CustomLink: (
|
||||
values: unknown,
|
||||
fieldValidation: FieldValidation,
|
||||
_context: { apiHolder: ApiHolder },
|
||||
) => {
|
||||
const input = values as CustomLinkType[];
|
||||
for (const item of input) {
|
||||
const validGitlabUrlRegex =
|
||||
/gitlab\.(?:stg\.)?spotify\.com\?owner=.*&repo=.*/;
|
||||
|
||||
if (!item || !validGitlabUrlRegex.test(item.url)) {
|
||||
fieldValidation.addError(
|
||||
`Make sure to put in a valid gitlab clone url.`,
|
||||
);
|
||||
}
|
||||
if (!item || !validGitlabUrlRegex.test(item.url)) {
|
||||
fieldValidation.addError(
|
||||
`Make sure to put in a valid gitlab clone url.`,
|
||||
);
|
||||
}
|
||||
},
|
||||
TagPicker: (
|
||||
values: unknown,
|
||||
fieldValidation: FieldValidation,
|
||||
_context: { apiHolder: ApiHolder },
|
||||
) => {
|
||||
const input = values as string[];
|
||||
for (const item of input) {
|
||||
if (!/^[a-z0-9-]+$/.test(item)) {
|
||||
fieldValidation.addError(
|
||||
'A tag name can only contain lowercase letters, numeric characters or dashes',
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
TagPicker: (
|
||||
values: unknown,
|
||||
fieldValidation: FieldValidation,
|
||||
_context: { apiHolder: ApiHolder },
|
||||
) => {
|
||||
const input = values as string[];
|
||||
for (const item of input) {
|
||||
if (!/^[a-z0-9-]+$/.test(item)) {
|
||||
fieldValidation.addError(
|
||||
'A tag name can only contain lowercase letters, numeric characters or dashes',
|
||||
);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const apiHolderMock: jest.Mocked<ApiHolder> = {
|
||||
get: jest.fn().mockImplementation(() => {
|
||||
@@ -109,7 +111,7 @@ describe('createValidator', () => {
|
||||
|
||||
/* THEN */
|
||||
expect(result).not.toBeNull();
|
||||
expect(result.p1.addError).toHaveBeenCalledTimes(1);
|
||||
expect(result.p1?.addError).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should call validator for array property from a custom field extension', () => {
|
||||
@@ -144,7 +146,7 @@ describe('createValidator', () => {
|
||||
|
||||
/* THEN */
|
||||
expect(result).not.toBeNull();
|
||||
expect(result.tags.addError).toHaveBeenCalledTimes(1);
|
||||
expect(result.tags?.addError).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should call validator for array object property from a custom field extension', () => {
|
||||
@@ -193,6 +195,6 @@ describe('createValidator', () => {
|
||||
|
||||
/* THEN */
|
||||
expect(result).not.toBeNull();
|
||||
expect(result.links.addError).toHaveBeenCalledTimes(1);
|
||||
expect(result.links?.addError).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
+4
-4
@@ -14,8 +14,8 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { CustomFieldValidator } from '@backstage/plugin-scaffolder-react';
|
||||
import { FormValidation } from '@rjsf/core';
|
||||
import { LegacyCustomFieldValidator } from '@backstage/plugin-scaffolder-react/alpha';
|
||||
import { FieldValidation, FormValidation } from '@rjsf/utils';
|
||||
import { JsonObject, JsonValue } from '@backstage/types';
|
||||
import { ApiHolder } from '@backstage/core-plugin-api';
|
||||
|
||||
@@ -29,7 +29,7 @@ function isArray(obj: unknown): obj is JsonObject {
|
||||
|
||||
export const createValidator = (
|
||||
rootSchema: JsonObject,
|
||||
validators: Record<string, undefined | CustomFieldValidator<unknown>>,
|
||||
validators: Record<string, undefined | LegacyCustomFieldValidator<unknown>>,
|
||||
context: {
|
||||
apiHolder: ApiHolder;
|
||||
},
|
||||
@@ -56,7 +56,7 @@ export const createValidator = (
|
||||
if (fieldName && typeof validators[fieldName] === 'function') {
|
||||
validators[fieldName]!(
|
||||
propData as JsonObject[],
|
||||
propValidation,
|
||||
propValidation as FieldValidation,
|
||||
context,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* 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 { LegacyRouter, type LegacyRouterProps } from './Router';
|
||||
export { LegacyScaffolderPage } from '../plugin';
|
||||
@@ -31,12 +31,10 @@ import CloseIcon from '@material-ui/icons/Close';
|
||||
import CodeMirror from '@uiw/react-codemirror';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import yaml from 'yaml';
|
||||
import {
|
||||
NextFieldExtensionOptions,
|
||||
Form,
|
||||
} from '@backstage/plugin-scaffolder-react/alpha';
|
||||
import { Form } from '@backstage/plugin-scaffolder-react/alpha';
|
||||
import { TemplateEditorForm } from './TemplateEditorForm';
|
||||
import validator from '@rjsf/validator-ajv8';
|
||||
import { FieldExtensionOptions } from '@backstage/plugin-scaffolder-react';
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
root: {
|
||||
@@ -68,7 +66,7 @@ export const CustomFieldExplorer = ({
|
||||
customFieldExtensions = [],
|
||||
onClose,
|
||||
}: {
|
||||
customFieldExtensions?: NextFieldExtensionOptions<any, any>[];
|
||||
customFieldExtensions?: FieldExtensionOptions<any, any>[];
|
||||
onClose?: () => void;
|
||||
}) => {
|
||||
const classes = useStyles();
|
||||
@@ -102,7 +100,7 @@ export const CustomFieldExplorer = ({
|
||||
}, [customFieldExtensions]);
|
||||
|
||||
const handleSelectionChange = useCallback(
|
||||
(selection: NextFieldExtensionOptions) => {
|
||||
(selection: FieldExtensionOptions) => {
|
||||
setSelectedField(selection);
|
||||
setFieldFormState({});
|
||||
},
|
||||
@@ -131,7 +129,7 @@ export const CustomFieldExplorer = ({
|
||||
label="Choose Custom Field Extension"
|
||||
labelId="select-field-label"
|
||||
onChange={e =>
|
||||
handleSelectionChange(e.target.value as NextFieldExtensionOptions)
|
||||
handleSelectionChange(e.target.value as FieldExtensionOptions)
|
||||
}
|
||||
>
|
||||
{fieldOptions.map((option, idx) => (
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user