diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index 2216dc9d01..354c047f91 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -47,6 +47,7 @@ import { scaffolderPlugin, OwnerPickerFieldExtension, RepoUrlPickerFieldExtension, + ScaffolderCustomFields, } from '@backstage/plugin-scaffolder'; import { SearchPage, SearchPageNext } from '@backstage/plugin-search'; import { TechRadarPage } from '@backstage/plugin-tech-radar'; @@ -113,8 +114,10 @@ const routes = ( } /> } /> }> - - + + + + } /> { - const children = useOutlet(); - const fieldExtensions = React.Children.map(children ?? [], child => { - if (!React.isValidElement(child)) { - return null; - } + const outlet = useOutlet(); - const data = getComponentData>( - child, - 'scaffolder.extensions.field.v1', - ); - - return data; - }).filter(Boolean); + const fieldExtensions = collect( + collectChildren(outlet, FIELD_EXTENSION_WRAPPER_KEY).flat(), + FIELD_EXTENSION_KEY, + ); return ( diff --git a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx index 00d4fbae16..b946c89dfc 100644 --- a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx +++ b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx @@ -56,9 +56,9 @@ function isObject(obj: unknown): obj is JsonObject { export const createValidator = ( rootSchema: JsonObject, - validators: Map< + validators: Record< string, - (value: JsonValue, validation: FieldValidation) => void + undefined | ((value: JsonValue, validation: FieldValidation) => void) >, ) => { function validate( @@ -87,8 +87,8 @@ export const createValidator = ( const propSchema = schemaProps[key]; const fieldName = isObject(propSchema) && (propSchema['ui:field'] as string); - if (fieldName && validators.has(fieldName)) { - validators.get(fieldName)!(propData as JsonValue, propValidation); + if (fieldName && typeof validators[fieldName] === 'function') { + validators[fieldName]!(propData as JsonValue, propValidation); } } } diff --git a/plugins/scaffolder/src/extensions/helpers.ts b/plugins/scaffolder/src/extensions/helpers.ts new file mode 100644 index 0000000000..92fa06d372 --- /dev/null +++ b/plugins/scaffolder/src/extensions/helpers.ts @@ -0,0 +1,73 @@ +/* + * Copyright 2021 Spotify AB + * + * 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 { getComponentData } from '@backstage/core'; + +export const collect = ( + children: React.ReactNode, + componentDataKey: string, +) => { + const stack = [children]; + const found: T[] = []; + + while (stack.length) { + const current: React.ReactNode = stack.pop()!; + + React.Children.forEach(current, child => { + if (!React.isValidElement(child)) { + return; + } + + const data = getComponentData(child, componentDataKey); + if (data) { + found.push(data); + } + + if (child.props.children) { + stack.push(child.props.children); + } + }); + } + + return found; +}; + +export const collectChildren = ( + component: React.ReactNode, + componentDataKey: string, +) => { + const stack = [component]; + const found: React.ReactNode[] = []; + + while (stack.length) { + const current: React.ReactNode = stack.pop()!; + + React.Children.forEach(current, child => { + if (!React.isValidElement(child)) { + return; + } + + if (child.props.children) { + if (getComponentData(child, componentDataKey)) { + found.push(child.props.children); + } + stack.push(child.props.children); + } + }); + } + + return found; +}; diff --git a/plugins/scaffolder/src/extensions/index.tsx b/plugins/scaffolder/src/extensions/index.tsx index 307bdd8aeb..3a43c713d7 100644 --- a/plugins/scaffolder/src/extensions/index.tsx +++ b/plugins/scaffolder/src/extensions/index.tsx @@ -22,6 +22,9 @@ export type FieldExtensionOptions = { validation?: (data: T, field: FieldValidation) => void; }; +export const FIELD_EXTENSION_WRAPPER_KEY = 'scaffolder.extensions.wrapper.v1'; +export const FIELD_EXTENSION_KEY = 'scaffolder.extensions.field.v1'; + export function createScaffolderFieldExtension( options: FieldExtensionOptions, ): Extension<() => null> { @@ -31,7 +34,7 @@ export function createScaffolderFieldExtension( attachComponentData( FieldExtensionDataHolder, - 'scaffolder.extensions.field.v1', + FIELD_EXTENSION_KEY, options, ); @@ -39,3 +42,19 @@ export function createScaffolderFieldExtension( }, }; } + +export function createScaffolderFieldExtensionWrapper(): Extension<() => null> { + return { + expose() { + const FieldExtensionWrapperDataHolder: any = () => null; + + attachComponentData( + FieldExtensionWrapperDataHolder, + FIELD_EXTENSION_WRAPPER_KEY, + true, + ); + + return FieldExtensionWrapperDataHolder; + }, + }; +} diff --git a/plugins/scaffolder/src/index.ts b/plugins/scaffolder/src/index.ts index bda87eee5f..8e4415e3c1 100644 --- a/plugins/scaffolder/src/index.ts +++ b/plugins/scaffolder/src/index.ts @@ -20,6 +20,7 @@ export { ScaffolderPage, OwnerPickerFieldExtension, RepoUrlPickerFieldExtension, + ScaffolderCustomFields, } from './plugin'; export type { ScaffolderApi } from './api'; export { ScaffolderClient, scaffolderApiRef } from './api'; diff --git a/plugins/scaffolder/src/plugin.ts b/plugins/scaffolder/src/plugin.ts index 0177425098..796882be03 100644 --- a/plugins/scaffolder/src/plugin.ts +++ b/plugins/scaffolder/src/plugin.ts @@ -26,7 +26,10 @@ import { OwnerPicker } from './components/fields/OwnerPicker'; import { RepoUrlPicker } from './components/fields/RepoUrlPicker'; import { scmIntegrationsApiRef } from '@backstage/integration-react'; import { scaffolderApiRef, ScaffolderClient } from './api'; -import { createScaffolderFieldExtension } from './extensions'; +import { + createScaffolderFieldExtension, + createScaffolderFieldExtensionWrapper, +} from './extensions'; import { rootRouteRef, registerComponentRouteRef } from './routes'; export const scaffolderPlugin = createPlugin({ @@ -76,9 +79,14 @@ export const OwnerPickerFieldExtension = scaffolderPlugin.provide( name: 'OwnerPicker', }), ); + export const ScaffolderPage = scaffolderPlugin.provide( createRoutableExtension({ component: () => import('./components/Router').then(m => m.Router), mountPoint: rootRouteRef, }), ); + +export const ScaffolderCustomFields = scaffolderPlugin.provide( + createScaffolderFieldExtensionWrapper(), +);