Skip to content

Commit b9be64b

Browse files
authored
feat(orchestrator): add 'fetch:response:mandatory selector for ActiveMultiSelect (#1009)
Signed-off-by: Marek Libra <marek.libra@gmail.com>
1 parent f5e85c5 commit b9be64b

4 files changed

Lines changed: 56 additions & 10 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+
Added "fetch:response:mandatory" selector for the ActiveMultiSelect widget.

workspaces/orchestrator/docs/orchestratorFormWidgets.md

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,7 @@ Key Differentiators:
1515
- Provide default data or option lists.
1616
- Handle complex validation logic for widgets.
1717

18-
Implementation of the HTTP endpoints is out of the scope of this library.
19-
20-
Deployment Considerations:
21-
22-
- Use one or multiple servers depending on organizational needs.
23-
- Ensure endpoint structures and response formats exactly match the naming conventions and data structures defined in your schema’s `ui:props` by the creator of workflow's `data input schema`.
18+
Implementation of the HTTP endpoints is out of the scope of this library, they are expected to be custom developed to match rules and data sources of target environment.
2419

2520
## Content
2621

@@ -29,13 +24,19 @@ The frontend plugin provides implementation of `OrchestratorFormApi` (for `orche
2924
## Context
3025

3126
The provided widgets enable forms to incorporate dynamically retrieved data.
27+
3228
This data can be fetched from external HTTP servers, the Backstage API, as well as from other form fields, with all evaluations performed in real time during use.
3329

30+
## Deployment considerations
31+
32+
- Use one or multiple servers depending on organizational needs.
33+
- Ensure endpoint structures and response formats exactly match the naming conventions and data structures defined in your schema’s `ui:props` by the creator of workflow's `data input schema`.
34+
3435
## SchemaUpdater widget
3536

3637
Referenced as: `"ui:widget": "SchemaUpdater"`.
3738

38-
A headless widget used for fetching snippets of JSON schema and dynamically updating the RJSF form JSON schema on the fly.
39+
A **headless** widget used for fetching snippets of JSON schema and dynamically updating the RJSF form JSON schema on the fly.
3940

4041
Thanks to this component, complex subparts of the form can be changed based on data entered in other fields by the user.
4142

@@ -163,6 +164,12 @@ For the schema:
163164
}
164165
```
165166

167+
### Default mandatory data
168+
169+
When the optional `fetch:response:mandatory` JSONata selector is provided, it must return an array of strings that act as default values, which the user can not unselect.
170+
171+
Together with `fetch:retrigger` and other fetch-related parameters, the endpoint can continuously update the data for the selector.
172+
166173
### SchemaUpdater widget ui:props
167174

168175
The widget supports following `ui:props`:
@@ -172,6 +179,7 @@ The widget supports following `ui:props`:
172179
- fetch:method
173180
- fetch:body
174181
- fetch:response:value
182+
- fetch:response:mandatory
175183
- fetch:retrigger
176184

177185
[Check mode details](#content-of-uiprops)

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,15 @@ export function isJsonObject(value?: JsonValue): value is JsonObject {
2323
export const applySelectorArray = async (
2424
data: JsonObject,
2525
selector: string,
26+
createArrayIfNeeded: boolean = false,
2627
): Promise<string[]> => {
2728
const expression = jsonata(selector);
2829
const value = await expression.evaluate(data);
2930

31+
if (typeof value === 'string' && createArrayIfNeeded) {
32+
return [value];
33+
}
34+
3035
if (Array.isArray(value) && value.every(item => typeof item === 'string')) {
3136
return value;
3237
}

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

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,13 +67,15 @@ export const ActiveMultiSelect: Widget<
6767

6868
const autocompleteSelector =
6969
uiProps['fetch:response:autocomplete']?.toString();
70+
const mandatorySelector = uiProps['fetch:response:mandatory']?.toString();
7071

7172
const [localError] = useState<string | undefined>(
7273
autocompleteSelector
7374
? undefined
7475
: `Missing fetch:response:autocomplete selector for ${id}`,
7576
);
7677
const [autocompleteOptions, setAutocompleteOptions] = useState<string[]>();
78+
const [mandatoryValues, setMandatoryValues] = useState<string[]>();
7779

7880
const retrigger = useRetriggerEvaluate(
7981
templateUnitEvaluator,
@@ -97,10 +99,29 @@ export const ActiveMultiSelect: Widget<
9799
);
98100
setAutocompleteOptions(autocompleteValues);
99101
}
102+
103+
if (mandatorySelector) {
104+
const mandatory = await applySelectorArray(
105+
data,
106+
mandatorySelector,
107+
true,
108+
);
109+
setMandatoryValues(mandatory);
110+
if (!mandatory.every(item => value.includes(item))) {
111+
onChange([...new Set([...mandatory, ...value])]);
112+
}
113+
}
100114
};
101115

102116
doItAsync();
103-
}, [autocompleteSelector, data, props.id, value]);
117+
}, [
118+
autocompleteSelector,
119+
mandatorySelector,
120+
data,
121+
props.id,
122+
value,
123+
onChange,
124+
]);
104125

105126
const handleChange = (
106127
_: React.SyntheticEvent,
@@ -152,14 +173,21 @@ export const ActiveMultiSelect: Widget<
152173
renderTags={(values, getTagProps) =>
153174
values.map((item, index) => {
154175
const tagProps = getTagProps({ index });
155-
tagProps.className = clsx(tagProps.className, classes.chip);
176+
const { className, onDelete, ...restTagProps } = tagProps;
177+
156178
return (
157179
<Box key={item} title={item}>
158180
<Chip
159181
data-testid={`${id}-chip-${item}`}
160182
variant="outlined"
161183
label={item}
162-
{...tagProps}
184+
className={clsx(tagProps.className, classes.chip)}
185+
onDelete={
186+
mandatoryValues?.includes(item)
187+
? undefined /* mandatory - can not be deleted */
188+
: onDelete
189+
}
190+
{...restTagProps}
163191
/>
164192
</Box>
165193
);

0 commit comments

Comments
 (0)