Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@red-hat-developer-hub/backstage-plugin-orchestrator': patch
'@red-hat-developer-hub/backstage-plugin-orchestrator-form-api': patch
'@red-hat-developer-hub/backstage-plugin-orchestrator-form-widgets': patch
---

Scope SchemaUpdater replacements to the originating step and improve scope resolution.
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export type OrchestratorFormContextProps = {
export type OrchestratorFormDecorator = (FormComponent: React.ComponentType<FormDecoratorProps>) => React.ComponentType<OrchestratorFormContextProps>;

// @public
export type OrchestratorFormSchemaUpdater = (chunks: SchemaChunksResponse) => void;
export type OrchestratorFormSchemaUpdater = (chunks: SchemaChunksResponse, scopeId?: string) => void;

// @public
export type SchemaChunksResponse = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ export type SchemaChunksResponse = {
*/
export type OrchestratorFormSchemaUpdater = (
chunks: SchemaChunksResponse,
scopeId?: string,
) => void;

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ export const SchemaUpdater: Widget<
});

try {
updateSchema(typedData);
updateSchema(typedData, props.id);
} catch (err) {
// eslint-disable-next-line no-console
console.error('Error when updating schema', props.id, err);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,260 @@
/*
* Copyright Red Hat, Inc.
*
* 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 type { JSONSchema7 } from 'json-schema';

import { getSchemaUpdater } from './schemaUpdater';

describe('getSchemaUpdater', () => {
it('updates only within the scoped step when id is provided', () => {
const schema = {
type: 'object',
properties: {
'app-registration': {
type: 'object',
properties: {
schemaUpdater: { type: 'string' },
placeholderTwo: { type: 'string', title: 'App placeholder' },
},
},
'caas-namespace': {
type: 'object',
properties: {
schemaUpdater: { type: 'string' },
placeholderTwo: { type: 'string', title: 'CaaS placeholder' },
},
},
},
} as JSONSchema7;

const setSchema = jest.fn();
const updateSchema = getSchemaUpdater(schema, setSchema);

updateSchema(
{
placeholderTwo: {
type: 'string',
title: 'CaaS placeholder (updated)',
},
},
'root_caas-namespace_schemaUpdater',
);

expect(setSchema).toHaveBeenCalledTimes(1);
const updatedSchema = setSchema.mock.calls[0][0] as JSONSchema7;
expect(
(updatedSchema.properties?.['app-registration'] as JSONSchema7).properties
?.placeholderTwo,
).toEqual({ type: 'string', title: 'App placeholder' });
expect(
(updatedSchema.properties?.['caas-namespace'] as JSONSchema7).properties
?.placeholderTwo,
).toEqual({ type: 'string', title: 'CaaS placeholder (updated)' });
});

it('scopes updates to nested objects when SchemaUpdater is nested', () => {
const schema = {
type: 'object',
properties: {
step: {
type: 'object',
properties: {
placeholderTwo: { type: 'string', title: 'Step placeholder' },
nested: {
type: 'object',
properties: {
schemaUpdater: { type: 'string' },
placeholderTwo: { type: 'string', title: 'Nested placeholder' },
},
},
},
},
},
} as JSONSchema7;

const setSchema = jest.fn();
const updateSchema = getSchemaUpdater(schema, setSchema);

updateSchema(
{
placeholderTwo: {
type: 'string',
title: 'Nested placeholder (updated)',
},
},
'root_step_nested_schemaUpdater',
);

expect(setSchema).toHaveBeenCalledTimes(1);
const updatedSchema = setSchema.mock.calls[0][0] as JSONSchema7;
const stepProps = (updatedSchema.properties?.step as JSONSchema7)
.properties;
expect(stepProps?.placeholderTwo).toEqual({
type: 'string',
title: 'Step placeholder',
});
expect(
(stepProps?.nested as JSONSchema7).properties?.placeholderTwo,
).toEqual({ type: 'string', title: 'Nested placeholder (updated)' });
});

it('supports dot-notation ids for scoping', () => {
const schema = {
type: 'object',
properties: {
step: {
type: 'object',
properties: {
placeholderTwo: { type: 'string', title: 'Step placeholder' },
nested: {
type: 'object',
properties: {
schemaUpdater: { type: 'string' },
placeholderTwo: { type: 'string', title: 'Nested placeholder' },
},
},
},
},
},
} as JSONSchema7;

const setSchema = jest.fn();
const updateSchema = getSchemaUpdater(schema, setSchema);

updateSchema(
{
placeholderTwo: {
type: 'string',
title: 'Nested placeholder (dot updated)',
},
},
'root.step.nested.schemaUpdater',
);

expect(setSchema).toHaveBeenCalledTimes(1);
const updatedSchema = setSchema.mock.calls[0][0] as JSONSchema7;
const stepProps = (updatedSchema.properties?.step as JSONSchema7)
.properties;
expect(stepProps?.placeholderTwo).toEqual({
type: 'string',
title: 'Step placeholder',
});
expect(
(stepProps?.nested as JSONSchema7).properties?.placeholderTwo,
).toEqual({ type: 'string', title: 'Nested placeholder (dot updated)' });
});

it('resolves scope through array items', () => {
const schema = {
type: 'object',
properties: {
step: {
type: 'object',
properties: {
items: {
type: 'array',
items: {
type: 'object',
properties: {
schemaUpdater: { type: 'string' },
placeholderTwo: {
type: 'string',
title: 'Array placeholder',
},
},
},
},
},
},
},
} as JSONSchema7;

const setSchema = jest.fn();
const updateSchema = getSchemaUpdater(schema, setSchema);

updateSchema(
{
placeholderTwo: {
type: 'string',
title: 'Array placeholder (updated)',
},
},
'root_step_items_schemaUpdater',
);

expect(setSchema).toHaveBeenCalledTimes(1);
const updatedSchema = setSchema.mock.calls[0][0] as JSONSchema7;
const arrayProps = (
(updatedSchema.properties?.step as JSONSchema7).properties
?.items as JSONSchema7
).items as JSONSchema7;
expect(arrayProps.properties?.placeholderTwo).toEqual({
type: 'string',
title: 'Array placeholder (updated)',
});
});

it('prefers longest matching keys when scoping', () => {
const schema = {
type: 'object',
properties: {
app: {
type: 'object',
properties: {
schemaUpdater: { type: 'string' },
placeholderTwo: { type: 'string', title: 'App placeholder' },
},
},
'app-registration': {
type: 'object',
properties: {
schemaUpdater: { type: 'string' },
placeholderTwo: {
type: 'string',
title: 'App-registration placeholder',
},
},
},
},
} as JSONSchema7;

const setSchema = jest.fn();
const updateSchema = getSchemaUpdater(schema, setSchema);

updateSchema(
{
placeholderTwo: {
type: 'string',
title: 'App-registration placeholder (updated)',
},
},
'root_app-registration_schemaUpdater',
);

expect(setSchema).toHaveBeenCalledTimes(1);
const updatedSchema = setSchema.mock.calls[0][0] as JSONSchema7;
expect(
(updatedSchema.properties?.app as JSONSchema7).properties?.placeholderTwo,
).toEqual({ type: 'string', title: 'App placeholder' });
expect(
(updatedSchema.properties?.['app-registration'] as JSONSchema7).properties
?.placeholderTwo,
).toEqual({
type: 'string',
title: 'App-registration placeholder (updated)',
});
});
});
Loading
Loading