scaffolder: implement uiSchema support in template parameter schema

Co-authored-by: Johan Haals <johan.haals@gmail.com>
This commit is contained in:
Patrik Oldsberg
2021-02-19 16:03:39 +01:00
parent dffce46611
commit ccaa5d7d43
3 changed files with 172 additions and 31 deletions
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { JSONSchema } from '@backstage/catalog-model';
import { JsonObject } from '@backstage/config';
import { Content, StructuredMetadataTable } from '@backstage/core';
import {
Box,
@@ -28,10 +28,11 @@ import {
import { FormProps, IChangeEvent, withTheme } from '@rjsf/core';
import { Theme as MuiTheme } from '@rjsf/material-ui';
import React, { useState } from 'react';
import { transformSchemaToProps } from './schema';
const Form = withTheme(MuiTheme);
type Step = {
schema: JSONSchema;
schema: JsonObject;
title: string;
} & Partial<Omit<FormProps<any>, 'schema'>>;
@@ -66,35 +67,33 @@ export const MultistepJsonForm = ({
return (
<>
<Stepper activeStep={activeStep} orientation="vertical">
{steps.map(
({ title, schema: { title: _, ...schema }, ...formProps }) => (
<StepUI key={title}>
<StepLabel>
<Typography variant="h6">{title}</Typography>
</StepLabel>
<StepContent key={title}>
<Form
key={title}
noHtml5Validate
formData={formData}
onChange={onChange}
schema={schema as FormProps<any>['schema']}
onSubmit={e => {
if (e.errors.length === 0) handleNext();
}}
{...formProps}
>
<Button disabled={activeStep === 0} onClick={handleBack}>
Back
</Button>
<Button variant="contained" color="primary" type="submit">
Next step
</Button>
</Form>
</StepContent>
</StepUI>
),
)}
{steps.map(({ title, schema, ...formProps }) => (
<StepUI key={title}>
<StepLabel>
<Typography variant="h6">{title}</Typography>
</StepLabel>
<StepContent key={title}>
<Form
key={title}
noHtml5Validate
formData={formData}
onChange={onChange}
onSubmit={e => {
if (e.errors.length === 0) handleNext();
}}
{...formProps}
{...transformSchemaToProps(schema)}
>
<Button disabled={activeStep === 0} onClick={handleBack}>
Back
</Button>
<Button variant="contained" color="primary" type="submit">
Next step
</Button>
</Form>
</StepContent>
</StepUI>
))}
</Stepper>
{activeStep === steps.length && (
<Content>
@@ -0,0 +1,71 @@
/*
* Copyright 2020 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 { transformSchemaToProps } from './schema';
describe('transformSchemaToProps', () => {
it('transforms deep schema', () => {
const inputSchema = {
type: 'object',
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 = {
field1: {
'ui:derp': 'herp',
},
field2: {
fieldX: {
'ui:derp': 'xerp',
},
},
};
expect(transformSchemaToProps(inputSchema)).toEqual({
schema: expectedSchema,
uiSchema: expectedUiSchema,
});
});
});
@@ -0,0 +1,71 @@
/*
* 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 { JsonObject } from '@backstage/config';
import { FormProps } 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) {
const { properties } = schema;
if (!isObject(properties)) {
return;
}
for (const propName in properties) {
if (!properties.hasOwnProperty(propName)) {
continue;
}
const schemaNode = properties[propName];
if (!isObject(schemaNode)) {
continue;
}
if (schemaNode.type === 'object') {
const innerUiSchema = {};
uiSchema[propName] = innerUiSchema;
extractUiSchema(schemaNode, innerUiSchema);
} else {
for (const innerKey in schemaNode) {
if (!schemaNode.hasOwnProperty(innerKey)) {
continue;
}
const innerValue = schemaNode[innerKey];
if (innerKey.startsWith('ui:')) {
const innerUiSchema = uiSchema[propName] || {};
if (!isObject(innerUiSchema)) {
throw new TypeError('Unexpected non-object in uiSchema');
}
uiSchema[propName] = innerUiSchema;
innerUiSchema[innerKey] = innerValue;
delete schemaNode[innerKey];
}
}
}
}
}
export function transformSchemaToProps(
inputSchema: JsonObject,
): { schema: FormProps<any>['schema']; uiSchema: FormProps<any>['uiSchema'] } {
const schema = JSON.parse(JSON.stringify(inputSchema));
delete schema.title; // Rendered separately
const uiSchema = {};
extractUiSchema(schema, uiSchema);
return { schema, uiSchema };
}