Merge pull request #15414 from dagda1/add-scaffolder-layouts-to-next-scaffolder-page

Add scaffolder layouts to next scaffolder page
This commit is contained in:
Ben Lambert
2023-01-23 15:35:09 +01:00
committed by GitHub
31 changed files with 400 additions and 107 deletions
+12
View File
@@ -0,0 +1,12 @@
---
'@backstage/plugin-scaffolder': minor
---
- **Deprecation** - Deprecated the following exports, please import them directly from `@backstage/plugin-scaffolder-react` instead
```
createScaffolderLayout
ScaffolderLayouts
LayoutOptions
LayoutTemplate
```
+6
View File
@@ -0,0 +1,6 @@
---
'@backstage/plugin-scaffolder': patch
'@backstage/plugin-scaffolder-react': patch
---
Add `ScaffolderLayouts` to `NextScaffolderPage`
+3
View File
@@ -244,6 +244,9 @@ const routes = (
<ScaffolderFieldExtensions>
<DelayingComponentFieldExtension />
</ScaffolderFieldExtensions>
<ScaffolderLayouts>
<TwoColumnLayout />
</ScaffolderLayouts>
</Route>
<Route path="/explore" element={<ExplorePage />} />
<Route
+37 -2
View File
@@ -65,6 +65,11 @@ export function createScaffolderFieldExtension<
options: FieldExtensionOptions<TReturnValue, TInputProps>,
): Extension<FieldExtensionComponent<TReturnValue, TInputProps>>;
// @public
export function createScaffolderLayout<TInputProps = unknown>(
options: LayoutOptions,
): Extension<LayoutComponent<TInputProps>>;
// @public
export type CustomFieldExtensionSchema = {
returnValue: JSONSchema7;
@@ -116,12 +121,28 @@ export type FieldExtensionOptions<
schema?: CustomFieldExtensionSchema;
};
// @alpha
// @public
export type FormProps = Pick<
FormProps_2,
'transformErrors' | 'noHtml5Validate'
>;
// @public
export type LayoutComponent<_TInputProps> = () => null;
// @public
export interface LayoutOptions<P = any> {
// (undocumented)
component: LayoutTemplate<P>;
// (undocumented)
name: string;
}
// @public
export type LayoutTemplate<T = any> = NonNullable<
FormProps_2<T>['uiSchema']
>['ui:ObjectFieldTemplate'];
// @public
export type ListActionsResponse = Array<Action>;
@@ -276,6 +297,9 @@ export interface ScaffolderGetIntegrationsListResponse {
}[];
}
// @public
export const ScaffolderLayouts: React.ComponentType;
// @public (undocumented)
export type ScaffolderOutputLink = {
title?: string;
@@ -361,6 +385,7 @@ export type StepperProps = {
createButtonText?: ReactNode;
reviewButtonText?: ReactNode;
};
layouts?: LayoutOptions[];
};
// @alpha
@@ -422,6 +447,11 @@ export const useCustomFieldExtensions: <
outlet: React.ReactNode,
) => TComponentDataType[];
// @public
export const useCustomLayouts: <TComponentDataType = LayoutOptions<any>>(
outlet: React.ReactNode,
) => TComponentDataType[];
// @alpha
export const useFormDataFromQuery: (
initialState?: Record<string, JsonValue>,
@@ -454,7 +484,12 @@ export type WorkflowProps = {
onError(error: Error | undefined): JSX.Element | null;
} & Pick<
StepperProps,
'extensions' | 'FormProps' | 'components' | 'onCreate' | 'initialState'
| 'extensions'
| 'FormProps'
| 'components'
| 'onCreate'
| 'initialState'
| 'layouts'
>;
// (No @packageDocumentation comment for this package)
@@ -25,7 +25,8 @@ import { Extension, attachComponentData } from '@backstage/core-plugin-api';
import { FIELD_EXTENSION_KEY, FIELD_EXTENSION_WRAPPER_KEY } from './keys';
/**
* A type used to wrap up the FieldExtension to embed the ReturnValue and the InputProps
* The type used to wrap up the Layout and embed the input props
*
* @public
*/
export type FieldExtensionComponent<_TReturnValue, _TInputProps> = () => null;
@@ -15,3 +15,4 @@
*/
export { useCustomFieldExtensions } from './useCustomFieldExtensions';
export { useCustomLayouts } from './useCustomLayouts';
@@ -0,0 +1,36 @@
/*
* 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 { useElementFilter } from '@backstage/core-plugin-api';
import { LAYOUTS_KEY, LAYOUTS_WRAPPER_KEY } from '../next/layouts/keys';
import { type LayoutOptions } from '../next/types';
/**
* Hook that returns all custom field extensions from the current outlet.
* @public
*/
export const useCustomLayouts = <TComponentDataType = LayoutOptions>(
outlet: React.ReactNode,
) => {
return useElementFilter(outlet, elements =>
elements
.selectByComponentData({
key: LAYOUTS_WRAPPER_KEY,
})
.findComponentData<TComponentDataType>({
key: LAYOUTS_KEY,
}),
);
};
@@ -21,6 +21,7 @@ import { act, fireEvent } from '@testing-library/react';
import type { RJSFValidationError } from '@rjsf/utils';
import { JsonValue } from '@backstage/types';
import { NextFieldExtensionComponentProps } from '../../extensions';
import { LayoutTemplate } from '../../types';
describe('Stepper', () => {
it('should render the step titles for each step of the manifest', async () => {
@@ -392,4 +393,47 @@ describe('Stepper', () => {
await fireEvent.click(getByRole('button', { name: 'Make' }));
});
});
describe('Scaffolder Layouts', () => {
it('should render the step in the scaffolder layout', async () => {
const ScaffolderLayout: LayoutTemplate = ({ properties }) => (
<>
<h1>A Scaffolder Layout</h1>
{properties.map((prop, i) => (
<div key={i}>{prop.content}</div>
))}
</>
);
const manifest: TemplateParameterSchema = {
steps: [
{
title: 'Step 1',
schema: {
type: 'object',
'ui:ObjectFieldTemplate': 'Layout',
properties: {
field1: {
type: 'string',
},
},
},
},
],
title: 'scaffolder layouts',
};
const { getByText, getByRole } = await renderInTestApp(
<Stepper
manifest={manifest}
extensions={[]}
onCreate={jest.fn()}
layouts={[{ name: 'Layout', component: ScaffolderLayout }]}
/>,
);
expect(getByText('A Scaffolder Layout')).toBeInTheDocument();
expect(getByRole('textbox', { name: 'field1' })).toBeInTheDocument();
});
});
});
@@ -28,11 +28,12 @@ import React, { useCallback, useMemo, useState, type ReactNode } from 'react';
import { NextFieldExtensionOptions } from '../../extensions';
import { TemplateParameterSchema } from '../../../types';
import { createAsyncValidators } from './createAsyncValidators';
import type { FormProps } from '../../types';
import { ReviewState, type ReviewStateProps } from '../ReviewState';
import { useTemplateSchema } from '../../hooks/useTemplateSchema';
import { useFormDataFromQuery } from '../../hooks/useFormDataFromQuery';
import validator from '@rjsf/validator-ajv6';
import { useFormDataFromQuery } from '../../hooks';
import type { FormProps, LayoutOptions } from '../../types';
import { useTransformSchemaToProps } from '../../hooks/useTransformSchemaToProps';
const useStyles = makeStyles(theme => ({
backButton: {
@@ -65,6 +66,7 @@ export type StepperProps = {
createButtonText?: ReactNode;
reviewButtonText?: ReactNode;
};
layouts?: LayoutOptions[];
};
// TODO(blam): We require here, as the types in this package depend on @rjsf/core explicitly
@@ -76,15 +78,13 @@ const Form = withTheme(require('@rjsf/material-ui-v5').Theme);
* The `Stepper` component is the Wizard that is rendered when a user selects a template
* @alpha
*/
export const Stepper = (stepperProps: StepperProps) => {
const { components = {}, ...props } = stepperProps;
const { layouts = [], components = {}, ...props } = stepperProps;
const {
ReviewStateComponent = ReviewState,
createButtonText = 'Create',
reviewButtonText = 'Review',
} = components;
const analytics = useAnalytics();
const { steps } = useTemplateSchema(props.manifest);
const apiHolder = useApiHolder();
@@ -152,6 +152,8 @@ export const Stepper = (stepperProps: StepperProps) => {
setFormState(current => ({ ...current, ...formData }));
};
const currentStep = useTransformSchemaToProps(steps[activeStep], { layouts });
return (
<>
<MuiStepper activeStep={activeStep} alternativeLabel variant="elevation">
@@ -171,8 +173,8 @@ export const Stepper = (stepperProps: StepperProps) => {
extraErrors={errors as unknown as ErrorSchema}
formData={formState}
formContext={{ formData: formState }}
schema={steps[activeStep].schema}
uiSchema={steps[activeStep].uiSchema}
schema={currentStep.schema}
uiSchema={currentStep.uiSchema}
onSubmit={handleNext}
fields={extensions}
showErrorList={false}
@@ -51,7 +51,12 @@ export type WorkflowProps = {
onError(error: Error | undefined): JSX.Element | null;
} & Pick<
StepperProps,
'extensions' | 'FormProps' | 'components' | 'onCreate' | 'initialState'
| 'extensions'
| 'FormProps'
| 'components'
| 'onCreate'
| 'initialState'
| 'layouts'
>;
/**
@@ -0,0 +1,52 @@
/*
* 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 { renderHook } from '@testing-library/react-hooks';
import { TestApiProvider } from '@backstage/test-utils';
import { type ParsedTemplateSchema } from './useTemplateSchema';
import { useTransformSchemaToProps } from './useTransformSchemaToProps';
describe('useTransformSchemaToProps', () => {
it('should replace ui:ObjectFieldTemplate with actual component', () => {
const layouts = [{ name: 'TwoColumn', component: jest.fn() }];
const step: ParsedTemplateSchema = {
title: 'Fill in some steps',
mergedSchema: {},
schema: {},
uiSchema: {
'ui:ObjectFieldTemplate': 'TwoColumn' as any,
name: {
'ui:field': 'EntityNamePicker',
'ui:autofocus': true,
},
},
};
const { result } = renderHook(
() => useTransformSchemaToProps(step, { layouts }),
{
wrapper: ({ children }) => (
<TestApiProvider apis={[]}>{children}</TestApiProvider>
),
},
);
const { uiSchema } = result.current;
expect(uiSchema['ui:ObjectFieldTemplate']).toEqual(layouts[0].component);
});
});
@@ -0,0 +1,51 @@
/*
* 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 { type ParsedTemplateSchema } from './useTemplateSchema';
import { type LayoutOptions } from '../types';
interface Options {
layouts?: LayoutOptions[];
}
export const useTransformSchemaToProps = (
step: ParsedTemplateSchema,
options: Options = {},
): ParsedTemplateSchema => {
const { layouts = [] } = options;
const objectFieldTemplate = step?.uiSchema['ui:ObjectFieldTemplate'] as
| string
| undefined;
if (typeof objectFieldTemplate !== 'string') {
return step;
}
const Layout = layouts.find(
layout => layout.name === objectFieldTemplate,
)?.component;
if (!Layout) {
return step;
}
return {
...step,
uiSchema: {
...step.uiSchema,
['ui:ObjectFieldTemplate']: Layout,
},
};
};
+3 -1
View File
@@ -15,6 +15,8 @@
*/
export * from './components';
export * from './extensions';
export type { FormProps } from './types';
export * from './layouts';
export * from './types';
export * from './lib';
export * from './hooks';
export * from './layouts';
@@ -1,5 +1,8 @@
import { LayoutOptions } from '../types';
import { LAYOUTS_KEY, LAYOUTS_WRAPPER_KEY } from './keys';
/*
* 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,16 +16,10 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { attachComponentData, Extension } from '@backstage/core-plugin-api';
import type { LayoutOptions } from './types';
export const LAYOUTS_KEY = 'scaffolder.layout.v1';
export const LAYOUTS_WRAPPER_KEY = 'scaffolder.layouts.wrapper.v1';
/**
* The type used to wrap up the Layout and embed the input props
*
* A type used to wrap up the FieldExtension to embed the ReturnValue and the InputProps
* @public
*/
export type LayoutComponent<_TInputProps> = () => null;
@@ -55,5 +52,3 @@ export const ScaffolderLayouts: React.ComponentType = (): JSX.Element | null =>
null;
attachComponentData(ScaffolderLayouts, LAYOUTS_WRAPPER_KEY, true);
export type { LayoutOptions, LayoutTemplate } from './types';
@@ -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,21 +13,5 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { FormProps } from '@rjsf/core';
/**
* The field template from \@rjsf/core which is a react component that gets passed \@rjsf/core field related props.
*
* @public
*/
export type LayoutTemplate<T = any> = FormProps<T>['ObjectFieldTemplate'];
/**
* The type of layouts that is passed to the TemplateForms
*
* @public
*/
export interface LayoutOptions<P = any> {
name: string;
component: LayoutTemplate<P>;
}
export const LAYOUTS_KEY = 'scaffolder.layout.v1';
export const LAYOUTS_WRAPPER_KEY = 'scaffolder.layouts.wrapper.v1';
+20 -2
View File
@@ -14,13 +14,31 @@
* limitations under the License.
*/
import type { FormProps as SchemaFormProps } from '@rjsf/core-v5';
/**
* Any `@rjsf/core` form properties that are publicly exposed to the `NextScaffolderpage`
*
* @alpha
* @public
*/
export type FormProps = Pick<
SchemaFormProps,
'transformErrors' | 'noHtml5Validate'
>;
/**
* The field template from `@rjsf/core` which is a react component that gets passed `@rjsf/core` field related props.
*
* @public
*/
export type LayoutTemplate<T = any> = NonNullable<
SchemaFormProps<T>['uiSchema']
>['ui:ObjectFieldTemplate'];
/**
* The type of layouts that is passed to the TemplateForms
*
* @public
*/
export interface LayoutOptions<P = any> {
name: string;
component: LayoutTemplate<P>;
}
+13 -21
View File
@@ -10,21 +10,23 @@ import { ApiRef } from '@backstage/core-plugin-api';
import { BackstagePlugin } from '@backstage/core-plugin-api';
import { ComponentType } from 'react';
import { createScaffolderFieldExtension as createScaffolderFieldExtension_2 } from '@backstage/plugin-scaffolder-react';
import { createScaffolderLayout as createScaffolderLayout_2 } from '@backstage/plugin-scaffolder-react';
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 { Extension } from '@backstage/core-plugin-api';
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 type { FormProps as FormProps_2 } from '@rjsf/core';
import { FormProps as FormProps_2 } from '@backstage/plugin-scaffolder-react';
import type { FormProps as FormProps_3 } from '@rjsf/core-v5';
import { IdentityApi } from '@backstage/core-plugin-api';
import { JsonObject } from '@backstage/types';
import { LayoutOptions as LayoutOptions_2 } from '@backstage/plugin-scaffolder-react';
import { LayoutTemplate as LayoutTemplate_2 } from '@backstage/plugin-scaffolder-react';
import { ListActionsResponse as ListActionsResponse_2 } from '@backstage/plugin-scaffolder-react';
import { LogEvent as LogEvent_2 } from '@backstage/plugin-scaffolder-react';
import { Observable } from '@backstage/types';
@@ -56,10 +58,8 @@ import { z } from 'zod';
// @public @deprecated (undocumented)
export const createScaffolderFieldExtension: typeof createScaffolderFieldExtension_2;
// @public
export function createScaffolderLayout<TInputProps = unknown>(
options: LayoutOptions,
): Extension<LayoutComponent<TInputProps>>;
// @public @deprecated (undocumented)
export const createScaffolderLayout: typeof createScaffolderLayout_2;
// @public @deprecated (undocumented)
export type CustomFieldExtensionSchema = CustomFieldExtensionSchema_2;
@@ -161,19 +161,11 @@ export type FormProps = Pick<
'transformErrors' | 'noHtml5Validate'
>;
// @public
export type LayoutComponent<_TInputProps> = () => null;
// @public @deprecated (undocumented)
export type LayoutOptions = LayoutOptions_2;
// @public
export interface LayoutOptions<P = any> {
// (undocumented)
component: LayoutTemplate<P>;
// (undocumented)
name: string;
}
// @public
export type LayoutTemplate<T = any> = FormProps_2<T>['ObjectFieldTemplate'];
// @public @deprecated (undocumented)
export type LayoutTemplate = LayoutTemplate_2;
// @public @deprecated (undocumented)
export type ListActionsResponse = ListActionsResponse_2;
@@ -207,7 +199,7 @@ export type NextRouterProps = {
TaskPageComponent?: React_2.ComponentType<{}>;
};
groups?: TemplateGroupFilter[];
FormProps?: FormProps;
FormProps?: FormProps_2;
};
// @alpha
@@ -453,8 +445,8 @@ export type ScaffolderGetIntegrationsListOptions =
export type ScaffolderGetIntegrationsListResponse =
ScaffolderGetIntegrationsListResponse_2;
// @public
export const ScaffolderLayouts: React.ComponentType;
// @public @deprecated (undocumented)
export const ScaffolderLayouts: ComponentType<{}>;
// @public @deprecated (undocumented)
export type ScaffolderOutputlink = ScaffolderOutputLink;
@@ -35,10 +35,12 @@ import React, { ComponentType, useState } from 'react';
import { transformSchemaToProps } from './schema';
import cloneDeep from 'lodash/cloneDeep';
import * as fieldOverrides from './FieldOverrides';
import { LayoutOptions } from '../../layouts';
import { ReviewStepProps } from '../types';
import { ReviewStep } from './ReviewStep';
import { extractSchemaFromStep } from '@backstage/plugin-scaffolder-react';
import {
extractSchemaFromStep,
type LayoutOptions,
} from '@backstage/plugin-scaffolder-react';
import { selectedTemplateRouteRef } from '../../routes';
const Form = withTheme(MuiTheme);
@@ -16,7 +16,7 @@
import { JsonObject } from '@backstage/types';
import { FormProps, UiSchema } from '@rjsf/core';
import { LayoutOptions } from '../../layouts';
import type { LayoutOptions } from '@backstage/plugin-scaffolder-react';
function isObject(value: unknown): value is JsonObject {
return typeof value === 'object' && value !== null && !Array.isArray(value);
@@ -120,7 +120,8 @@ export function transformSchemaToProps(
)?.component;
if (Layout) {
uiSchema['ui:ObjectFieldTemplate'] = Layout;
uiSchema['ui:ObjectFieldTemplate'] =
Layout as unknown as FormProps<any>['ObjectFieldTemplate'];
}
}
+3 -16
View File
@@ -24,18 +24,14 @@ import { TaskPage } from './TaskPage';
import { ActionsPage } from './ActionsPage';
import { TemplateEditorPage } from './TemplateEditorPage';
import { DEFAULT_SCAFFOLDER_FIELD_EXTENSIONS } from '../extensions/default';
import {
useElementFilter,
useRouteRef,
useRouteRefParams,
} from '@backstage/core-plugin-api';
import { useRouteRef, useRouteRefParams } from '@backstage/core-plugin-api';
import {
FieldExtensionOptions,
SecretsContextProvider,
useCustomFieldExtensions,
useCustomLayouts,
} from '@backstage/plugin-scaffolder-react';
import { ListTasksPage } from './ListTasksPage';
import { LayoutOptions, LAYOUTS_KEY, LAYOUTS_WRAPPER_KEY } from '../layouts';
import { ReviewStepProps } from './types';
import {
actionsRouteRef,
@@ -104,16 +100,7 @@ export const Router = (props: RouterProps) => {
),
] as FieldExtensionOptions[];
// todo(blam): this should also be moved to a hook in -react
const customLayouts = useElementFilter(outlet, elements =>
elements
.selectByComponentData({
key: LAYOUTS_WRAPPER_KEY,
})
.findComponentData<LayoutOptions>({
key: LAYOUTS_KEY,
}),
);
const customLayouts = useCustomLayouts(outlet);
/**
* This component can be deleted once the older routes have been deprecated.
@@ -15,8 +15,10 @@
*/
import { makeStyles } from '@material-ui/core';
import React, { useState } from 'react';
import { FieldExtensionOptions } from '@backstage/plugin-scaffolder-react';
import type { LayoutOptions } from '../../layouts';
import type {
FieldExtensionOptions,
LayoutOptions,
} from '@backstage/plugin-scaffolder-react';
import { TemplateDirectoryAccess } from '../../lib/filesystem';
import { DirectoryEditorProvider } from './DirectoryEditorContext';
import { DryRunProvider } from './DryRunContext';
@@ -19,11 +19,11 @@ 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 {
import type {
FieldExtensionOptions,
LayoutOptions,
TemplateParameterSchema,
} from '@backstage/plugin-scaffolder-react';
import { LayoutOptions } from '../../layouts';
import { MultistepJsonForm } from '../MultistepJsonForm';
import { createValidator } from '../TemplatePage';
import { useDirectoryEditor } from './DirectoryEditorContext';
@@ -23,8 +23,10 @@ import { CustomFieldExplorer } from './CustomFieldExplorer';
import { TemplateEditorIntro } from './TemplateEditorIntro';
import { TemplateEditor } from './TemplateEditor';
import { TemplateFormPreviewer } from './TemplateFormPreviewer';
import { FieldExtensionOptions } from '@backstage/plugin-scaffolder-react';
import type { LayoutOptions } from '../../layouts';
import {
type FieldExtensionOptions,
type LayoutOptions,
} from '@backstage/plugin-scaffolder-react';
type Selection =
| {
@@ -32,8 +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 { FieldExtensionOptions } from '@backstage/plugin-scaffolder-react';
import { LayoutOptions } from '../../layouts';
import {
type FieldExtensionOptions,
type LayoutOptions,
} from '@backstage/plugin-scaffolder-react';
import { TemplateEditorForm } from './TemplateEditorForm';
import { TemplateEditorTextArea } from './TemplateEditorTextArea';
@@ -20,7 +20,8 @@ import React, { ComponentType, useCallback, useState } from 'react';
import { Navigate, useNavigate } from 'react-router-dom';
import useAsync from 'react-use/lib/useAsync';
import {
FieldExtensionOptions,
type FieldExtensionOptions,
type LayoutOptions,
scaffolderApiRef,
useTemplateSecrets,
} from '@backstage/plugin-scaffolder-react';
@@ -37,7 +38,6 @@ import {
useRouteRefParams,
} from '@backstage/core-plugin-api';
import { stringifyEntityRef } from '@backstage/catalog-model';
import { LayoutOptions } from '../../layouts';
import { ReviewStepProps } from '../types';
import {
rootRouteRef,
+24
View File
@@ -25,6 +25,10 @@ import {
ScaffolderFieldExtensions as ScaffolderFieldExtensionsTemp,
useTemplateSecrets as useTemplateSecretsTemp,
scaffolderApiRef as scaffolderApiRefTemp,
createScaffolderLayout as createScaffolderLayoutTemp,
ScaffolderLayouts as ScaffolderLayoutsTemp,
type LayoutOptions as LayoutOptionsTemp,
type LayoutTemplate as LayoutTemplateTemp,
type ScaffolderApi as ScaffolderApiTemp,
type ScaffolderUseTemplateSecrets as ScaffolderUseTemplateSecretsTemp,
type TemplateParameterSchema as TemplateParameterSchemaTemp,
@@ -188,3 +192,23 @@ export type ScaffolderTaskOutput = ScaffolderTaskOutputTemp;
* @deprecated use import from {@link @backstage/plugin-scaffolder-react#ScaffolderTaskStatus} instead as this has now been moved.
*/
export type ScaffolderTaskStatus = ScaffolderTaskStatusTemp;
/**
* @public
* @deprecated use import from {@link @backstage/plugin-scaffolder-react#createScaffolderLayout} instead as this has now been moved.
*/
export const createScaffolderLayout = createScaffolderLayoutTemp;
/**
* @public
* @deprecated use import from {@link @backstage/plugin-scaffolder-react#ScaffolderLayouts} instead as this has now been moved.
*/
export const ScaffolderLayouts = ScaffolderLayoutsTemp;
/**
* @public
* @deprecated use import from {@link @backstage/plugin-scaffolder-react#LayoutTemplate} instead as this has now been moved.
*/
export type LayoutTemplate = LayoutTemplateTemp;
/**
* @public
* @deprecated use import from {@link @backstage/plugin-scaffolder-react#LayoutOptions} instead as this has now been moved.
*/
export type LayoutOptions = LayoutOptionsTemp;
-8
View File
@@ -21,14 +21,6 @@
*/
export { ScaffolderClient } from './api';
export {
createScaffolderLayout,
ScaffolderLayouts,
type LayoutOptions,
type LayoutTemplate,
type LayoutComponent,
} from './layouts';
export {
EntityPickerFieldExtension,
EntityNamePickerFieldExtension,
@@ -23,6 +23,10 @@ import {
ScaffolderFieldExtensions,
} from '@backstage/plugin-scaffolder-react';
import { scaffolderPlugin } from '../../plugin';
import {
createScaffolderLayout,
ScaffolderLayouts,
} from '@backstage/plugin-scaffolder-react';
jest.mock('../TemplateListPage', () => ({
TemplateListPage: jest.fn(() => null),
@@ -106,5 +110,36 @@ describe('Router', () => {
]),
);
});
it('should extract the layouts and pass them through', async () => {
const mockLayoutComponent = () => null;
const Layout = scaffolderPlugin.provide(
createScaffolderLayout({
name: 'layout',
component: mockLayoutComponent,
}),
);
await renderInTestApp(
<Router>
<ScaffolderLayouts>
<Layout />
</ScaffolderLayouts>
</Router>,
{ routeEntries: ['/templates/default/foo'] },
);
const mock = TemplateWizardPage as jest.Mock;
const [{ layouts }] = mock.mock.calls[0];
expect(layouts).toEqual(
expect.arrayContaining([
expect.objectContaining({
name: 'layout',
component: mockLayoutComponent,
}),
]),
);
});
});
});
@@ -21,12 +21,13 @@ import {
NextFieldExtensionOptions,
SecretsContextProvider,
useCustomFieldExtensions,
useCustomLayouts,
type FormProps,
} from '@backstage/plugin-scaffolder-react';
import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common';
import { TemplateGroupFilter } from '../TemplateListPage/TemplateGroups';
import { DEFAULT_SCAFFOLDER_FIELD_EXTENSIONS } from '../../extensions/default';
import { type FormProps } from '../types';
import { nextSelectedTemplateRouteRef } from '../routes';
/**
@@ -65,6 +66,8 @@ export const Router = (props: PropsWithChildren<NextRouterProps>) => {
),
] as NextFieldExtensionOptions[];
const customLayouts = useCustomLayouts(outlet);
return (
<Routes>
<Route
@@ -82,6 +85,7 @@ export const Router = (props: PropsWithChildren<NextRouterProps>) => {
<SecretsContextProvider>
<TemplateWizardPage
customFieldExtensions={fieldExtensions}
layouts={customLayouts}
FormProps={props.FormProps}
/>
</SecretsContextProvider>
@@ -25,17 +25,19 @@ import {
import {
scaffolderApiRef,
useTemplateSecrets,
NextFieldExtensionOptions,
Workflow,
type LayoutOptions,
} from '@backstage/plugin-scaffolder-react';
import { NextFieldExtensionOptions } from '@backstage/plugin-scaffolder-react';
import { JsonValue } from '@backstage/types';
import { type FormProps } from '../types';
import { nextRouteRef } from '../routes';
import { scaffolderTaskRouteRef, selectedTemplateRouteRef } from '../../routes';
import { Header, Page } from '@backstage/core-components';
import { Workflow } from '@backstage/plugin-scaffolder-react';
type TemplateWizardPageProps = {
export type TemplateWizardPageProps = {
customFieldExtensions: NextFieldExtensionOptions<any, any>[];
layouts?: LayoutOptions[];
FormProps?: FormProps;
};
@@ -82,6 +84,7 @@ export const TemplateWizardPage = (props: TemplateWizardPageProps) => {
onError={onError}
extensions={props.customFieldExtensions}
FormProps={props.FormProps}
layouts={props.layouts}
/>
</Page>
</AnalyticsContext>
+1 -1
View File
@@ -23,7 +23,7 @@
import type { FormProps as SchemaFormProps } from '@rjsf/core-v5';
/**
* Any `@rjsf/core` form properties that are publicly exposed to the `NextScaffolderpage`
* Any `@rjsf/core` form properties that are publicly exposed to the `NextScaffolderPage`
*
* @alpha
* @deprecated use the import from {@link @backstage/plugin-scaffolder-react/alpha#FormProps} instead