Skip to content

Commit 4fa1356

Browse files
authored
feat(orchestrator): track user changes for setting defaults (#1638)
Signed-off-by: Marek Libra <marek.libra@gmail.com>
1 parent 64becf4 commit 4fa1356

File tree

8 files changed

+72
-20
lines changed

8 files changed

+72
-20
lines changed
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+
In the active widgets, the default value received from an endpoint now replaces the actual value, unless the user has modified it.

workspaces/orchestrator/plugins/orchestrator-form-api/report.api.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ export type OrchestratorFormContextProps = {
3636
children: React.ReactNode;
3737
onSubmit: (formData: JsonObject) => void;
3838
setAuthTokenDescriptors: (authTokenDescriptors: AuthTokenDescriptor[]) => void;
39+
getIsChangedByUser: (id: string) => boolean;
40+
setIsChangedByUser: (id: string, isChangedByUser: boolean) => void;
3941
};
4042

4143
// @public
@@ -56,7 +58,7 @@ export const useOrchestratorFormApiOrDefault: () => OrchestratorFormApi;
5658

5759
// Warnings were encountered during analysis:
5860
//
59-
// src/api.d.ts:96:22 - (ae-undocumented) Missing documentation for "useOrchestratorFormApiOrDefault".
61+
// src/api.d.ts:98:22 - (ae-undocumented) Missing documentation for "useOrchestratorFormApiOrDefault".
6062

6163
// (No @packageDocumentation comment for this package)
6264

workspaces/orchestrator/plugins/orchestrator-form-api/src/api.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ export type OrchestratorFormContextProps = {
4242
setAuthTokenDescriptors: (
4343
authTokenDescriptors: AuthTokenDescriptor[],
4444
) => void;
45+
getIsChangedByUser: (id: string) => boolean;
46+
setIsChangedByUser: (id: string, isChangedByUser: boolean) => void;
4547
};
4648

4749
/**

workspaces/orchestrator/plugins/orchestrator-form-react/src/components/OrchestratorForm.tsx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,20 @@ const OrchestratorForm = ({
114114
initialFormData ? () => structuredClone(initialFormData) : {},
115115
);
116116

117+
const [changedByUserMap, setChangedByUserMap] = useState<
118+
Record<string, boolean>
119+
>({});
120+
const getIsChangedByUser = useCallback(
121+
(id: string) => !!changedByUserMap[id],
122+
[changedByUserMap],
123+
);
124+
const setIsChangedByUser = useCallback(
125+
(id: string, isChangedByUser: boolean) => {
126+
setChangedByUserMap(prev => ({ ...prev, [id]: isChangedByUser }));
127+
},
128+
[],
129+
);
130+
117131
const schema = useMemo(() => removeHiddenSteps(rawSchema), [rawSchema]);
118132

119133
const numStepsInMultiStepSchema = useMemo(
@@ -162,6 +176,8 @@ const OrchestratorForm = ({
162176
formData={formData}
163177
setFormData={setFormData}
164178
setAuthTokenDescriptors={setAuthTokenDescriptors}
179+
getIsChangedByUser={getIsChangedByUser}
180+
setIsChangedByUser={setIsChangedByUser}
165181
>
166182
<Fragment />
167183
</OrchestratorFormWrapper> // it is required to pass the fragment so rjsf won't generate a Submit button
@@ -174,6 +190,8 @@ const OrchestratorForm = ({
174190
formData={formData}
175191
setFormData={setFormData}
176192
setAuthTokenDescriptors={setAuthTokenDescriptors}
193+
getIsChangedByUser={getIsChangedByUser}
194+
setIsChangedByUser={setIsChangedByUser}
177195
/>
178196
)}
179197
</StepperContextProvider>

workspaces/orchestrator/plugins/orchestrator-form-react/src/components/SingleStepForm.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ type SingleStepFormProps = Pick<
3232
| 'formData'
3333
| 'setFormData'
3434
| 'setAuthTokenDescriptors'
35+
| 'getIsChangedByUser'
36+
| 'setIsChangedByUser'
3537
>;
3638

3739
const SingleStepForm = (props: SingleStepFormProps) => {

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

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,9 @@ export const ActiveDropdown: Widget<
6060

6161
const { id, label, value, onChange, formContext } = props;
6262
const formData = formContext?.formData;
63+
const isChangedByUser = !!formContext?.getIsChangedByUser(id);
64+
const setIsChangedByUser = formContext?.setIsChangedByUser;
65+
6366
const labelId = `${props.id}-label`;
6467

6568
const uiProps = useMemo(
@@ -112,17 +115,22 @@ export const ActiveDropdown: Widget<
112115
}, [labelSelector, valueSelector, data, props.id]);
113116

114117
const handleChange = useCallback(
115-
(changed: string) => {
118+
(changed: string, isByUser: boolean) => {
119+
if (isByUser && setIsChangedByUser) {
120+
// we must handle this change out of this component's state since the component can be (de)mounted on wizard transitions or by the SchemaUpdater
121+
setIsChangedByUser(id, true);
122+
}
116123
onChange(changed);
117124
},
118-
[onChange],
125+
[onChange, id, setIsChangedByUser],
119126
);
120127

128+
// set default value to the first one
121129
useEffect(() => {
122-
if (!value && values && values.length > 0) {
123-
handleChange(values[0]);
130+
if (!isChangedByUser && values && values.length > 0) {
131+
handleChange(values[0], false);
124132
}
125-
}, [handleChange, value, values]);
133+
}, [handleChange, value, values, isChangedByUser]);
126134

127135
if (localError ?? error) {
128136
return <ErrorText text={localError ?? error ?? ''} id={id} />;
@@ -142,7 +150,7 @@ export const ActiveDropdown: Widget<
142150
value={value}
143151
label={label}
144152
disabled={isReadOnly}
145-
onChange={event => handleChange(event.target.value as string)}
153+
onChange={event => handleChange(event.target.value as string, true)}
146154
MenuProps={{
147155
PaperProps: { sx: { maxHeight: '20rem' } },
148156
}}

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

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ export const ActiveMultiSelect: Widget<
5858
const { id, name, label, value: rawValue, onChange, formContext } = props;
5959
const value = rawValue as string[];
6060
const formData = formContext?.formData;
61+
const isChangedByUser = !!formContext?.getIsChangedByUser(id);
62+
const setIsChangedByUser = formContext?.setIsChangedByUser;
6163

6264
const uiProps = useMemo(
6365
() => (props.options.props ?? {}) as UiProps,
@@ -76,7 +78,6 @@ export const ActiveMultiSelect: Widget<
7678
? undefined
7779
: `Missing fetch:response:autocomplete selector for ${id}`,
7880
);
79-
const [isTouched, setIsTouched] = useState(false);
8081
const [inProgressItem, setInProgressItem] = useState<string | undefined>();
8182

8283
const [autocompleteOptions, setAutocompleteOptions] = useState<string[]>();
@@ -115,7 +116,7 @@ export const ActiveMultiSelect: Widget<
115116
}
116117

117118
let defaults: string[] = [];
118-
if (!isTouched) {
119+
if (!isChangedByUser) {
119120
// set this just once, when the user has not touched the field
120121
if (defaultValueSelector) {
121122
defaults = await applySelectorArray(
@@ -147,7 +148,7 @@ export const ActiveMultiSelect: Widget<
147148
autocompleteSelector,
148149
mandatorySelector,
149150
defaultValueSelector,
150-
isTouched,
151+
isChangedByUser,
151152
data,
152153
props.id,
153154
value,
@@ -158,7 +159,9 @@ export const ActiveMultiSelect: Widget<
158159
_: SyntheticEvent,
159160
changed: AutocompleteValue<string[], false, false, false>,
160161
) => {
161-
setIsTouched(true);
162+
if (setIsChangedByUser) {
163+
setIsChangedByUser(id, true);
164+
}
162165
setInProgressItem(undefined);
163166
onChange(changed);
164167
};

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

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ export const ActiveTextInput: Widget<
5656

5757
const { id, label, value, onChange, formContext } = props;
5858
const formData = formContext?.formData;
59+
const isChangedByUser = !!formContext?.getIsChangedByUser(id);
60+
const setIsChangedByUser = formContext?.setIsChangedByUser;
5961

6062
const uiProps = useMemo(
6163
() => (props.options?.props ?? {}) as UiProps,
@@ -84,10 +86,14 @@ export const ActiveTextInput: Widget<
8486
const { data, error, loading } = useFetch(formData ?? {}, uiProps, retrigger);
8587

8688
const handleChange = useCallback(
87-
(changed: string) => {
89+
(changed: string, isByUser: boolean) => {
90+
if (isByUser && setIsChangedByUser) {
91+
// we must handle this change out of this component's state since the component can be (de)mounted on wizard transitions or by the SchemaUpdater
92+
setIsChangedByUser(id, true);
93+
}
8894
onChange(changed);
8995
},
90-
[onChange],
96+
[onChange, id, setIsChangedByUser],
9197
);
9298

9399
useEffect(() => {
@@ -96,15 +102,20 @@ export const ActiveTextInput: Widget<
96102
}
97103

98104
const doItAsync = async () => {
99-
if (value === undefined) {
100-
// loading default so do it only once
105+
if (!isChangedByUser) {
106+
// loading default so replace the value unless the user touched the field
101107
const defaultValue = await applySelectorString(
102108
data,
103109
defaultValueSelector,
104110
);
105111

106-
if (defaultValue && defaultValue !== null && defaultValue !== 'null') {
107-
handleChange(defaultValue);
112+
if (
113+
value !== defaultValue &&
114+
defaultValue &&
115+
defaultValue !== null &&
116+
defaultValue !== 'null'
117+
) {
118+
handleChange(defaultValue, false);
108119
}
109120
}
110121

@@ -125,6 +136,7 @@ export const ActiveTextInput: Widget<
125136
props.id,
126137
value,
127138
handleChange,
139+
isChangedByUser,
128140
]);
129141

130142
if (localError ?? error) {
@@ -140,7 +152,7 @@ export const ActiveTextInput: Widget<
140152
<TextField
141153
{...params}
142154
data-testid={`${id}-textfield`}
143-
onChange={event => handleChange(event.target.value)}
155+
onChange={event => handleChange(event.target.value, true)}
144156
label={label}
145157
disabled={isReadOnly}
146158
/>
@@ -152,7 +164,7 @@ export const ActiveTextInput: Widget<
152164
options={autocompleteOptions}
153165
data-testid={`${id}-autocomplete`}
154166
value={value}
155-
onChange={(_, v) => handleChange(v)}
167+
onChange={(_, v) => handleChange(v, true)}
156168
disabled={isReadOnly}
157169
renderInput={renderInput}
158170
renderOption={(liProps, item, state) => {
@@ -180,7 +192,7 @@ export const ActiveTextInput: Widget<
180192
<TextField
181193
value={value ?? ''}
182194
data-testid={`${id}-textfield`}
183-
onChange={event => handleChange(event.target.value)}
195+
onChange={event => handleChange(event.target.value, true)}
184196
label={label}
185197
/>
186198
</FormControl>

0 commit comments

Comments
 (0)