Skip to content

Commit bb02bdb

Browse files
Add selector default support for ActiveDropdown (#2251)
1 parent 3fadb68 commit bb02bdb

6 files changed

Lines changed: 158 additions & 38 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@red-hat-developer-hub/backstage-plugin-orchestrator-form-widgets': patch
3+
---
4+
5+
Allow ActiveDropdown fetch:response:default to be a JSONata selector evaluated against the fetch response.

workspaces/orchestrator/docs/orchestratorFormWidgets.md

Lines changed: 24 additions & 22 deletions
Large diffs are not rendered by default.

workspaces/orchestrator/plugins/orchestrator-form-widgets/src/utils/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,4 @@ export * from './useFetch';
2323
export * from './useFetchAndEvaluate';
2424
export * from './applySelector';
2525
export * from './useProcessingState';
26+
export * from './resolveDropdownDefault';
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
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+
import { resolveDropdownDefault } from './resolveDropdownDefault';
17+
18+
describe('resolveDropdownDefault', () => {
19+
it('uses selector-derived default when it matches a fetched option', async () => {
20+
const result = await resolveDropdownDefault({
21+
data: { selected: 'b' },
22+
values: ['a', 'b'],
23+
staticDefault: 'selected',
24+
});
25+
26+
expect(result).toBe('b');
27+
});
28+
29+
it('falls back to the static default when selector evaluation fails', async () => {
30+
const result = await resolveDropdownDefault({
31+
data: { other: 'value' },
32+
values: ['create', 'update'],
33+
staticDefault: 'create',
34+
});
35+
36+
expect(result).toBe('create');
37+
});
38+
});
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
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+
import { JsonObject } from '@backstage/types';
17+
18+
import { applySelectorString } from './applySelector';
19+
20+
type ResolveDropdownDefaultOptions = {
21+
data?: JsonObject;
22+
values: string[];
23+
staticDefault?: string;
24+
};
25+
26+
export const resolveDropdownDefault = async ({
27+
data,
28+
values,
29+
staticDefault,
30+
}: ResolveDropdownDefaultOptions): Promise<string | undefined> => {
31+
if (!values || values.length === 0) {
32+
return undefined;
33+
}
34+
35+
const hasStaticDefault = typeof staticDefault === 'string';
36+
let resolvedDefault: string | undefined;
37+
38+
if (hasStaticDefault && data) {
39+
try {
40+
resolvedDefault = await applySelectorString(data, staticDefault);
41+
} catch {
42+
resolvedDefault = undefined;
43+
}
44+
}
45+
46+
if (resolvedDefault && values.includes(resolvedDefault)) {
47+
return resolvedDefault;
48+
}
49+
50+
if (hasStaticDefault && values.includes(staticDefault)) {
51+
return staticDefault;
52+
}
53+
54+
return values[0];
55+
};

workspaces/orchestrator/plugins/orchestrator-form-widgets/src/widgets/ActiveDropdown.tsx

Lines changed: 35 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import {
3232
useRetriggerEvaluate,
3333
useTemplateUnitEvaluator,
3434
applySelectorArray,
35+
resolveDropdownDefault,
3536
useProcessingState,
3637
} from '../utils';
3738
import { UiProps } from '../uiPropTypes';
@@ -159,33 +160,51 @@ export const ActiveDropdown: Widget<
159160
);
160161

161162
// Set default value from fetched options
162-
// Priority: static default (if valid option) > first fetched option
163+
// Priority: selector default (if valid option) > static default (if valid) > first fetched option
163164
// Note: Static defaults are applied at form initialization level (in OrchestratorForm)
164165
useEffect(() => {
165-
if (
166-
!skipInitialValue &&
167-
!isChangedByUser &&
168-
!value &&
169-
values &&
170-
values.length > 0
171-
) {
172-
// If static default is provided and is a valid option, use it
173-
if (hasStaticDefault && values.includes(staticDefault)) {
174-
handleChange(staticDefault, false);
175-
} else {
176-
// Otherwise use the first fetched value
177-
handleChange(values[0], false);
166+
let isActive = true;
167+
const applyDefault = async () => {
168+
if (
169+
skipInitialValue ||
170+
isChangedByUser ||
171+
!values ||
172+
values.length === 0
173+
) {
174+
return;
178175
}
179-
}
176+
177+
const defaultValue = await resolveDropdownDefault({
178+
data,
179+
values,
180+
staticDefault: staticDefaultValue,
181+
});
182+
183+
if (!isActive || defaultValue === undefined) {
184+
return;
185+
}
186+
187+
const shouldApplyDefault =
188+
!value || (hasStaticDefault && value === staticDefaultValue);
189+
if (shouldApplyDefault && defaultValue !== value) {
190+
handleChange(defaultValue, false);
191+
}
192+
};
193+
194+
applyDefault();
195+
196+
return () => {
197+
isActive = false;
198+
};
180199
}, [
181200
handleChange,
182201
value,
183202
values,
184203
isChangedByUser,
185-
staticDefault,
186204
staticDefaultValue,
187205
hasStaticDefault,
188206
skipInitialValue,
207+
data,
189208
]);
190209

191210
const shouldShowFetchError = uiProps['fetch:error:silent'] !== true;

0 commit comments

Comments
 (0)