fix review step processing using multi step templates

Signed-off-by: Stephen Glass <stephen@stephen.glass>
This commit is contained in:
Stephen Glass
2024-09-07 04:33:26 -04:00
parent 7b8446a5e1
commit 1f3c5aab4a
5 changed files with 254 additions and 6 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-scaffolder-react': patch
---
Fix scaffolder review step issue where schema options are not handled for fields on multi-step templates.
@@ -497,4 +497,109 @@ describe('ReviewState', () => {
expect(queryByRole('row', { name: 'Bar test' })).toBeInTheDocument();
expect(queryByRole('row', { name: 'Foo test' })).not.toBeInTheDocument();
});
it('should handle options in multiple schemas', async () => {
const formState = {
foo1: 'bar1',
foo2: 'bar2',
foo3: {
foo4: 'bar4',
},
foo5: 'bar5',
};
const schemas: ParsedTemplateSchema[] = [
{
mergedSchema: {
type: 'object',
properties: {
foo1: {
type: 'string',
'ui:backstage': {
review: {
name: 'Test 1',
},
},
},
},
},
schema: {},
title: 'Schema 1',
uiSchema: {},
},
{
mergedSchema: {
type: 'object',
properties: {
foo2: {
type: 'string',
'ui:backstage': {
review: {
name: 'Test 2',
},
},
},
foo3: {
type: 'object',
properties: {
foo4: {
type: 'string',
'ui:backstage': {
review: {
name: 'Test 4',
},
},
},
},
},
},
},
schema: {},
title: 'Schema 2',
uiSchema: {},
},
{
mergedSchema: {
type: 'object',
dependencies: {
foo1: {
oneOf: [
{
properties: {
foo5: {
type: 'string',
'ui:backstage': {
review: {
name: 'Test 5',
},
},
},
},
},
],
},
},
},
schema: {},
title: 'Schema 3',
uiSchema: {},
},
];
const { queryByRole } = render(
<ReviewState formState={formState} schemas={schemas} />,
);
// handles options in first schema
expect(queryByRole('row', { name: 'Test 1 bar1' })).toBeInTheDocument();
// handles options in second schema
expect(queryByRole('row', { name: 'Test 2 bar2' })).toBeInTheDocument();
// handles options for nested object in second schema
expect(queryByRole('row', { name: 'Test 4 bar4' })).toBeInTheDocument();
// handles options for property in dependencies in third schema
expect(queryByRole('row', { name: 'Test 5 bar5' })).toBeInTheDocument();
});
});
@@ -18,7 +18,7 @@ import { StructuredMetadataTable } from '@backstage/core-components';
import { JsonObject, JsonValue } from '@backstage/types';
import { Draft07 as JSONSchema } from 'json-schema-library';
import { ParsedTemplateSchema } from '../../hooks/useTemplateSchema';
import { isJsonObject, formatKey } from './util';
import { isJsonObject, formatKey, findSchemaForKey } from './util';
/**
* The props for the {@link ReviewState} component.
@@ -94,10 +94,10 @@ export const ReviewState = (props: ReviewStateProps) => {
const reviewData = Object.fromEntries(
Object.entries(props.formState)
.flatMap(([key, value]) => {
for (const step of props.schemas) {
return processSchema(key, value, step, props.formState);
}
return [[key, value]];
const schema = findSchemaForKey(key, props.schemas, props.formState);
return schema
? processSchema(key, value, schema, props.formState)
: [[key, value]];
})
.filter(prop => prop.length > 0),
);
@@ -14,7 +14,8 @@
* limitations under the License.
*/
import { isJsonObject, formatKey } from './util';
import { isJsonObject, formatKey, findSchemaForKey } from './util';
import { ParsedTemplateSchema } from '../../hooks/useTemplateSchema';
describe('isJsonObject', () => {
it('should return true for non-null objects', () => {
@@ -78,3 +79,95 @@ describe('formatKey', () => {
expect(formatKey('parent/child@!#$%^&*()')).toBe('Parent > Child');
});
});
describe('findSchemaForKey', () => {
const schemas: ParsedTemplateSchema[] = [
{
mergedSchema: {
type: 'object',
properties: {
foo: {
type: 'string',
},
},
},
schema: {},
title: 'Schema 1',
uiSchema: {},
},
{
mergedSchema: {
type: 'object',
properties: {
bar: {
type: 'string',
},
hello: {
type: 'object',
properties: {
world: {
type: 'string',
},
},
},
},
},
schema: {},
title: 'Schema 2',
uiSchema: {},
},
{
mergedSchema: {
type: 'object',
dependencies: {
foo: {
oneOf: [
{
properties: {
artifact: {
type: 'string',
},
},
},
],
},
},
},
schema: {},
title: 'Schema 3',
uiSchema: {},
},
];
const formState = {
foo: 'value',
bar: 'value',
hello: { world: 'value' },
artifact: 'value',
};
it('should return the schema for the direct key', () => {
const result = findSchemaForKey('foo', schemas, formState);
expect(result).toBe(schemas[0]);
});
it('should return the schema for a key in dependencies', () => {
const result = findSchemaForKey('artifact', schemas, formState);
expect(result).toBe(schemas[2]);
});
it('should return null if the key does not exist', () => {
const result = findSchemaForKey('nonexistentKey', schemas, formState);
expect(result).toBeNull();
});
it('should return the schema for a key in the second schema', () => {
const result = findSchemaForKey('bar', schemas, formState);
expect(result).toBe(schemas[1]);
});
it('should return the schema for an object key', () => {
const result = findSchemaForKey('hello', schemas, formState);
expect(result).toBe(schemas[1]);
});
});
@@ -16,6 +16,8 @@
import { JsonObject, JsonValue } from '@backstage/types';
import { startCase } from 'lodash';
import { ParsedTemplateSchema } from '../../hooks/useTemplateSchema';
import { Draft07 as JSONSchema } from 'json-schema-library';
export function isJsonObject(value?: JsonValue): value is JsonObject {
return typeof value === 'object' && value !== null && !Array.isArray(value);
@@ -29,3 +31,46 @@ export function formatKey(key: string): string {
.map(part => startCase(part))
.join(' > ');
}
export function findSchemaForKey(
key: string,
schemas: ParsedTemplateSchema[],
formState: Partial<{ [key: string]: JsonValue }>,
): ParsedTemplateSchema | null {
for (const step of schemas) {
/*
To determine if a key is defined in a schema we need to call getSchema
with an empty form state. Otherwise, it will never return undefined as it
will fallback to a default schema based on the form state.
An exception to this is when your schema is dynamic i.e. using dependencies
because the form state is required for generating the schema. In this case,
we add only the dependencies data to the getSchema call.
*/
const schema = step.mergedSchema;
// Declare data to be a subset of formState
const data: typeof formState = {};
if (schema.dependencies && isJsonObject(schema.dependencies)) {
for (const dep in schema.dependencies) {
if (formState.hasOwnProperty(dep)) {
data[dep] = formState[dep]; // Add each dependency key from formState
}
}
}
const draft = new JSONSchema(schema);
const res = draft.getSchema({
pointer: `#/${key}`,
data,
});
if (!!res) {
return step;
}
}
return null; // Return null if the key isn't found in any schema
}