Skip to content

Commit 30eec39

Browse files
committed
refactor
1 parent aaa35ee commit 30eec39

File tree

3 files changed

+165
-115
lines changed

3 files changed

+165
-115
lines changed

src/components/ComponentsSelection/ComponentsSelection.tsx

Lines changed: 112 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useState } from 'react';
1+
import React, { useState, useMemo, useCallback } from 'react';
22
import {
33
CheckBox,
44
Select,
@@ -34,42 +34,65 @@ export const ComponentsSelection: React.FC<ComponentsSelectionProps> = ({
3434
}) => {
3535
const [searchTerm, setSearchTerm] = useState('');
3636
const { t } = useTranslation();
37-
const handleSelectionChange = (
38-
e: Ui5CustomEvent<CheckBoxDomRef, { checked: boolean }>,
39-
) => {
40-
const id = e.target?.id;
41-
setComponentsList(
42-
componentsList.map((component) =>
43-
component.name === id
44-
? { ...component, isSelected: !component.isSelected }
45-
: component,
46-
),
37+
38+
const selectedComponents = useMemo(
39+
() => getSelectedComponents(componentsList),
40+
[componentsList],
41+
);
42+
43+
const searchResults = useMemo(() => {
44+
const lowerSearch = searchTerm.toLowerCase();
45+
return componentsList.filter(({ name }) =>
46+
name.toLowerCase().includes(lowerSearch),
4747
);
48-
};
48+
}, [componentsList, searchTerm]);
4949

50-
const handleSearch = (e: Ui5CustomEvent<InputDomRef, never>) => {
50+
const handleSelectionChange = useCallback(
51+
(e: Ui5CustomEvent<CheckBoxDomRef, { checked: boolean }>) => {
52+
const id = e.target?.id;
53+
if (!id) return;
54+
setComponentsList(
55+
componentsList.map((component) =>
56+
component.name === id
57+
? { ...component, isSelected: !component.isSelected }
58+
: component,
59+
),
60+
);
61+
},
62+
[componentsList, setComponentsList],
63+
);
64+
65+
const handleSearch = useCallback((e: Ui5CustomEvent<InputDomRef, never>) => {
5166
setSearchTerm(e.target.value.trim());
52-
};
67+
}, []);
5368

54-
const handleVersionChange = (
55-
e: Ui5CustomEvent<SelectDomRef, { selectedOption: HTMLElement }>,
56-
) => {
57-
const selectedOption = e.detail.selectedOption as HTMLElement;
58-
const name = selectedOption.dataset.name;
59-
const version = selectedOption.dataset.version;
60-
setComponentsList(
61-
componentsList.map((component) =>
62-
component.name === name
63-
? { ...component, selectedVersion: version || '' }
64-
: component,
65-
),
66-
);
67-
};
69+
const handleVersionChange = useCallback(
70+
(e: Ui5CustomEvent<SelectDomRef, { selectedOption: HTMLElement }>) => {
71+
const selectedOption = e.detail.selectedOption as HTMLElement;
72+
const name = selectedOption.dataset.name;
73+
const version = selectedOption.dataset.version;
74+
if (!name) return;
75+
setComponentsList(
76+
componentsList.map((component) =>
77+
component.name === name
78+
? { ...component, selectedVersion: version || '' }
79+
: component,
80+
),
81+
);
82+
},
83+
[componentsList, setComponentsList],
84+
);
6885

69-
const searchResults = componentsList.filter(({ name }) =>
70-
name.toLowerCase().includes(searchTerm.toLowerCase()),
86+
const isProviderDisabled = useCallback(
87+
(component: ComponentsListItem) => {
88+
if (!component.name?.includes('provider')) return false;
89+
const crossplane = componentsList.find(
90+
({ name }) => name === 'crossplane',
91+
);
92+
return crossplane?.isSelected === false;
93+
},
94+
[componentsList],
7195
);
72-
const selectedComponents = getSelectedComponents(componentsList);
7396

7497
return (
7598
<div>
@@ -80,62 +103,75 @@ export const ComponentsSelection: React.FC<ComponentsSelectionProps> = ({
80103
id="search"
81104
showClearIcon
82105
icon={<Icon name="search" />}
106+
value={searchTerm}
107+
aria-label={t('common.search')}
83108
onInput={handleSearch}
84109
/>
85110

86111
<Grid>
87112
<div data-layout-span="XL8 L8 M8 S8">
88-
{searchResults.map((component) => {
89-
const isProviderDisabled =
90-
component.name?.includes('provider') &&
91-
componentsList?.find(({ name }) => name === 'crossplane')
92-
?.isSelected === false;
93-
return (
94-
<FlexBox
95-
key={component.name}
96-
className={styles.row}
97-
gap={10}
98-
justifyContent="SpaceBetween"
99-
>
100-
<CheckBox
101-
valueState="None"
102-
text={component.name}
103-
id={component.name}
104-
checked={component.isSelected}
105-
disabled={isProviderDisabled}
106-
onChange={handleSelectionChange}
107-
/>
113+
{searchResults.length > 0 ? (
114+
searchResults.map((component) => {
115+
const providerDisabled = isProviderDisabled(component);
116+
return (
108117
<FlexBox
118+
key={component.name}
119+
className={styles.row}
109120
gap={10}
110121
justifyContent="SpaceBetween"
111-
alignItems="Baseline"
122+
data-testid={`component-row-${component.name}`}
112123
>
113-
{/*This button will be implemented later*/}
114-
{component.documentationUrl && (
115-
<Button design="Transparent">
116-
{t('common.documentation')}
117-
</Button>
118-
)}
119-
<Select
120-
value={component.selectedVersion}
121-
disabled={!component.isSelected || isProviderDisabled}
122-
onChange={handleVersionChange}
124+
<CheckBox
125+
valueState="None"
126+
text={component.name}
127+
id={component.name}
128+
checked={component.isSelected}
129+
disabled={providerDisabled}
130+
aria-label={component.name}
131+
onChange={handleSelectionChange}
132+
/>
133+
<FlexBox
134+
gap={10}
135+
justifyContent="SpaceBetween"
136+
alignItems="Baseline"
123137
>
124-
{component.versions.map((version) => (
125-
<Option
126-
key={version}
127-
data-version={version}
128-
data-name={component.name}
129-
selected={component.selectedVersion === version}
138+
{/* TODO: Add documentation link */}
139+
{component.documentationUrl && (
140+
<Button
141+
design="Transparent"
142+
rel="noopener noreferrer"
143+
aria-label={t('common.documentation')}
144+
tabIndex={0}
130145
>
131-
{version}
132-
</Option>
133-
))}
134-
</Select>
146+
{t('common.documentation')}
147+
</Button>
148+
)}
149+
<Select
150+
value={component.selectedVersion}
151+
disabled={!component.isSelected || providerDisabled}
152+
aria-label={`${component.name} version`}
153+
onChange={handleVersionChange}
154+
>
155+
{component.versions.map((version) => (
156+
<Option
157+
key={version}
158+
data-version={version}
159+
data-name={component.name}
160+
selected={component.selectedVersion === version}
161+
>
162+
{version}
163+
</Option>
164+
))}
165+
</Select>
166+
</FlexBox>
135167
</FlexBox>
136-
</FlexBox>
137-
);
138-
})}
168+
);
169+
})
170+
) : (
171+
<Infobox fullWidth variant="success">
172+
<Text>{t('componentsSelection.pleaseSelectComponents')}</Text>
173+
</Infobox>
174+
)}
139175
</div>
140176
<div data-layout-span="XL4 L4 M4 S4">
141177
{selectedComponents.length > 0 ? (
@@ -149,7 +185,7 @@ export const ComponentsSelection: React.FC<ComponentsSelectionProps> = ({
149185
))}
150186
</List>
151187
) : (
152-
<Infobox fullWidth variant={'success'}>
188+
<Infobox fullWidth variant="success">
153189
<Text>{t('componentsSelection.pleaseSelectComponents')}</Text>
154190
</Infobox>
155191
)}
Lines changed: 46 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useEffect, useState } from 'react';
1+
import React, { useEffect, useRef } from 'react';
22
import { ComponentsSelection } from './ComponentsSelection.tsx';
33

44
import IllustratedError from '../Shared/IllustratedError.tsx';
@@ -15,17 +15,22 @@ export interface ComponentsSelectionProps {
1515
setComponentsList: (components: ComponentsListItem[]) => void;
1616
}
1717

18-
// get selected components and when Crossplane is selected then also providers
19-
export const getSelectedComponents = (components: ComponentsListItem[]) =>
20-
components.filter(
21-
(component) =>
22-
component.isSelected &&
23-
!(
24-
component.name?.includes('provider') &&
25-
components?.find(({ name }) => name === 'crossplane')?.isSelected ===
26-
false
27-
),
18+
/**
19+
* Returns the selected components. If Crossplane is not selected,
20+
* provider components are excluded.
21+
*/
22+
export const getSelectedComponents = (components: ComponentsListItem[]) => {
23+
const isCrossplaneSelected = components.some(
24+
({ name, isSelected }) => name === 'crossplane' && isSelected,
2825
);
26+
return components.filter((component) => {
27+
if (!component.isSelected) return false;
28+
if (component.name?.includes('provider') && !isCrossplaneSelected) {
29+
return false;
30+
}
31+
return true;
32+
});
33+
};
2934

3035
export const ComponentsSelectionContainer: React.FC<
3136
ComponentsSelectionProps
@@ -35,44 +40,52 @@ export const ComponentsSelectionContainer: React.FC<
3540
error,
3641
isLoading,
3742
} = useApiResource(ListManagedComponents());
38-
const [isReady, setIsReady] = useState(false);
3943
const { t } = useTranslation();
44+
const initialized = useRef(false);
45+
4046
useEffect(() => {
4147
if (
42-
availableManagedComponentsListData?.items.length === 0 ||
48+
initialized.current ||
4349
!availableManagedComponentsListData?.items ||
44-
isReady
45-
)
50+
availableManagedComponentsListData.items.length === 0
51+
) {
4652
return;
53+
}
4754

48-
setComponentsList(
49-
availableManagedComponentsListData?.items?.map((item) => {
55+
const newComponentsList = availableManagedComponentsListData.items.map(
56+
(item) => {
5057
const versions = sortVersions(item.status.versions);
5158
return {
5259
name: item.metadata.name,
53-
versions: versions,
54-
selectedVersion: versions[0],
60+
versions,
61+
selectedVersion: versions[0] ?? '',
5562
isSelected: false,
5663
documentationUrl: '',
5764
};
58-
}) ?? [],
65+
},
5966
);
60-
setIsReady(true);
61-
}, [availableManagedComponentsListData, isReady, setComponentsList]);
67+
68+
setComponentsList(newComponentsList);
69+
initialized.current = true;
70+
}, [availableManagedComponentsListData, setComponentsList]);
71+
6272
if (isLoading) {
6373
return <Loading />;
6474
}
65-
if (error) return <IllustratedError />;
75+
76+
if (error) {
77+
return <IllustratedError />;
78+
}
79+
80+
// Defensive: If the API returned no items, show error
81+
if (!componentsList || componentsList.length === 0) {
82+
return <IllustratedError title={t('componentsSelection.cannotLoad')} />;
83+
}
84+
6685
return (
67-
<>
68-
{componentsList.length > 0 ? (
69-
<ComponentsSelection
70-
componentsList={componentsList}
71-
setComponentsList={setComponentsList}
72-
/>
73-
) : (
74-
<IllustratedError title={t('componentsSelection.cannotLoad')} />
75-
)}
76-
</>
86+
<ComponentsSelection
87+
componentsList={componentsList}
88+
setComponentsList={setComponentsList}
89+
/>
7790
);
7891
};

0 commit comments

Comments
 (0)