Skip to content

Commit 64fd859

Browse files
fix(orchestrator): honor json schema defaults in initial formData (#2654)
* fix(orchestrator): honor json schema defaults Ensure extractStaticDefaults falls back to JSON Schema defaults when ui:props fetch:response:default is absent so initial formData includes schema defaults. Made-with: Cursor * chore(changeset): document schema default fix Add changeset for orchestrator form defaults update. Made-with: Cursor * fix(orchestrator): handle root defaults Avoid setting an empty key for root schema defaults and add tests to cover root default handling. Made-with: Cursor * fix(orchestrator): document and test defaults Clarify extractStaticDefaults precedence in docs and add test coverage for default handling. Made-with: Cursor
1 parent ac2cbf8 commit 64fd859

3 files changed

Lines changed: 151 additions & 7 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@red-hat-developer-hub/backstage-plugin-orchestrator-form-react': patch
3+
---
4+
5+
Include JSON Schema `default` values when computing initial form data so
6+
template expressions can resolve defaults before widgets render.
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
/*
2+
* Copyright Red Hat, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import type { JSONSchema7 } from 'json-schema';
18+
19+
import extractStaticDefaults from './extractStaticDefaults';
20+
21+
describe('extractStaticDefaults', () => {
22+
it('applies schema defaults when no fetch default', () => {
23+
const schema: JSONSchema7 = {
24+
type: 'object',
25+
properties: {
26+
name: { type: 'string', default: 'app' },
27+
},
28+
};
29+
30+
expect(extractStaticDefaults(schema)).toEqual({ name: 'app' });
31+
});
32+
33+
it('prefers fetch:response:default over schema default', () => {
34+
const schema: JSONSchema7 = {
35+
type: 'object',
36+
properties: {
37+
name: {
38+
type: 'string',
39+
default: 'schema',
40+
'ui:props': { 'fetch:response:default': 'fetch' },
41+
} as JSONSchema7 & Record<string, unknown>,
42+
},
43+
};
44+
45+
expect(extractStaticDefaults(schema)).toEqual({ name: 'fetch' });
46+
});
47+
48+
it('does not overwrite existing form data values', () => {
49+
const schema: JSONSchema7 = {
50+
type: 'object',
51+
properties: {
52+
name: { type: 'string', default: 'schema' },
53+
},
54+
};
55+
56+
expect(extractStaticDefaults(schema, { name: 'existing' })).toEqual({
57+
name: 'existing',
58+
});
59+
});
60+
61+
it('preserves falsy defaults', () => {
62+
const schema: JSONSchema7 = {
63+
type: 'object',
64+
properties: {
65+
enabled: { type: 'boolean', default: false },
66+
retries: { type: 'number', default: 0 },
67+
note: { type: 'string', default: '' },
68+
},
69+
};
70+
71+
expect(extractStaticDefaults(schema)).toEqual({
72+
enabled: false,
73+
retries: 0,
74+
note: '',
75+
});
76+
});
77+
78+
it('applies defaults from composed schemas', () => {
79+
const schema: JSONSchema7 = {
80+
type: 'object',
81+
allOf: [
82+
{
83+
type: 'object',
84+
properties: {
85+
name: { type: 'string', default: 'composed' },
86+
},
87+
},
88+
],
89+
};
90+
91+
expect(extractStaticDefaults(schema)).toEqual({ name: 'composed' });
92+
});
93+
94+
it('applies root default objects without creating empty key', () => {
95+
const schema: JSONSchema7 = {
96+
type: 'object',
97+
default: { foo: 'bar' },
98+
properties: {
99+
foo: { type: 'string' },
100+
},
101+
};
102+
103+
expect(extractStaticDefaults(schema)).toEqual({ foo: 'bar' });
104+
});
105+
106+
it('does not override existing data with root defaults', () => {
107+
const schema: JSONSchema7 = {
108+
type: 'object',
109+
default: { foo: 'bar' },
110+
properties: {
111+
foo: { type: 'string' },
112+
},
113+
};
114+
115+
expect(extractStaticDefaults(schema, { foo: 'existing' })).toEqual({
116+
foo: 'existing',
117+
});
118+
});
119+
});

workspaces/orchestrator/plugins/orchestrator-form-react/src/utils/extractStaticDefaults.ts

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,19 @@ import type { JSONSchema7, JSONSchema7Definition } from 'json-schema';
2020
import get from 'lodash/get';
2121
import set from 'lodash/set';
2222

23+
const isPlainObject = (value: unknown): value is JsonObject => {
24+
return value !== null && typeof value === 'object' && !Array.isArray(value);
25+
};
26+
2327
/**
24-
* Extracts static default values from fetch:response:default properties in the schema.
28+
* Extracts static default values from the schema in priority order:
29+
* 1) ui:props.fetch:response:default
30+
* 2) JSON Schema default
31+
*
2532
* These values are applied to formData before widgets render, ensuring defaults
2633
* are available immediately without waiting for fetch operations.
2734
*
28-
* @param schema - The JSON Schema containing ui:props with fetch:response:default
35+
* @param schema - The JSON Schema containing ui:props and/or default values
2936
* @param existingFormData - Existing form data to preserve (won't be overwritten)
3037
* @returns An object containing the extracted default values merged with existing data
3138
*/
@@ -60,14 +67,26 @@ export function extractStaticDefaults(
6067

6168
// Extract fetch:response:default from ui:props
6269
const uiProps = (curSchema as Record<string, unknown>)['ui:props'];
70+
let staticDefault: unknown;
6371
if (uiProps && typeof uiProps === 'object') {
64-
const staticDefault = (uiProps as Record<string, unknown>)[
72+
staticDefault = (uiProps as Record<string, unknown>)[
6573
'fetch:response:default'
6674
];
67-
if (staticDefault !== undefined) {
68-
// Only set if not already in existing form data
69-
const existingValue = get(existingFormData, path);
70-
if (existingValue === undefined || existingValue === null) {
75+
}
76+
77+
if (staticDefault === undefined && 'default' in curSchema) {
78+
staticDefault = (curSchema as JSONSchema7).default;
79+
}
80+
81+
if (staticDefault !== undefined) {
82+
// Only set if not already in existing form data
83+
const existingValue = get(existingFormData, path);
84+
if (existingValue === undefined || existingValue === null) {
85+
if (path === '') {
86+
if (isPlainObject(staticDefault)) {
87+
Object.assign(defaults, staticDefault);
88+
}
89+
} else {
7190
set(defaults, path, staticDefault);
7291
}
7392
}

0 commit comments

Comments
 (0)