diff --git a/.changeset/hot-badgers-double.md b/.changeset/hot-badgers-double.md new file mode 100644 index 0000000000..835420b91a --- /dev/null +++ b/.changeset/hot-badgers-double.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-react': minor +--- + +`scaffolder/next`: Provide some default template components to `rjsf` to allow for standardization and markdown descriptions diff --git a/.changeset/twenty-suns-breathe.md b/.changeset/twenty-suns-breathe.md new file mode 100644 index 0000000000..edc189a95e --- /dev/null +++ b/.changeset/twenty-suns-breathe.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-react': minor +--- + +`scaffolder/next`: provide a `ScaffolderField` component which is meant to replace some of the `FormControl` components from MUI, making it easier to write `FieldExtensions`. diff --git a/plugins/scaffolder-react/alpha-api-report.md b/plugins/scaffolder-react/alpha-api-report.md index 6c22a5caa9..f988eb0831 100644 --- a/plugins/scaffolder-react/alpha-api-report.md +++ b/plugins/scaffolder-react/alpha-api-report.md @@ -6,7 +6,6 @@ /// import { ApiHolder } from '@backstage/core-plugin-api'; -import { ComponentType } from 'react'; import { CustomFieldExtensionSchema } from '@backstage/plugin-scaffolder-react'; import { Dispatch } from 'react'; import { Extension } from '@backstage/core-plugin-api'; @@ -20,8 +19,8 @@ import { JsonValue } from '@backstage/types'; import { LayoutOptions } from '@backstage/plugin-scaffolder-react'; import { PropsWithChildren } from 'react'; import { default as React_2 } from 'react'; +import { ReactElement } from 'react'; import { ReactNode } from 'react'; -import { RJSFSchema } from '@rjsf/utils'; import { ScaffolderStep } from '@backstage/plugin-scaffolder-react'; import { ScaffolderTaskOutput } from '@backstage/plugin-scaffolder-react'; import { SetStateAction } from 'react'; @@ -56,8 +55,8 @@ export const extractSchemaFromStep: (inputStep: JsonObject) => { schema: JsonObject; }; -// @alpha (undocumented) -export const Form: ComponentType>; +// @alpha +export const Form: (props: PropsWithChildren) => JSX.Element; // @alpha export type FormProps = Pick< @@ -132,6 +131,31 @@ export type ReviewStateProps = { formState: JsonObject; }; +// @alpha +export const ScaffolderField: ( + props: PropsWithChildren, +) => JSX.Element; + +// @alpha +export interface ScaffolderFieldProps { + // (undocumented) + disabled?: boolean; + // (undocumented) + displayLabel?: boolean; + // (undocumented) + errors?: ReactElement; + // (undocumented) + help?: ReactElement; + // (undocumented) + rawDescription?: string; + // (undocumented) + rawErrors?: string[]; + // (undocumented) + rawHelp?: string; + // (undocumented) + required?: boolean; +} + // @alpha (undocumented) export function ScaffolderPageContextMenu( props: ScaffolderPageContextMenuProps, diff --git a/plugins/scaffolder-react/src/next/components/Form/DescriptionFieldTemplate.tsx b/plugins/scaffolder-react/src/next/components/Form/DescriptionFieldTemplate.tsx new file mode 100644 index 0000000000..8debfb3e08 --- /dev/null +++ b/plugins/scaffolder-react/src/next/components/Form/DescriptionFieldTemplate.tsx @@ -0,0 +1,71 @@ +/* + * 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 React from 'react'; + +import { MarkdownContent } from '@backstage/core-components'; +import { makeStyles, Typography } from '@material-ui/core'; +import { + DescriptionFieldProps, + FormContextType, + RJSFSchema, + StrictRJSFSchema, +} from '@rjsf/utils'; + +const useStyles = makeStyles({ + markdownDescription: { + fontSize: '0.75rem', + margin: 0, + marginTop: '5px', + color: 'rgba(0, 0, 0, 0.54)', + '& p:first-child': { + margin: 0, + }, + }, +}); + +/** The `DescriptionField` is the template to use to render the description of a field + * @alpha + * @param props - The `DescriptionFieldProps` for this component + */ +export const DescriptionFieldTemplate = < + T = any, + S extends StrictRJSFSchema = RJSFSchema, + F extends FormContextType = any, +>( + props: DescriptionFieldProps, +) => { + const { id, description } = props; + const classes = useStyles(); + + if (description) { + if (typeof description === 'string') { + return ( + + ); + } + + return ( + + {description} + + ); + } + + return null; +}; diff --git a/plugins/scaffolder-react/src/next/components/Form/FieldTemplate.tsx b/plugins/scaffolder-react/src/next/components/Form/FieldTemplate.tsx new file mode 100644 index 0000000000..79cac33054 --- /dev/null +++ b/plugins/scaffolder-react/src/next/components/Form/FieldTemplate.tsx @@ -0,0 +1,102 @@ +/* + * 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 React from 'react'; + +import { + FieldTemplateProps, + FormContextType, + RJSFSchema, + StrictRJSFSchema, + getTemplate, + getUiOptions, +} from '@rjsf/utils'; + +import { ScaffolderField } from '../ScaffolderField'; + +/** The `FieldTemplate` component is the template used by `SchemaField` to render any field. It renders the field + * content, (label, description, children, errors and help) inside of a `WrapIfAdditional` component. + * @alpha + * @param props - The `FieldTemplateProps` for this component + */ +export const FieldTemplate = < + T = any, + S extends StrictRJSFSchema = RJSFSchema, + F extends FormContextType = any, +>( + props: FieldTemplateProps, +) => { + const { + id, + children, + classNames, + style, + disabled, + displayLabel, + hidden, + label, + onDropPropertyClick, + onKeyChange, + readonly, + required, + rawErrors = [], + errors, + help, + rawDescription, + schema, + uiSchema, + registry, + } = props; + + const uiOptions = getUiOptions(uiSchema); + const WrapIfAdditionalTemplate = getTemplate< + 'WrapIfAdditionalTemplate', + T, + S, + F + >('WrapIfAdditionalTemplate', registry, uiOptions); + + if (hidden) { + return
{children}
; + } + return ( + + + {children} + + + ); +}; diff --git a/plugins/scaffolder-react/src/next/components/Form/Form.tsx b/plugins/scaffolder-react/src/next/components/Form/Form.tsx index 79bcaa8475..790c9027fe 100644 --- a/plugins/scaffolder-react/src/next/components/Form/Form.tsx +++ b/plugins/scaffolder-react/src/next/components/Form/Form.tsx @@ -14,10 +14,26 @@ * limitations under the License. */ -import { withTheme } from '@rjsf/core-v5'; +import { FormProps, withTheme } from '@rjsf/core-v5'; +import React from 'react'; +import { PropsWithChildren } from 'react'; +import { FieldTemplate } from './FieldTemplate'; +import { DescriptionFieldTemplate } from './DescriptionFieldTemplate'; // 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. -/** @alpha */ -export const Form = withTheme(require('@rjsf/material-ui-v5').Theme); +const WrappedForm = withTheme(require('@rjsf/material-ui-v5').Theme); + +/** + * The Form component + * @alpha + */ +export const Form = (props: PropsWithChildren) => { + const templates = { + FieldTemplate, + DescriptionFieldTemplate, + ...props.templates, + }; + return ; +}; diff --git a/plugins/scaffolder-react/src/next/components/ScaffolderField/ScaffolderField.tsx b/plugins/scaffolder-react/src/next/components/ScaffolderField/ScaffolderField.tsx new file mode 100644 index 0000000000..da233eeba0 --- /dev/null +++ b/plugins/scaffolder-react/src/next/components/ScaffolderField/ScaffolderField.tsx @@ -0,0 +1,85 @@ +/* + * 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 React, { PropsWithChildren, ReactElement } from 'react'; + +import { MarkdownContent } from '@backstage/core-components'; +import { FormControl, makeStyles } from '@material-ui/core'; + +const useStyles = makeStyles(theme => ({ + markdownDescription: { + fontSize: theme.typography.caption.fontSize, + margin: 0, + color: theme.palette.text.secondary, + '& :first-child': { + margin: 0, + marginTop: '3px', // to keep the standard browser padding + }, + }, +})); + +/** + * Props for the {@link ScaffolderField} component + * @alpha + */ +export interface ScaffolderFieldProps { + rawDescription?: string; + errors?: ReactElement; + rawErrors?: string[]; + help?: ReactElement; + rawHelp?: string; + required?: boolean; + disabled?: boolean; + displayLabel?: boolean; +} + +/** + * A component to wrap up a input field which helps with formatting and supporting markdown + * on the field types + * @alpha + */ +export const ScaffolderField = ( + props: PropsWithChildren, +) => { + const { + children, + displayLabel = true, + rawErrors = [], + errors, + help, + rawDescription, + required, + disabled, + } = props; + const classes = useStyles(); + return ( + + {children} + {displayLabel && rawDescription ? ( + + ) : null} + {errors} + {help} + + ); +}; diff --git a/plugins/scaffolder-react/src/next/components/ScaffolderField/index.ts b/plugins/scaffolder-react/src/next/components/ScaffolderField/index.ts new file mode 100644 index 0000000000..d3c2b4a25d --- /dev/null +++ b/plugins/scaffolder-react/src/next/components/ScaffolderField/index.ts @@ -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 { ScaffolderField, type ScaffolderFieldProps } from './ScaffolderField'; diff --git a/plugins/scaffolder-react/src/next/components/index.ts b/plugins/scaffolder-react/src/next/components/index.ts index 5bfa5ccdc8..119f983a9f 100644 --- a/plugins/scaffolder-react/src/next/components/index.ts +++ b/plugins/scaffolder-react/src/next/components/index.ts @@ -25,3 +25,4 @@ export * from './TaskSteps'; export * from './TaskLogStream'; export * from './TemplateCategoryPicker'; export * from './ScaffolderPageContextMenu'; +export * from './ScaffolderField';