Skip to content

Commit f47b22f

Browse files
replace values in ActiveMultiSelect when clearOnRetrigger is enabled (#3064)
1 parent c6824df commit f47b22f

3 files changed

Lines changed: 192 additions & 3 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-widgets': patch
3+
'@red-hat-developer-hub/backstage-plugin-orchestrator': patch
4+
---
5+
6+
Replace values in ActiveMultiSelect when clearOnRetrigger is enabled
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
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 { useState } from 'react';
17+
import { render, screen, waitFor } from '@testing-library/react';
18+
import { OrchestratorFormContextProps } from '@red-hat-developer-hub/backstage-plugin-orchestrator-form-api';
19+
import { ActiveMultiSelect } from './ActiveMultiSelect';
20+
import * as utils from '../utils';
21+
22+
jest.mock('../utils', () => {
23+
const actual = jest.requireActual('../utils');
24+
return {
25+
...actual,
26+
useTemplateUnitEvaluator: jest.fn(),
27+
useRetriggerEvaluate: jest.fn(),
28+
useFetch: jest.fn(),
29+
useProcessingState: jest.fn(),
30+
};
31+
});
32+
33+
const mockedUseTemplateUnitEvaluator =
34+
utils.useTemplateUnitEvaluator as jest.Mock;
35+
const mockedUseRetriggerEvaluate = utils.useRetriggerEvaluate as jest.Mock;
36+
const mockedUseFetch = utils.useFetch as jest.Mock;
37+
const mockedUseProcessingState = utils.useProcessingState as jest.Mock;
38+
39+
type HarnessProps = {
40+
initialValue?: string[];
41+
};
42+
43+
const WidgetHarness = ({ initialValue = [] }: HarnessProps) => {
44+
const [value, setValue] = useState<string[]>(initialValue);
45+
46+
return (
47+
<ActiveMultiSelect
48+
id="contacts"
49+
name="contacts"
50+
label="Contacts"
51+
required={false}
52+
readonly={false}
53+
disabled={false}
54+
autofocus={false}
55+
schema={{ type: 'array', items: { type: 'string' } }}
56+
uiSchema={{}}
57+
options={{
58+
props: {
59+
'fetch:response:autocomplete': '$.contacts ? $.contacts : []',
60+
'fetch:response:value': '$.contacts ? $.contacts : []',
61+
'fetch:retrigger': ['current.step.xParams.selectedEnvironment'],
62+
'fetch:clearOnRetrigger': true,
63+
},
64+
}}
65+
value={value}
66+
onChange={changed => setValue(changed as string[])}
67+
onBlur={() => {}}
68+
onFocus={() => {}}
69+
formContext={
70+
{
71+
formData: {
72+
current: {
73+
step: {
74+
xParams: {},
75+
},
76+
},
77+
},
78+
getIsChangedByUser: () => false,
79+
setIsChangedByUser: () => {},
80+
} as unknown as OrchestratorFormContextProps
81+
}
82+
rawErrors={[]}
83+
registry={{} as any}
84+
/>
85+
);
86+
};
87+
88+
describe('ActiveMultiSelect', () => {
89+
beforeEach(() => {
90+
mockedUseTemplateUnitEvaluator.mockReturnValue(() => undefined);
91+
mockedUseProcessingState.mockReturnValue({
92+
completeLoading: false,
93+
wrapProcessing: async (fn: () => Promise<void>) => {
94+
await fn();
95+
},
96+
});
97+
});
98+
99+
it('replaces values on retrigger when clearOnRetrigger is enabled', async () => {
100+
let retrigger = ['sandbox'];
101+
let responseData = { contacts: ['Alice', 'Bob', 'Charlie'] };
102+
103+
mockedUseRetriggerEvaluate.mockImplementation(() => retrigger);
104+
mockedUseFetch.mockImplementation(() => ({
105+
data: responseData,
106+
error: undefined,
107+
loading: false,
108+
}));
109+
110+
const { rerender } = render(<WidgetHarness />);
111+
112+
await waitFor(() => {
113+
expect(screen.getByTestId('contacts-chip-Alice')).toBeInTheDocument();
114+
expect(screen.getByTestId('contacts-chip-Bob')).toBeInTheDocument();
115+
expect(screen.getByTestId('contacts-chip-Charlie')).toBeInTheDocument();
116+
});
117+
118+
retrigger = ['prod'];
119+
responseData = { contacts: ['Alice', 'Bob', 'Charlie', 'Dave', 'Eve'] };
120+
rerender(<WidgetHarness />);
121+
122+
await waitFor(() => {
123+
expect(screen.getByTestId('contacts-chip-Dave')).toBeInTheDocument();
124+
expect(screen.getByTestId('contacts-chip-Eve')).toBeInTheDocument();
125+
});
126+
127+
retrigger = ['sandbox'];
128+
responseData = { contacts: ['Alice', 'Bob', 'Charlie'] };
129+
rerender(<WidgetHarness />);
130+
131+
await waitFor(() => {
132+
expect(screen.getByTestId('contacts-chip-Alice')).toBeInTheDocument();
133+
expect(screen.getByTestId('contacts-chip-Bob')).toBeInTheDocument();
134+
expect(screen.getByTestId('contacts-chip-Charlie')).toBeInTheDocument();
135+
expect(
136+
screen.queryByTestId('contacts-chip-Dave'),
137+
).not.toBeInTheDocument();
138+
expect(screen.queryByTestId('contacts-chip-Eve')).not.toBeInTheDocument();
139+
});
140+
});
141+
142+
it('replaces stale initial values when clearOnRetrigger is enabled', async () => {
143+
const retrigger = ['sandbox'];
144+
const responseData = { contacts: ['Alice', 'Bob', 'Charlie'] };
145+
146+
mockedUseRetriggerEvaluate.mockImplementation(() => retrigger);
147+
mockedUseFetch.mockImplementation(() => ({
148+
data: responseData,
149+
error: undefined,
150+
loading: false,
151+
}));
152+
153+
render(
154+
<WidgetHarness
155+
initialValue={['Alice', 'Bob', 'Charlie', 'Dave', 'Eve']}
156+
/>,
157+
);
158+
159+
await waitFor(() => {
160+
expect(screen.getByTestId('contacts-chip-Alice')).toBeInTheDocument();
161+
expect(screen.getByTestId('contacts-chip-Bob')).toBeInTheDocument();
162+
expect(screen.getByTestId('contacts-chip-Charlie')).toBeInTheDocument();
163+
expect(
164+
screen.queryByTestId('contacts-chip-Dave'),
165+
).not.toBeInTheDocument();
166+
expect(screen.queryByTestId('contacts-chip-Eve')).not.toBeInTheDocument();
167+
});
168+
});
169+
});

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

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
useCallback,
2020
useEffect,
2121
useMemo,
22+
useRef,
2223
useState,
2324
} from 'react';
2425
import clsx from 'clsx';
@@ -98,6 +99,7 @@ export const ActiveMultiSelect: Widget<
9899
: `Missing fetch:response:autocomplete selector for ${id}`,
99100
);
100101
const [inProgressItem, setInProgressItem] = useState<string>('');
102+
const clearedByRetriggerRef = useRef(false);
101103

102104
const [autocompleteOptions, setAutocompleteOptions] = useState<string[]>();
103105
const [mandatoryValues, setMandatoryValues] = useState<string[]>();
@@ -149,6 +151,7 @@ export const ActiveMultiSelect: Widget<
149151
);
150152

151153
const handleClear = useCallback(() => {
154+
clearedByRetriggerRef.current = true;
152155
setInProgressItem('');
153156
onChange([]);
154157
}, [onChange]);
@@ -217,11 +220,21 @@ export const ActiveMultiSelect: Widget<
217220
}
218221
}
219222

223+
const shouldIgnoreCurrentValue =
224+
clearOnRetrigger || clearedByRetriggerRef.current;
225+
const valueForMerge = shouldIgnoreCurrentValue ? [] : value;
226+
220227
if (
221-
!mandatory.every(item => value.includes(item)) ||
222-
!defaults.every(item => value.includes(item))
228+
!mandatory.every(item => valueForMerge.includes(item)) ||
229+
!defaults.every(item => valueForMerge.includes(item))
223230
) {
224-
onChange([...new Set([...mandatory, ...value, ...defaults])]);
231+
const mergedValues = [
232+
...new Set([...mandatory, ...valueForMerge, ...defaults]),
233+
];
234+
clearedByRetriggerRef.current = false;
235+
onChange(mergedValues);
236+
} else if (clearedByRetriggerRef.current) {
237+
clearedByRetriggerRef.current = false;
225238
}
226239
});
227240
};
@@ -238,6 +251,7 @@ export const ActiveMultiSelect: Widget<
238251
data,
239252
props.id,
240253
value,
254+
clearOnRetrigger,
241255
onChange,
242256
wrapProcessing,
243257
]);

0 commit comments

Comments
 (0)