Merge pull request #28059 from backstage/timbonicush/scaffolder-custom-field-blueprint
Register custom fields from FormFieldBlueprint
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder-react': patch
|
||||
'@backstage/plugin-scaffolder': patch
|
||||
---
|
||||
|
||||
Scaffolder field extensions registered with `FormFieldBlueprint` are now collected in the `useCustomFieldExtensions` hook, enabling them for use in the scaffolder.
|
||||
@@ -5,13 +5,17 @@
|
||||
```ts
|
||||
/// <reference types="react" />
|
||||
|
||||
import { AnyApiFactory } from '@backstage/frontend-plugin-api';
|
||||
import { AnyApiRef } from '@backstage/core-plugin-api';
|
||||
import { ApiHolder } from '@backstage/core-plugin-api';
|
||||
import { ApiRef } from '@backstage/frontend-plugin-api';
|
||||
import { ComponentType } from 'react';
|
||||
import { ConfigurableExtensionDataRef } from '@backstage/frontend-plugin-api';
|
||||
import { CustomFieldValidator } from '@backstage/plugin-scaffolder-react';
|
||||
import { Dispatch } from 'react';
|
||||
import { ExtensionBlueprint } from '@backstage/frontend-plugin-api';
|
||||
import { ExtensionDefinition } from '@backstage/frontend-plugin-api';
|
||||
import { ExtensionInput } from '@backstage/frontend-plugin-api';
|
||||
import { FieldExtensionComponentProps } from '@backstage/plugin-scaffolder-react';
|
||||
import { FieldExtensionOptions } from '@backstage/plugin-scaffolder-react';
|
||||
import { FieldSchema } from '@backstage/plugin-scaffolder-react';
|
||||
@@ -171,6 +175,34 @@ export type FormFieldExtensionData<
|
||||
schema?: FieldSchema<z.output<TReturnValue>, z.output<TUiOptions>>;
|
||||
};
|
||||
|
||||
// @alpha (undocumented)
|
||||
export const formFieldsApi: ExtensionDefinition<{
|
||||
config: {};
|
||||
configInput: {};
|
||||
output: ConfigurableExtensionDataRef<AnyApiFactory, 'core.api.factory', {}>;
|
||||
inputs: {
|
||||
formFields: ExtensionInput<
|
||||
ConfigurableExtensionDataRef<
|
||||
() => Promise<FormField>,
|
||||
'scaffolder.form-field-loader',
|
||||
{}
|
||||
>,
|
||||
{
|
||||
singleton: false;
|
||||
optional: false;
|
||||
}
|
||||
>;
|
||||
};
|
||||
kind: 'api';
|
||||
name: 'form-fields';
|
||||
params: {
|
||||
factory: AnyApiFactory;
|
||||
};
|
||||
}>;
|
||||
|
||||
// @alpha (undocumented)
|
||||
export const formFieldsApiRef: ApiRef<ScaffolderFormFieldsApi>;
|
||||
|
||||
// @alpha (undocumented)
|
||||
export type FormValidation = {
|
||||
[name: string]: FieldValidation | FormValidation;
|
||||
@@ -245,6 +277,12 @@ export type ScaffolderFormDecoratorContext<
|
||||
) => void;
|
||||
};
|
||||
|
||||
// @alpha (undocumented)
|
||||
export interface ScaffolderFormFieldsApi {
|
||||
// (undocumented)
|
||||
getFormFields(): Promise<FormFieldExtensionData[]>;
|
||||
}
|
||||
|
||||
// @alpha (undocumented)
|
||||
export function ScaffolderPageContextMenu(
|
||||
props: ScaffolderPageContextMenuProps,
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
/*
|
||||
* Copyright 2024 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 } from 'react';
|
||||
import { createPlugin } from '@backstage/core-plugin-api';
|
||||
import { TestApiProvider, wrapInTestApp } from '@backstage/test-utils';
|
||||
import { renderHook, waitFor } from '@testing-library/react';
|
||||
import { ScaffolderFormFieldsApi, formFieldsApiRef } from '../alpha';
|
||||
import { useCustomFieldExtensions } from './useCustomFieldExtensions';
|
||||
import {
|
||||
ScaffolderFieldExtensions,
|
||||
createScaffolderFieldExtension,
|
||||
} from '../extensions';
|
||||
|
||||
const plugin = createPlugin({
|
||||
id: 'scaffolder',
|
||||
apis: [],
|
||||
routes: {},
|
||||
externalRoutes: {},
|
||||
});
|
||||
|
||||
describe('useCustomFieldExtensions', () => {
|
||||
const mockFormFieldsApi: jest.Mocked<ScaffolderFormFieldsApi> = {
|
||||
getFormFields: jest.fn(),
|
||||
};
|
||||
const wrapper = ({ children }: PropsWithChildren<{}>) =>
|
||||
wrapInTestApp(
|
||||
<TestApiProvider apis={[[formFieldsApiRef, mockFormFieldsApi]]}>
|
||||
{children}
|
||||
</TestApiProvider>,
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it('should return field extensions from the React tree', async () => {
|
||||
mockFormFieldsApi.getFormFields.mockResolvedValue([]);
|
||||
const CustomFieldExtension = plugin.provide(
|
||||
createScaffolderFieldExtension({
|
||||
name: 'test',
|
||||
component: () => <div>Test</div>,
|
||||
}),
|
||||
);
|
||||
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useCustomFieldExtensions(
|
||||
<ScaffolderFieldExtensions>
|
||||
<CustomFieldExtension />
|
||||
</ScaffolderFieldExtensions>,
|
||||
),
|
||||
{
|
||||
wrapper,
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.current).toEqual([expect.objectContaining({ name: 'test' })]);
|
||||
});
|
||||
|
||||
it('should return field extensions from formFieldsApi', async () => {
|
||||
mockFormFieldsApi.getFormFields.mockResolvedValue([
|
||||
{
|
||||
name: 'blueprint',
|
||||
component: () => <div>Test</div>,
|
||||
},
|
||||
]);
|
||||
|
||||
const { result } = renderHook(() => useCustomFieldExtensions(<div />), {
|
||||
wrapper,
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
expect(result.current).toEqual([
|
||||
expect.objectContaining({ name: 'blueprint' }),
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return field extensions from both sources', async () => {
|
||||
mockFormFieldsApi.getFormFields.mockResolvedValue([
|
||||
{
|
||||
name: 'blueprint',
|
||||
component: () => <div>Test</div>,
|
||||
},
|
||||
]);
|
||||
|
||||
const CustomFieldExtension = plugin.provide(
|
||||
createScaffolderFieldExtension({
|
||||
name: 'test',
|
||||
component: () => <div>Test</div>,
|
||||
}),
|
||||
);
|
||||
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useCustomFieldExtensions(
|
||||
<ScaffolderFieldExtensions>
|
||||
<CustomFieldExtension />
|
||||
</ScaffolderFieldExtensions>,
|
||||
),
|
||||
{
|
||||
wrapper,
|
||||
},
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current).toHaveLength(2);
|
||||
});
|
||||
|
||||
const fieldNames = result.current.map(field => field.name);
|
||||
expect(fieldNames).toEqual(expect.arrayContaining(['test', 'blueprint']));
|
||||
});
|
||||
});
|
||||
@@ -13,7 +13,9 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { useElementFilter } from '@backstage/core-plugin-api';
|
||||
import { useAsync, useMountEffect } from '@react-hookz/web';
|
||||
import { useApiHolder, useElementFilter } from '@backstage/core-plugin-api';
|
||||
import { formFieldsApiRef } from '../next';
|
||||
import { FieldExtensionOptions } from '../extensions';
|
||||
import {
|
||||
FIELD_EXTENSION_KEY,
|
||||
@@ -25,11 +27,22 @@ import {
|
||||
* @public
|
||||
*/
|
||||
export const useCustomFieldExtensions = <
|
||||
// todo(blam): this shouldn't be here, should remove this, but this is a breaking change to remove the generic.
|
||||
TComponentDataType = FieldExtensionOptions,
|
||||
>(
|
||||
outlet: React.ReactNode,
|
||||
) => {
|
||||
return useElementFilter(outlet, elements =>
|
||||
// Get custom fields created with FormFieldBlueprint
|
||||
const apiHolder = useApiHolder();
|
||||
const formFieldsApi = apiHolder.get(formFieldsApiRef);
|
||||
const [{ result: blueprintFields }, methods] = useAsync(
|
||||
formFieldsApi?.getFormFields ?? (async () => []),
|
||||
[],
|
||||
);
|
||||
useMountEffect(methods.execute);
|
||||
|
||||
// Get custom fields created with ScaffolderFieldExtensions
|
||||
const outletFields = useElementFilter(outlet, elements =>
|
||||
elements
|
||||
.selectByComponentData({
|
||||
key: FIELD_EXTENSION_WRAPPER_KEY,
|
||||
@@ -38,4 +51,18 @@ export const useCustomFieldExtensions = <
|
||||
key: FIELD_EXTENSION_KEY,
|
||||
}),
|
||||
);
|
||||
|
||||
// This should really be a different type moving foward, but we do this to keep type compatibility.
|
||||
// should probably also move the defaults into the API eventually too, but that will come with the move
|
||||
// to the new frontend system.
|
||||
const blueprintsToLegacy: FieldExtensionOptions[] = blueprintFields?.map(
|
||||
field => ({
|
||||
component: field.component,
|
||||
name: field.name,
|
||||
validation: field.validation,
|
||||
schema: field.schema?.schema,
|
||||
}),
|
||||
);
|
||||
|
||||
return [...blueprintsToLegacy, ...outletFields] as TComponentDataType[];
|
||||
};
|
||||
|
||||
+2
-1
@@ -21,7 +21,7 @@ import {
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
import { formFieldsApiRef } from './ref';
|
||||
import { ScaffolderFormFieldsApi } from './types';
|
||||
import { FormFieldBlueprint } from '@backstage/plugin-scaffolder-react/alpha';
|
||||
import { FormFieldBlueprint } from '../blueprints';
|
||||
import { FormField, OpaqueFormField } from '@internal/scaffolder';
|
||||
|
||||
class DefaultScaffolderFormFieldsApi implements ScaffolderFormFieldsApi {
|
||||
@@ -40,6 +40,7 @@ class DefaultScaffolderFormFieldsApi implements ScaffolderFormFieldsApi {
|
||||
}
|
||||
}
|
||||
|
||||
/** @alpha */
|
||||
export const formFieldsApi = ApiBlueprint.makeWithOverrides({
|
||||
name: 'form-fields',
|
||||
inputs: {
|
||||
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* Copyright 2024 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 { formFieldsApi } from './FormFieldsApi';
|
||||
export { formFieldsApiRef } from './ref';
|
||||
export type { ScaffolderFormFieldsApi } from './types';
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright 2024 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 { createApiRef } from '@backstage/frontend-plugin-api';
|
||||
import { ScaffolderFormFieldsApi } from './types';
|
||||
|
||||
/** @alpha */
|
||||
export const formFieldsApiRef = createApiRef<ScaffolderFormFieldsApi>({
|
||||
id: 'plugin.scaffolder.form-fields',
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* Copyright 2024 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 { FormFieldExtensionData } from '../blueprints';
|
||||
|
||||
/** @alpha */
|
||||
export interface ScaffolderFormFieldsApi {
|
||||
getFormFields(): Promise<FormFieldExtensionData[]>;
|
||||
}
|
||||
@@ -13,6 +13,7 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
export * from './api';
|
||||
export * from './components';
|
||||
export * from './lib';
|
||||
export * from './hooks';
|
||||
|
||||
@@ -14,7 +14,7 @@ import { ExtensionInput } from '@backstage/frontend-plugin-api';
|
||||
import { ExternalRouteRef } from '@backstage/frontend-plugin-api';
|
||||
import { FieldExtensionOptions } from '@backstage/plugin-scaffolder-react';
|
||||
import { FormField } from '@internal/scaffolder';
|
||||
import { FormFieldExtensionData } from '@backstage/plugin-scaffolder-react/alpha';
|
||||
import { formFieldsApiRef } from '@backstage/plugin-scaffolder-react/alpha';
|
||||
import type { FormProps as FormProps_2 } from '@rjsf/core';
|
||||
import { FormProps as FormProps_3 } from '@backstage/plugin-scaffolder-react';
|
||||
import { FrontendPlugin } from '@backstage/frontend-plugin-api';
|
||||
@@ -26,6 +26,7 @@ import { default as React_2 } from 'react';
|
||||
import { ReviewStepProps } from '@backstage/plugin-scaffolder-react';
|
||||
import { RouteRef } from '@backstage/frontend-plugin-api';
|
||||
import { ScaffolderFormDecorator } from '@backstage/plugin-scaffolder-react/alpha';
|
||||
import { ScaffolderFormFieldsApi } from '@backstage/plugin-scaffolder-react/alpha';
|
||||
import { SubRouteRef } from '@backstage/frontend-plugin-api';
|
||||
import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common';
|
||||
import { TemplateGroupFilter } from '@backstage/plugin-scaffolder-react';
|
||||
@@ -52,6 +53,33 @@ const _default: FrontendPlugin<
|
||||
}>;
|
||||
},
|
||||
{
|
||||
'api:scaffolder/form-fields': ExtensionDefinition<{
|
||||
config: {};
|
||||
configInput: {};
|
||||
output: ConfigurableExtensionDataRef<
|
||||
AnyApiFactory,
|
||||
'core.api.factory',
|
||||
{}
|
||||
>;
|
||||
inputs: {
|
||||
formFields: ExtensionInput<
|
||||
ConfigurableExtensionDataRef<
|
||||
() => Promise<FormField>,
|
||||
'scaffolder.form-field-loader',
|
||||
{}
|
||||
>,
|
||||
{
|
||||
singleton: false;
|
||||
optional: false;
|
||||
}
|
||||
>;
|
||||
};
|
||||
kind: 'api';
|
||||
name: 'form-fields';
|
||||
params: {
|
||||
factory: AnyApiFactory;
|
||||
};
|
||||
}>;
|
||||
'page:scaffolder': ExtensionDefinition<{
|
||||
kind: 'page';
|
||||
name: undefined;
|
||||
@@ -129,33 +157,6 @@ const _default: FrontendPlugin<
|
||||
factory: AnyApiFactory;
|
||||
};
|
||||
}>;
|
||||
'api:scaffolder/form-fields': ExtensionDefinition<{
|
||||
config: {};
|
||||
configInput: {};
|
||||
output: ConfigurableExtensionDataRef<
|
||||
AnyApiFactory,
|
||||
'core.api.factory',
|
||||
{}
|
||||
>;
|
||||
inputs: {
|
||||
formFields: ExtensionInput<
|
||||
ConfigurableExtensionDataRef<
|
||||
() => Promise<FormField>,
|
||||
'scaffolder.form-field-loader',
|
||||
{}
|
||||
>,
|
||||
{
|
||||
singleton: false;
|
||||
optional: false;
|
||||
}
|
||||
>;
|
||||
};
|
||||
kind: 'api';
|
||||
name: 'form-fields';
|
||||
params: {
|
||||
factory: AnyApiFactory;
|
||||
};
|
||||
}>;
|
||||
}
|
||||
>;
|
||||
export default _default;
|
||||
@@ -175,8 +176,7 @@ export class DefaultScaffolderFormDecoratorsApi
|
||||
// @alpha (undocumented)
|
||||
export const formDecoratorsApiRef: ApiRef<ScaffolderFormDecoratorsApi>;
|
||||
|
||||
// @alpha (undocumented)
|
||||
export const formFieldsApiRef: ApiRef<ScaffolderFormFieldsApi>;
|
||||
export { formFieldsApiRef };
|
||||
|
||||
// @alpha @deprecated
|
||||
export type FormProps = Pick<
|
||||
@@ -197,11 +197,7 @@ export interface ScaffolderFormDecoratorsApi {
|
||||
getFormDecorators(): Promise<ScaffolderFormDecorator[]>;
|
||||
}
|
||||
|
||||
// @alpha (undocumented)
|
||||
export interface ScaffolderFormFieldsApi {
|
||||
// (undocumented)
|
||||
getFormFields(): Promise<FormFieldExtensionData[]>;
|
||||
}
|
||||
export { ScaffolderFormFieldsApi };
|
||||
|
||||
// @public (undocumented)
|
||||
export type ScaffolderTemplateEditorClassKey =
|
||||
|
||||
@@ -14,9 +14,6 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { formFieldsApiRef, formDecoratorsApiRef } from './ref';
|
||||
export type {
|
||||
ScaffolderFormFieldsApi,
|
||||
ScaffolderFormDecoratorsApi,
|
||||
} from './types';
|
||||
export { formDecoratorsApiRef } from './ref';
|
||||
export type { ScaffolderFormDecoratorsApi } from './types';
|
||||
export { DefaultScaffolderFormDecoratorsApi } from './FormDecoratorsApi';
|
||||
|
||||
@@ -15,12 +15,7 @@
|
||||
*/
|
||||
|
||||
import { createApiRef } from '@backstage/frontend-plugin-api';
|
||||
import { ScaffolderFormFieldsApi, ScaffolderFormDecoratorsApi } from './types';
|
||||
|
||||
/** @alpha */
|
||||
export const formFieldsApiRef = createApiRef<ScaffolderFormFieldsApi>({
|
||||
id: 'plugin.scaffolder.form-fields',
|
||||
});
|
||||
import { ScaffolderFormDecoratorsApi } from './types';
|
||||
|
||||
/** @alpha */
|
||||
export const formDecoratorsApiRef = createApiRef<ScaffolderFormDecoratorsApi>({
|
||||
|
||||
@@ -14,15 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
FormFieldExtensionData,
|
||||
ScaffolderFormDecorator,
|
||||
} from '@backstage/plugin-scaffolder-react/alpha';
|
||||
|
||||
/** @alpha */
|
||||
export interface ScaffolderFormFieldsApi {
|
||||
getFormFields(): Promise<FormFieldExtensionData[]>;
|
||||
}
|
||||
import { ScaffolderFormDecorator } from '@backstage/plugin-scaffolder-react/alpha';
|
||||
|
||||
/** @alpha */
|
||||
export interface ScaffolderFormDecoratorsApi {
|
||||
|
||||
@@ -25,4 +25,9 @@ export {
|
||||
export { scaffolderTranslationRef } from '../translation';
|
||||
export * from './api';
|
||||
|
||||
export {
|
||||
formFieldsApiRef,
|
||||
type ScaffolderFormFieldsApi,
|
||||
} from '@backstage/plugin-scaffolder-react/alpha';
|
||||
|
||||
export { default } from './plugin';
|
||||
|
||||
@@ -32,7 +32,7 @@ import {
|
||||
scaffolderPage,
|
||||
scaffolderApi,
|
||||
} from './extensions';
|
||||
import { formFieldsApi } from './api/FormFieldsApi';
|
||||
import { formFieldsApi } from '@backstage/plugin-scaffolder-react/alpha';
|
||||
|
||||
/** @alpha */
|
||||
export default createFrontendPlugin({
|
||||
|
||||
@@ -80,6 +80,7 @@ import { RepoBranchPicker } from './components/fields/RepoBranchPicker/RepoBranc
|
||||
import { RepoBranchPickerSchema } from './components/fields/RepoBranchPicker/schema';
|
||||
import { formDecoratorsApiRef } from './alpha/api/ref';
|
||||
import { DefaultScaffolderFormDecoratorsApi } from './alpha/api/FormDecoratorsApi';
|
||||
import { formFieldsApiRef } from '@backstage/plugin-scaffolder-react/alpha';
|
||||
|
||||
/**
|
||||
* The main plugin export for the scaffolder.
|
||||
@@ -109,6 +110,11 @@ export const scaffolderPlugin = createPlugin({
|
||||
deps: {},
|
||||
factory: () => DefaultScaffolderFormDecoratorsApi.create(),
|
||||
}),
|
||||
createApiFactory({
|
||||
api: formFieldsApiRef,
|
||||
deps: {},
|
||||
factory: () => ({ getFormFields: async () => [] }),
|
||||
}),
|
||||
],
|
||||
routes: {
|
||||
root: rootRouteRef,
|
||||
|
||||
Reference in New Issue
Block a user