diff --git a/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/schema.test.ts b/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/schema.test.ts index b368b832a4..5c56c861b1 100644 --- a/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/schema.test.ts +++ b/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/schema.test.ts @@ -1,5 +1,5 @@ /* - * Copyright 2022 The Backstage Authors + * 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. @@ -13,11 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { extractSchemaFromManifest } from './schema'; -describe('schema utils', () => { - it('should do stuff', () => { - const inputSchema = { +import { JsonObject } from '@backstage/types'; +import { TemplateParameterSchema } from '../../../types'; +import { extractSchemaFromStep } from './schema'; + +describe('extractSchemaFromStep', () => { + it('transforms deep schema', () => { + const inputSchema: JsonObject = { type: 'object', 'ui:welp': 'warp', properties: { @@ -64,12 +67,347 @@ describe('schema utils', () => { }, }; - expect( - extractSchemaFromManifest({ - title: 'test', - steps: [{ title: 'test', schema: inputSchema }], - }), - ).toEqual({ + 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 index 6140cfd978..d255e56e27 100644 --- a/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/schema.ts +++ b/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/schema.ts @@ -13,27 +13,99 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { JsonObject } from '@backstage/types'; import { UiSchema } from '@rjsf/core'; import { JSONSchema7 } from 'json-schema'; import { TemplateParameterSchema } from '../../../types'; -export const extractSchemaFromManifest = ( - manifest: TemplateParameterSchema, -): { uiSchema: UiSchema; schema: JSONSchema7 } => { - const schema = manifest.steps[0].schema; - const uiSchema = JSON.parse(JSON.stringify(schema), (key, value) => { - if (typeof value === 'object') { - return Object.fromEntries( - Object.entries(value).filter( - ([k, v]) => k.startsWith('ui:') || typeof v === 'object', - ), - ); +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; } - return value; - }); + if (propName.startsWith('ui:')) { + uiSchema[propName] = schema[propName]; + delete schema[propName]; + } + } - console.log(uiSchema); - return { uiSchema, schema: {} }; + 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); + } + } +} + +export const extractSchemaFromStep = ( + inputStep: JSONSchema7, +): { uiSchema: UiSchema; schema: JSONSchema7 } => { + const uiSchema: UiSchema = {}; + const returnSchema: JsonObject = JSON.parse(JSON.stringify(inputStep)); + extractUiSchema(returnSchema, uiSchema); + return { uiSchema, schema: returnSchema }; };