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(),
+);