+ >
+ );
+};
diff --git a/plugins/scaffolder/src/next/TemplateWizardPage/SecretsContext/index.ts b/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/index.ts
similarity index 85%
rename from plugins/scaffolder/src/next/TemplateWizardPage/SecretsContext/index.ts
rename to plugins/scaffolder/src/next/TemplateWizardPage/Stepper/index.ts
index 65b530dea1..5532563e28 100644
--- a/plugins/scaffolder/src/next/TemplateWizardPage/SecretsContext/index.ts
+++ b/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/index.ts
@@ -13,8 +13,4 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-export {
- useTemplateSecrets,
- SecretsContext,
- SecretsContextProvider,
-} from './SecretsContext';
+export { Stepper } from './Stepper';
diff --git a/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/schema.test.ts b/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/schema.test.ts
new file mode 100644
index 0000000000..01e6cea3cc
--- /dev/null
+++ b/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/schema.test.ts
@@ -0,0 +1,414 @@
+/*
+ * Copyright 2020 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 { JsonObject } from '@backstage/types';
+import { extractSchemaFromStep } from './schema';
+
+describe('extractSchemaFromStep', () => {
+ it('transforms deep schema', () => {
+ const inputSchema: JsonObject = {
+ type: 'object',
+ 'ui:welp': 'warp',
+ properties: {
+ field1: {
+ type: 'string',
+ 'ui:derp': 'herp',
+ },
+ field2: {
+ type: 'object',
+ properties: {
+ fieldX: {
+ type: 'string',
+ 'ui:derp': 'xerp',
+ },
+ },
+ },
+ },
+ };
+ const expectedSchema = {
+ type: 'object',
+ properties: {
+ field1: {
+ type: 'string',
+ },
+ field2: {
+ type: 'object',
+ properties: {
+ fieldX: {
+ type: 'string',
+ },
+ },
+ },
+ },
+ };
+ const expectedUiSchema = {
+ 'ui:welp': 'warp',
+ field1: {
+ 'ui:derp': 'herp',
+ },
+ field2: {
+ fieldX: {
+ 'ui:derp': 'xerp',
+ },
+ },
+ };
+
+ expect(extractSchemaFromStep(inputSchema)).toEqual({
+ schema: expectedSchema,
+ uiSchema: expectedUiSchema,
+ });
+ });
+
+ it('transforms schema with anyOf fields', () => {
+ const inputSchema: JsonObject = {
+ type: 'object',
+ anyOf: [
+ {
+ properties: {
+ field3: {
+ type: 'string',
+ default: 'Value 1',
+ 'ui:readonly': true,
+ },
+ },
+ },
+ {
+ properties: {
+ field3: {
+ type: 'string',
+ default: 'Value 2',
+ 'ui:readonly': true,
+ },
+ },
+ },
+ ],
+ oneOf: [
+ {
+ properties: {
+ field4: {
+ type: 'string',
+ default: 'Value 1',
+ 'ui:readonly': true,
+ },
+ },
+ },
+ ],
+ allOf: [
+ {
+ properties: {
+ field5: {
+ type: 'string',
+ default: 'Value 1',
+ 'ui:readonly': true,
+ },
+ },
+ },
+ ],
+ properties: {
+ field1: {
+ type: 'object',
+ anyOf: [
+ {
+ properties: {
+ field3: {
+ type: 'string',
+ default: 'Value 1',
+ 'ui:readonly': true,
+ },
+ },
+ },
+ {
+ properties: {
+ field3: {
+ type: 'string',
+ default: 'Value 2',
+ 'ui:readonly': true,
+ },
+ },
+ },
+ ],
+ oneOf: [
+ {
+ properties: {
+ field4: {
+ type: 'string',
+ default: 'Value 1',
+ 'ui:readonly': true,
+ },
+ },
+ },
+ ],
+ allOf: [
+ {
+ properties: {
+ field5: {
+ type: 'string',
+ default: 'Value 1',
+ 'ui:readonly': true,
+ },
+ },
+ },
+ ],
+ },
+ field2: {
+ type: 'string',
+ 'ui:derp': 'xerp',
+ },
+ },
+ };
+ const expectedSchema = {
+ type: 'object',
+ anyOf: [
+ {
+ properties: {
+ field3: {
+ type: 'string',
+ default: 'Value 1',
+ },
+ },
+ },
+ {
+ properties: {
+ field3: {
+ type: 'string',
+ default: 'Value 2',
+ },
+ },
+ },
+ ],
+ oneOf: [
+ {
+ properties: {
+ field4: {
+ type: 'string',
+ default: 'Value 1',
+ },
+ },
+ },
+ ],
+ allOf: [
+ {
+ properties: {
+ field5: {
+ type: 'string',
+ default: 'Value 1',
+ },
+ },
+ },
+ ],
+ properties: {
+ field1: {
+ type: 'object',
+ anyOf: [
+ {
+ properties: {
+ field3: {
+ type: 'string',
+ default: 'Value 1',
+ },
+ },
+ },
+ {
+ properties: {
+ field3: {
+ type: 'string',
+ default: 'Value 2',
+ },
+ },
+ },
+ ],
+ oneOf: [
+ {
+ properties: {
+ field4: {
+ type: 'string',
+ default: 'Value 1',
+ },
+ },
+ },
+ ],
+ allOf: [
+ {
+ properties: {
+ field5: {
+ type: 'string',
+ default: 'Value 1',
+ },
+ },
+ },
+ ],
+ },
+ field2: {
+ type: 'string',
+ },
+ },
+ };
+ const expectedUiSchema = {
+ field3: {
+ 'ui:readonly': true,
+ },
+ field4: {
+ 'ui:readonly': true,
+ },
+ field5: {
+ 'ui:readonly': true,
+ },
+ field1: {
+ field3: {
+ 'ui:readonly': true,
+ },
+ field4: {
+ 'ui:readonly': true,
+ },
+ field5: {
+ 'ui:readonly': true,
+ },
+ },
+ field2: {
+ 'ui:derp': 'xerp',
+ },
+ };
+
+ expect(extractSchemaFromStep(inputSchema)).toEqual({
+ schema: expectedSchema,
+ uiSchema: expectedUiSchema,
+ });
+ });
+
+ it('transforms schema with dependencies', () => {
+ const inputSchema: JsonObject = {
+ type: 'object',
+ properties: {
+ name: {
+ type: 'string',
+ },
+ credit_card: {
+ type: 'number',
+ },
+ },
+ required: ['name'],
+ dependencies: {
+ credit_card: {
+ properties: {
+ billing_address: {
+ type: 'string',
+ 'ui:widget': 'textarea',
+ },
+ },
+ required: ['billing_address'],
+ },
+ },
+ };
+ const expectedSchema = {
+ type: 'object',
+ properties: {
+ name: {
+ type: 'string',
+ },
+ credit_card: {
+ type: 'number',
+ },
+ },
+ required: ['name'],
+ dependencies: {
+ credit_card: {
+ properties: {
+ billing_address: {
+ type: 'string',
+ },
+ },
+ required: ['billing_address'],
+ },
+ },
+ };
+ const expectedUiSchema = {
+ billing_address: {
+ 'ui:widget': 'textarea',
+ },
+ credit_card: {},
+ name: {},
+ };
+
+ expect(extractSchemaFromStep(inputSchema)).toEqual({
+ schema: expectedSchema,
+ uiSchema: expectedUiSchema,
+ });
+ });
+
+ it('transforms schema with array items', () => {
+ const inputSchema: JsonObject = {
+ type: 'object',
+ properties: {
+ person: {
+ type: 'array',
+ items: {
+ type: 'object',
+ properties: {
+ name: {
+ type: 'string',
+ },
+ address: {
+ type: 'string',
+ 'ui:widget': 'textarea',
+ },
+ },
+ },
+ },
+ accountNumber: {
+ type: 'number',
+ },
+ },
+ };
+ const expectedSchema = {
+ type: 'object',
+ properties: {
+ person: {
+ type: 'array',
+ items: {
+ type: 'object',
+ properties: {
+ name: {
+ type: 'string',
+ },
+ address: {
+ type: 'string',
+ },
+ },
+ },
+ },
+ accountNumber: {
+ type: 'number',
+ },
+ },
+ };
+ const expectedUiSchema = {
+ accountNumber: {},
+ person: {
+ items: {
+ name: {},
+ address: {
+ 'ui:widget': 'textarea',
+ },
+ },
+ },
+ };
+
+ expect(extractSchemaFromStep(inputSchema)).toEqual({
+ schema: expectedSchema,
+ uiSchema: expectedUiSchema,
+ });
+ });
+});
diff --git a/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/schema.ts b/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/schema.ts
new file mode 100644
index 0000000000..5a33a622bb
--- /dev/null
+++ b/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/schema.ts
@@ -0,0 +1,112 @@
+/*
+ * Copyright 2022 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 { JsonObject } from '@backstage/types';
+import { UiSchema } from '@rjsf/core';
+
+function isObject(value: unknown): value is JsonObject {
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
+}
+
+function extractUiSchema(schema: JsonObject, uiSchema: JsonObject) {
+ if (!isObject(schema)) {
+ return;
+ }
+
+ const { properties, items, anyOf, oneOf, allOf, dependencies } = schema;
+
+ for (const propName in schema) {
+ if (!schema.hasOwnProperty(propName)) {
+ continue;
+ }
+
+ if (propName.startsWith('ui:')) {
+ uiSchema[propName] = schema[propName];
+ delete schema[propName];
+ }
+ }
+
+ if (isObject(properties)) {
+ for (const propName in properties) {
+ if (!properties.hasOwnProperty(propName)) {
+ continue;
+ }
+
+ const schemaNode = properties[propName];
+ if (!isObject(schemaNode)) {
+ continue;
+ }
+ const innerUiSchema = {};
+ uiSchema[propName] = innerUiSchema;
+ extractUiSchema(schemaNode, innerUiSchema);
+ }
+ }
+
+ if (isObject(items)) {
+ const innerUiSchema = {};
+ uiSchema.items = innerUiSchema;
+ extractUiSchema(items, innerUiSchema);
+ }
+
+ if (Array.isArray(anyOf)) {
+ for (const schemaNode of anyOf) {
+ if (!isObject(schemaNode)) {
+ continue;
+ }
+ extractUiSchema(schemaNode, uiSchema);
+ }
+ }
+
+ if (Array.isArray(oneOf)) {
+ for (const schemaNode of oneOf) {
+ if (!isObject(schemaNode)) {
+ continue;
+ }
+ extractUiSchema(schemaNode, uiSchema);
+ }
+ }
+
+ if (Array.isArray(allOf)) {
+ for (const schemaNode of allOf) {
+ if (!isObject(schemaNode)) {
+ continue;
+ }
+ extractUiSchema(schemaNode, uiSchema);
+ }
+ }
+
+ if (isObject(dependencies)) {
+ for (const depName of Object.keys(dependencies)) {
+ const schemaNode = dependencies[depName];
+ if (!isObject(schemaNode)) {
+ continue;
+ }
+ extractUiSchema(schemaNode, uiSchema);
+ }
+ }
+}
+
+/**
+ * @alpha
+ * Takes a step from a Backstage Template Manifest and converts it to a JSON Schema and UI Schema for rjsf
+ */
+export const extractSchemaFromStep = (
+ inputStep: JsonObject,
+): { uiSchema: UiSchema; schema: JsonObject } => {
+ const uiSchema: UiSchema = {};
+ const returnSchema: JsonObject = JSON.parse(JSON.stringify(inputStep));
+ extractUiSchema(returnSchema, uiSchema);
+ return { uiSchema, schema: returnSchema };
+};
diff --git a/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/useTemplateSchema.test.tsx b/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/useTemplateSchema.test.tsx
new file mode 100644
index 0000000000..9be6faa0fa
--- /dev/null
+++ b/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/useTemplateSchema.test.tsx
@@ -0,0 +1,236 @@
+/*
+ * Copyright 2022 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 { TemplateParameterSchema } from '../../../types';
+import { useTemplateSchema } from './useTemplateSchema';
+import { renderHook } from '@testing-library/react-hooks';
+import { TestApiProvider } from '@backstage/test-utils';
+import React from 'react';
+import { featureFlagsApiRef } from '@backstage/core-plugin-api';
+
+describe('useTemplateSchema', () => {
+ it('should generate the correct schema', () => {
+ const manifest: TemplateParameterSchema = {
+ title: 'Test Template',
+ description: 'Test Template Description',
+ steps: [
+ {
+ title: 'Step 1',
+ description: 'Step 1 Description',
+ schema: {
+ type: 'object',
+ properties: {
+ field1: { type: 'string', 'ui:field': 'MyCoolComponent' },
+ },
+ },
+ },
+ {
+ title: 'Step 2',
+ description: 'Step 2 Description',
+ schema: {
+ type: 'object',
+ properties: {
+ field2: { type: 'string', 'ui:field': 'MyCoolerComponent' },
+ },
+ },
+ },
+ ],
+ };
+
+ const { result } = renderHook(() => useTemplateSchema(manifest), {
+ wrapper: ({ children }) => (
+