Closed cmoulliard closed 8 months ago
I did some minor changes to try to set selected
=> formData
or defaultQuarkusVersion
to select the proper value when we return back to the screen of the popup but we still got as first item of the list 3.8 (RECOMMENDED)
Definition of the backstage selectComponent
: https://github.com/backstage/backstage/blob/master/packages/core-components/src/components/Select/Select.tsx#L143
import React, {useState} from "react";
import FormControl from "@material-ui/core/FormControl";
import {FieldExtensionComponentProps} from "@backstage/plugin-scaffolder-react";
import {Progress, Select, SelectItem} from "@backstage/core-components";
import useAsync from 'react-use/esm/useAsync';
/* Example returned by code.quarkus.io/api/streams
{
"javaCompatibility": {
"recommended": 17,
"versions": [
17,
21
]
},
"key": "io.quarkus.platform:3.8",
"lts": false,
"platformVersion": "3.8.2",
"quarkusCoreVersion": "3.8.2",
"recommended": true,
"status": "FINAL"
}
*/
export interface Version {
key: string;
quarkusCoreVersion: string;
platformVersion: string;
lts: boolean;
recommended: boolean;
javaCompatibility: javaCompatibility[];
status: string;
}
export interface javaCompatibility {
recommended: boolean;
versions: string[];
}
function userLabel(v: Version) {
const key = v.key.split(":")
if (v.recommended) {
return `${key[1]} (RECOMMENDED)`;
} else if (v.status !== "FINAL") {
return `${key[1]} (${v.status})`;
} else {
return key[1];
}
}
export const QuarkusVersionList = (props: FieldExtensionComponentProps<string>) => {
const {
onChange,
rawErrors,
required,
formData,
} = props;
const [quarkusVersion, setQuarkusVersion] = useState<Version[]>([]);
let selected: string = '';
const codeQuarkusUrl = 'https://code.quarkus.io';
const apiStreamsUrl = `${codeQuarkusUrl}/api/streams`
const {loading, value} = useAsync(async () => {
const response = await fetch(apiStreamsUrl);
const newData = await response.json();
setQuarkusVersion(newData)
// If formData is not undefined and null, then we assign it as the selected value
if (formData !== undefined && formData!== null) {
selected = formData;
console.log(`Quarkus Version selected : ${formData}`)
} else {
newData.forEach((v: Version) => {
console.log(`Version key: ${v.key}`);
if (v.recommended) {
selected = v.key
onChange(v.key);
console.log(`Recommended Quarkus Version is: ${v.key}`)
}
});
}
return newData;
});
const versionItems: SelectItem[] = value
? value?.map((i: Version) => ({label: userLabel(i), value: i.key}))
: [{label: 'Loading...', value: 'loading'}];
if (loading) {
return <Progress/>;
} else {
return (
<FormControl
margin="normal"
required={required}
error={rawErrors?.length > 0 && !formData}
>
<Select
native
label="Quarkus versions"
onChange={s => {
let resp = String(Array.isArray(s) ? s[0] : s)
onChange(resp)
selected = resp;
console.log(`Selected : ${resp}`)
}}
disabled={quarkusVersion.length === 1}
selected={selected}
items={versionItems}
/>
</FormControl>)
}
}
export default QuarkusVersionList;
3.2
As you can see from console the selected key is io.quarkus.platform:3.2
The selected version is still io.quarkus.platform:3.2
but the popup list show alway the first item 3.8 (RECOMMENDED)
Idea: @iocanel
As the RepoUrlPicker
(github, azure, bitbucket, gitea, etc) which is using the backstage <Select/>
component is able to keep what the user selects, then we should be able (if we compare the code) to avoid the issue commented before.
// Backstage select code: https://github.com/backstage/backstage/blob/master/packages/core-components/src/components/Select/Select.tsx#L143
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import Box from '@material-ui/core/Box';
import Checkbox from '@material-ui/core/Checkbox';
import Chip from '@material-ui/core/Chip';
import FormControl from '@material-ui/core/FormControl';
import InputBase from '@material-ui/core/InputBase';
import InputLabel from '@material-ui/core/InputLabel';
import MenuItem from '@material-ui/core/MenuItem';
import Select from '@material-ui/core/Select';
import {
createStyles,
makeStyles,
Theme,
withStyles,
} from '@material-ui/core/styles';
import Typography from '@material-ui/core/Typography';
import React, { useEffect, useState } from 'react';
import ClosedDropdown from './static/ClosedDropdown';
import OpenedDropdown from './static/OpenedDropdown';
/** @public */
export type SelectInputBaseClassKey = 'root' | 'input';
const BootstrapInput = withStyles(
(theme: Theme) =>
createStyles({
root: {
'label + &': {
marginTop: theme.spacing(3),
},
'&.Mui-focused > div[role=button]': {
borderColor: theme.palette.primary.main,
},
},
input: {
borderRadius: theme.shape.borderRadius,
position: 'relative',
backgroundColor: theme.palette.background.paper,
border: '1px solid #ced4da',
fontSize: theme.typography.body1.fontSize,
padding: theme.spacing(1.25, 3.25, 1.25, 1.5),
transition: theme.transitions.create(['border-color', 'box-shadow']),
'&:focus': {
background: theme.palette.background.paper,
borderRadius: theme.shape.borderRadius,
},
},
}),
{ name: 'BackstageSelectInputBase' },
)(InputBase);
/** @public */
export type SelectClassKey =
| 'formControl'
| 'label'
| 'chips'
| 'chip'
| 'checkbox'
| 'root';
const useStyles = makeStyles(
(theme: Theme) =>
createStyles({
formControl: {
margin: theme.spacing(1, 0),
maxWidth: 300,
},
label: {
transform: 'initial',
fontWeight: 'bold',
fontSize: theme.typography.body2.fontSize,
fontFamily: theme.typography.fontFamily,
color: theme.palette.text.primary,
'&.Mui-focused': {
color: theme.palette.text.primary,
},
},
formLabel: {
transform: 'initial',
fontWeight: 'bold',
fontSize: theme.typography.body2.fontSize,
fontFamily: theme.typography.fontFamily,
color: theme.palette.text.primary,
'&.Mui-focused': {
color: theme.palette.text.primary,
},
},
chips: {
display: 'flex',
flexWrap: 'wrap',
},
chip: {
margin: 2,
},
checkbox: {},
root: {
display: 'flex',
flexDirection: 'column',
},
}),
{ name: 'BackstageSelect' },
);
/** @public */
export type SelectItem = {
label: string;
value: string | number;
};
/** @public */
export type SelectedItems = string | string[] | number | number[];
export type SelectProps = {
multiple?: boolean;
items: SelectItem[];
label: string;
placeholder?: string;
selected?: SelectedItems;
onChange: (arg: SelectedItems) => void;
triggerReset?: boolean;
native?: boolean;
disabled?: boolean;
margin?: 'dense' | 'none';
};
/** @public */
export function SelectComponent(props: SelectProps) {
const {
multiple,
items,
label,
placeholder,
selected,
onChange,
triggerReset,
native = false,
disabled = false,
margin,
} = props;
const classes = useStyles();
const [value, setValue] = useState<SelectedItems>(
selected || (multiple ? [] : ''),
);
const [isOpen, setOpen] = useState(false);
useEffect(() => {
setValue(multiple ? [] : '');
}, [triggerReset, multiple]);
useEffect(() => {
setValue(selected || (multiple ? [] : ''));
}, [selected, multiple]);
const handleChange = (event: React.ChangeEvent<{ value: unknown }>) => {
setValue(event.target.value as SelectedItems);
onChange(event.target.value as SelectedItems);
};
const handleOpen = (event: React.ChangeEvent<any>) => {
if (disabled) {
event.preventDefault();
return;
}
setOpen(previous => {
if (multiple && !(event.target instanceof HTMLElement)) {
return true;
}
return !previous;
});
};
const handleClose = () => {
setOpen(false);
};
const handleDelete = (selectedValue: string | number) => () => {
const newValue = (value as any[]).filter(chip => chip !== selectedValue);
setValue(newValue);
onChange(newValue);
};
return (
<Box className={classes.root}>
<FormControl className={classes.formControl}>
<InputLabel className={classes.formLabel}>{label}</InputLabel>
<Select
aria-label={label}
value={value}
native={native}
disabled={disabled}
data-testid="select"
displayEmpty
multiple={multiple}
margin={margin}
onChange={handleChange}
open={isOpen}
onOpen={handleOpen}
onClose={handleClose}
input={<BootstrapInput />}
label={label}
renderValue={s =>
multiple && (value as any[]).length !== 0 ? (
<Box className={classes.chips}>
{(s as string[]).map(selectedValue => {
const item = items.find(el => el.value === selectedValue);
return item ? (
<Chip
key={item?.value}
label={item?.label}
clickable
onDelete={handleDelete(selectedValue)}
className={classes.chip}
/>
) : (
false
);
})}
</Box>
) : (
<Typography>
{(value as any[]).length === 0
? placeholder || ''
: items.find(el => el.value === s)?.label}
</Typography>
)
}
IconComponent={() =>
!isOpen ? <ClosedDropdown /> : <OpenedDropdown />
}
MenuProps={{
anchorOrigin: {
vertical: 'bottom',
horizontal: 'left',
},
transformOrigin: {
vertical: 'top',
horizontal: 'left',
},
getContentAnchorEl: null,
}}
>
{placeholder && !multiple && (
<MenuItem value={[]}>{placeholder}</MenuItem>
)}
{native
? items &&
items.map(item => (
<option value={item.value} key={item.value}>
{item.label}
</option>
))
: items &&
items.map(item => (
<MenuItem key={item.value} value={item.value}>
{multiple && (
<Checkbox
color="primary"
checked={(value as any[]).includes(item.value) || false}
className={classes.checkbox}
/>
)}
{item.label}
</MenuItem>
))}
</Select>
</FormControl>
</Box>
);
}
@iocanel :-) :-) :-)
Problem fixed using FormData
with some changes
import React from "react";
import FormControl from "@material-ui/core/FormControl";
import {FieldExtensionComponentProps} from "@backstage/plugin-scaffolder-react";
import {Progress, Select, SelectItem} from "@backstage/core-components";
import useAsync from 'react-use/esm/useAsync';
// import useEffectOnce from 'react-use/lib/useEffectOnce';
/* Example returned by code.quarkus.io/api/streams
{
"javaCompatibility": {
"recommended": 17,
"versions": [
17,
21
]
},
"key": "io.quarkus.platform:3.8",
"lts": false,
"platformVersion": "3.8.2",
"quarkusCoreVersion": "3.8.2",
"recommended": true,
"status": "FINAL"
}
*/
export interface Version {
key: string;
quarkusCoreVersion: string;
platformVersion: string;
lts: boolean;
recommended: boolean;
javaCompatibility: javaCompatibility[];
status: string;
}
export interface javaCompatibility {
recommended: boolean;
versions: string[];
}
function userLabel(v: Version) {
const key = v.key.split(":")
if (v.recommended) {
return `${key[1]} (RECOMMENDED)`;
} else if (v.status !== "FINAL") {
return `${key[1]} (${v.status})`;
} else {
return key[1];
}
}
// TODO: Review the logic of this code against this backstage similar example to see if we can improve it:
// https://github.com/backstage/backstage/blob/master/plugins/scaffolder/src/components/fields/EntityTagsPicker/EntityTagsPicker.tsx
export const QuarkusVersionList = (props: FieldExtensionComponentProps<string>) => {
const {
onChange,
rawErrors,
required,
formData,
} = props;
const codeQuarkusUrl = 'https://code.quarkus.io';
const apiStreamsUrl = `${codeQuarkusUrl}/api/streams`;
const {loading, value} = useAsync(async () => {
const response = await fetch(apiStreamsUrl);
const newData = await response.json();
formData !== undefined ? formData : onChange(newData[0].key)
return newData;
});
const versionItems: SelectItem[] = value
? value?.map((i: Version) => ({label: userLabel(i), value: i.key}))
: [{label: 'Loading...', value: 'loading'}];
if (loading) {
return <Progress/>;
} else {
return (
<div>
<FormControl
margin="normal"
required={required}
error={rawErrors?.length > 0 && !formData}
>
<Select
native
label="Quarkus versions"
onChange={s => {
onChange(String(Array.isArray(s) ? s[0] : s))
}}
selected={formData}
items={versionItems}
/>
</FormControl>
</div>)
}
}
export default QuarkusVersionList;
TODO
Use
<Select/>
instead of<Autocomplete/>
for<QuarkusVersionList/>
. The following code work as RSJS FormData is getting the version selected.Note: When the user returns back to the screen of the
<Select/>
then the value they selected within the popup is not anymore selected.