Register custom fields from FormFieldBlueprint

Signed-off-by: Tim Hansen <timbonicush@spotify.com>
This commit is contained in:
Tim Hansen
2024-12-09 15:52:55 -07:00
parent ff067ad79e
commit 184161fe57
12 changed files with 221 additions and 24 deletions
+6
View File
@@ -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.
@@ -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 { useApi, useElementFilter } from '@backstage/core-plugin-api';
import { formFieldsApiRef } from '../next';
import { FieldExtensionOptions } from '../extensions';
import {
FIELD_EXTENSION_KEY,
@@ -29,7 +31,16 @@ export const useCustomFieldExtensions = <
>(
outlet: React.ReactNode,
) => {
return useElementFilter(outlet, elements =>
// Get custom fields created with FormFieldBlueprint
const formFieldsApi = useApi(formFieldsApiRef);
const [{ result: blueprintFields }, methods] = useAsync(
formFieldsApi.getFormFields,
[],
);
useMountEffect(methods.execute);
// Get custom fields created with ScaffolderFieldExtensions
const outletFields = useElementFilter(outlet, elements =>
elements
.selectByComponentData({
key: FIELD_EXTENSION_WRAPPER_KEY,
@@ -38,4 +49,6 @@ export const useCustomFieldExtensions = <
key: FIELD_EXTENSION_KEY,
}),
);
return [...blueprintFields, ...outletFields];
};
@@ -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 {
@@ -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';
+2 -5
View File
@@ -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';
+1 -6
View File
@@ -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>({
+1 -9
View File
@@ -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 {
+1 -1
View File
@@ -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({