From 184161fe572544045af87fa6d3c07f7f1a4f5ff6 Mon Sep 17 00:00:00 2001 From: Tim Hansen Date: Mon, 9 Dec 2024 15:52:55 -0700 Subject: [PATCH] Register custom fields from FormFieldBlueprint Signed-off-by: Tim Hansen --- .changeset/smart-islands-judge.md | 6 + .../hooks/useCustomFieldExtensions.test.tsx | 129 ++++++++++++++++++ .../src/hooks/useCustomFieldExtensions.ts | 17 ++- .../src/next}/api/FormFieldsApi.ts | 2 +- .../scaffolder-react/src/next/api/index.ts | 19 +++ plugins/scaffolder-react/src/next/api/ref.ts | 23 ++++ .../scaffolder-react/src/next/api/types.ts | 22 +++ plugins/scaffolder-react/src/next/index.ts | 1 + plugins/scaffolder/src/alpha/api/index.ts | 7 +- plugins/scaffolder/src/alpha/api/ref.ts | 7 +- plugins/scaffolder/src/alpha/api/types.ts | 10 +- plugins/scaffolder/src/alpha/plugin.tsx | 2 +- 12 files changed, 221 insertions(+), 24 deletions(-) create mode 100644 .changeset/smart-islands-judge.md create mode 100644 plugins/scaffolder-react/src/hooks/useCustomFieldExtensions.test.tsx rename plugins/{scaffolder/src/alpha => scaffolder-react/src/next}/api/FormFieldsApi.ts (95%) create mode 100644 plugins/scaffolder-react/src/next/api/index.ts create mode 100644 plugins/scaffolder-react/src/next/api/ref.ts create mode 100644 plugins/scaffolder-react/src/next/api/types.ts diff --git a/.changeset/smart-islands-judge.md b/.changeset/smart-islands-judge.md new file mode 100644 index 0000000000..8b21666c4c --- /dev/null +++ b/.changeset/smart-islands-judge.md @@ -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. diff --git a/plugins/scaffolder-react/src/hooks/useCustomFieldExtensions.test.tsx b/plugins/scaffolder-react/src/hooks/useCustomFieldExtensions.test.tsx new file mode 100644 index 0000000000..c21d43d6ff --- /dev/null +++ b/plugins/scaffolder-react/src/hooks/useCustomFieldExtensions.test.tsx @@ -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 = { + getFormFields: jest.fn(), + }; + const wrapper = ({ children }: PropsWithChildren<{}>) => + wrapInTestApp( + + {children} + , + ); + + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('should return field extensions from the React tree', async () => { + mockFormFieldsApi.getFormFields.mockResolvedValue([]); + const CustomFieldExtension = plugin.provide( + createScaffolderFieldExtension({ + name: 'test', + component: () =>
Test
, + }), + ); + + const { result } = renderHook( + () => + useCustomFieldExtensions( + + + , + ), + { + wrapper, + }, + ); + + expect(result.current).toEqual([expect.objectContaining({ name: 'test' })]); + }); + + it('should return field extensions from formFieldsApi', async () => { + mockFormFieldsApi.getFormFields.mockResolvedValue([ + { + name: 'blueprint', + component: () =>
Test
, + }, + ]); + + const { result } = renderHook(() => useCustomFieldExtensions(
), { + 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: () =>
Test
, + }, + ]); + + const CustomFieldExtension = plugin.provide( + createScaffolderFieldExtension({ + name: 'test', + component: () =>
Test
, + }), + ); + + const { result } = renderHook( + () => + useCustomFieldExtensions( + + + , + ), + { + wrapper, + }, + ); + + await waitFor(() => { + expect(result.current).toHaveLength(2); + }); + + const fieldNames = result.current.map(field => field.name); + expect(fieldNames).toEqual(expect.arrayContaining(['test', 'blueprint'])); + }); +}); diff --git a/plugins/scaffolder-react/src/hooks/useCustomFieldExtensions.ts b/plugins/scaffolder-react/src/hooks/useCustomFieldExtensions.ts index 7e0aafd53f..e100e147ab 100644 --- a/plugins/scaffolder-react/src/hooks/useCustomFieldExtensions.ts +++ b/plugins/scaffolder-react/src/hooks/useCustomFieldExtensions.ts @@ -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]; }; diff --git a/plugins/scaffolder/src/alpha/api/FormFieldsApi.ts b/plugins/scaffolder-react/src/next/api/FormFieldsApi.ts similarity index 95% rename from plugins/scaffolder/src/alpha/api/FormFieldsApi.ts rename to plugins/scaffolder-react/src/next/api/FormFieldsApi.ts index 35141463d5..e921856369 100644 --- a/plugins/scaffolder/src/alpha/api/FormFieldsApi.ts +++ b/plugins/scaffolder-react/src/next/api/FormFieldsApi.ts @@ -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 { diff --git a/plugins/scaffolder-react/src/next/api/index.ts b/plugins/scaffolder-react/src/next/api/index.ts new file mode 100644 index 0000000000..ecc62e5acf --- /dev/null +++ b/plugins/scaffolder-react/src/next/api/index.ts @@ -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'; diff --git a/plugins/scaffolder-react/src/next/api/ref.ts b/plugins/scaffolder-react/src/next/api/ref.ts new file mode 100644 index 0000000000..eaa6f49c21 --- /dev/null +++ b/plugins/scaffolder-react/src/next/api/ref.ts @@ -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({ + id: 'plugin.scaffolder.form-fields', +}); diff --git a/plugins/scaffolder-react/src/next/api/types.ts b/plugins/scaffolder-react/src/next/api/types.ts new file mode 100644 index 0000000000..2c98111b1c --- /dev/null +++ b/plugins/scaffolder-react/src/next/api/types.ts @@ -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; +} diff --git a/plugins/scaffolder-react/src/next/index.ts b/plugins/scaffolder-react/src/next/index.ts index c4e94f36a3..9c967a5db9 100644 --- a/plugins/scaffolder-react/src/next/index.ts +++ b/plugins/scaffolder-react/src/next/index.ts @@ -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'; diff --git a/plugins/scaffolder/src/alpha/api/index.ts b/plugins/scaffolder/src/alpha/api/index.ts index 0afefe17a5..07d4b65a3b 100644 --- a/plugins/scaffolder/src/alpha/api/index.ts +++ b/plugins/scaffolder/src/alpha/api/index.ts @@ -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'; diff --git a/plugins/scaffolder/src/alpha/api/ref.ts b/plugins/scaffolder/src/alpha/api/ref.ts index 9890a9a9b2..532669d0ad 100644 --- a/plugins/scaffolder/src/alpha/api/ref.ts +++ b/plugins/scaffolder/src/alpha/api/ref.ts @@ -15,12 +15,7 @@ */ import { createApiRef } from '@backstage/frontend-plugin-api'; -import { ScaffolderFormFieldsApi, ScaffolderFormDecoratorsApi } from './types'; - -/** @alpha */ -export const formFieldsApiRef = createApiRef({ - id: 'plugin.scaffolder.form-fields', -}); +import { ScaffolderFormDecoratorsApi } from './types'; /** @alpha */ export const formDecoratorsApiRef = createApiRef({ diff --git a/plugins/scaffolder/src/alpha/api/types.ts b/plugins/scaffolder/src/alpha/api/types.ts index 10f97a6a10..1d2d331adf 100644 --- a/plugins/scaffolder/src/alpha/api/types.ts +++ b/plugins/scaffolder/src/alpha/api/types.ts @@ -14,15 +14,7 @@ * limitations under the License. */ -import { - FormFieldExtensionData, - ScaffolderFormDecorator, -} from '@backstage/plugin-scaffolder-react/alpha'; - -/** @alpha */ -export interface ScaffolderFormFieldsApi { - getFormFields(): Promise; -} +import { ScaffolderFormDecorator } from '@backstage/plugin-scaffolder-react/alpha'; /** @alpha */ export interface ScaffolderFormDecoratorsApi { diff --git a/plugins/scaffolder/src/alpha/plugin.tsx b/plugins/scaffolder/src/alpha/plugin.tsx index f3f0e9ed9e..44ec1f5d4c 100644 --- a/plugins/scaffolder/src/alpha/plugin.tsx +++ b/plugins/scaffolder/src/alpha/plugin.tsx @@ -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({