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:
@@ -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
|
||||
```
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder': patch
|
||||
'@backstage/plugin-scaffolder-react': patch
|
||||
---
|
||||
|
||||
Add `ScaffolderLayouts` to `NextScaffolderPage`
|
||||
@@ -244,6 +244,9 @@ const routes = (
|
||||
<ScaffolderFieldExtensions>
|
||||
<DelayingComponentFieldExtension />
|
||||
</ScaffolderFieldExtensions>
|
||||
<ScaffolderLayouts>
|
||||
<TwoColumnLayout />
|
||||
</ScaffolderLayouts>
|
||||
</Route>
|
||||
<Route path="/explore" element={<ExplorePage />} />
|
||||
<Route
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -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';
|
||||
|
||||
+5
-10
@@ -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';
|
||||
+3
-19
@@ -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';
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
@@ -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'];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user