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';