2019-06-19 14:40:52 +00:00
|
|
|
import CircularProgress from "@material-ui/core/CircularProgress";
|
2019-08-09 11:14:35 +00:00
|
|
|
import IconButton from "@material-ui/core/IconButton";
|
2019-06-19 14:40:52 +00:00
|
|
|
import MenuItem from "@material-ui/core/MenuItem";
|
|
|
|
import Paper from "@material-ui/core/Paper";
|
|
|
|
import {
|
|
|
|
createStyles,
|
|
|
|
Theme,
|
|
|
|
withStyles,
|
|
|
|
WithStyles
|
|
|
|
} from "@material-ui/core/styles";
|
|
|
|
import TextField from "@material-ui/core/TextField";
|
2019-08-09 11:14:35 +00:00
|
|
|
import Typography from "@material-ui/core/Typography";
|
|
|
|
import CloseIcon from "@material-ui/icons/Close";
|
2019-06-19 14:40:52 +00:00
|
|
|
import Downshift, { ControllerStateAndHelpers } from "downshift";
|
2019-08-09 10:26:22 +00:00
|
|
|
import React from "react";
|
2019-08-26 21:54:03 +00:00
|
|
|
import { FormattedMessage } from "react-intl";
|
2019-08-09 11:14:35 +00:00
|
|
|
import { compareTwoStrings } from "string-similarity";
|
2019-06-19 14:40:52 +00:00
|
|
|
|
2019-08-09 11:14:35 +00:00
|
|
|
import { fade } from "@material-ui/core/styles/colorManipulator";
|
|
|
|
import Checkbox from "@saleor/components/Checkbox";
|
|
|
|
import Debounce, { DebounceProps } from "@saleor/components/Debounce";
|
|
|
|
import ArrowDropdownIcon from "@saleor/icons/ArrowDropdown";
|
|
|
|
import Hr from "../Hr";
|
2019-06-19 14:40:52 +00:00
|
|
|
|
2019-08-09 11:14:35 +00:00
|
|
|
export interface MultiAutocompleteChoiceType {
|
2019-06-19 14:40:52 +00:00
|
|
|
label: string;
|
|
|
|
value: string;
|
|
|
|
}
|
|
|
|
|
|
|
|
const styles = (theme: Theme) =>
|
|
|
|
createStyles({
|
2019-08-09 11:14:35 +00:00
|
|
|
checkbox: {
|
|
|
|
height: 24,
|
|
|
|
width: 20
|
|
|
|
},
|
2019-06-19 14:40:52 +00:00
|
|
|
chip: {
|
2019-08-09 11:14:35 +00:00
|
|
|
width: "100%"
|
|
|
|
},
|
|
|
|
chipClose: {
|
|
|
|
height: 32,
|
|
|
|
padding: 0,
|
|
|
|
width: 32
|
|
|
|
},
|
|
|
|
chipContainer: {
|
|
|
|
display: "flex",
|
|
|
|
flexDirection: "column",
|
|
|
|
marginTop: theme.spacing.unit
|
|
|
|
},
|
|
|
|
chipInner: {
|
|
|
|
"& svg": {
|
|
|
|
color: theme.palette.primary.contrastText
|
|
|
|
},
|
|
|
|
alignItems: "center",
|
2019-09-02 13:26:06 +00:00
|
|
|
background: fade(theme.palette.primary.main, 0.8),
|
|
|
|
borderRadius: 18,
|
2019-08-09 11:14:35 +00:00
|
|
|
color: theme.palette.primary.contrastText,
|
|
|
|
display: "flex",
|
|
|
|
justifyContent: "space-between",
|
|
|
|
margin: `${theme.spacing.unit}px 0`,
|
|
|
|
paddingLeft: theme.spacing.unit * 2,
|
|
|
|
paddingRight: theme.spacing.unit
|
|
|
|
},
|
|
|
|
chipLabel: {
|
2019-09-12 14:38:40 +00:00
|
|
|
color: theme.palette.primary.contrastText
|
2019-06-19 14:40:52 +00:00
|
|
|
},
|
|
|
|
container: {
|
|
|
|
flexGrow: 1,
|
|
|
|
position: "relative"
|
|
|
|
},
|
2019-08-09 11:14:35 +00:00
|
|
|
hr: {
|
|
|
|
margin: `${theme.spacing.unit}px 0`
|
|
|
|
},
|
|
|
|
menuItem: {
|
2019-09-16 02:14:57 +00:00
|
|
|
"&:focus": {
|
|
|
|
backgroundColor: [
|
|
|
|
theme.palette.background.default,
|
|
|
|
"!important"
|
|
|
|
] as any,
|
|
|
|
color: theme.palette.primary.main,
|
|
|
|
fontWeight: 400
|
|
|
|
},
|
|
|
|
"&:hover": {
|
|
|
|
backgroundColor: [
|
|
|
|
theme.palette.background.default,
|
|
|
|
"!important"
|
|
|
|
] as any,
|
|
|
|
color: theme.palette.primary.main,
|
|
|
|
fontWeight: 700
|
|
|
|
},
|
|
|
|
borderRadius: 4,
|
2019-08-09 11:14:35 +00:00
|
|
|
display: "grid",
|
|
|
|
gridColumnGap: theme.spacing.unit + "px",
|
2019-09-16 02:14:57 +00:00
|
|
|
gridTemplateColumns: "30px 1fr",
|
2019-08-09 11:14:35 +00:00
|
|
|
height: "auto",
|
2019-09-16 02:14:57 +00:00
|
|
|
padding: 0,
|
2019-08-09 11:14:35 +00:00
|
|
|
whiteSpace: "normal"
|
|
|
|
},
|
|
|
|
menuItemLabel: {
|
|
|
|
overflowWrap: "break-word"
|
|
|
|
},
|
2019-06-19 14:40:52 +00:00
|
|
|
paper: {
|
|
|
|
left: 0,
|
|
|
|
marginTop: theme.spacing.unit,
|
|
|
|
padding: theme.spacing.unit,
|
|
|
|
position: "absolute",
|
|
|
|
right: 0,
|
|
|
|
zIndex: 2
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2019-08-09 11:14:35 +00:00
|
|
|
export interface MultiAutocompleteSelectFieldProps {
|
|
|
|
allowCustomValues?: boolean;
|
|
|
|
displayValues: MultiAutocompleteChoiceType[];
|
2019-06-19 14:40:52 +00:00
|
|
|
name: string;
|
2019-08-09 11:14:35 +00:00
|
|
|
choices: MultiAutocompleteChoiceType[];
|
|
|
|
value: string[];
|
2019-06-19 14:40:52 +00:00
|
|
|
loading?: boolean;
|
|
|
|
placeholder?: string;
|
|
|
|
helperText?: string;
|
|
|
|
label?: string;
|
2019-08-09 11:14:35 +00:00
|
|
|
fetchChoices?: (value: string) => void;
|
|
|
|
onChange: (event: React.ChangeEvent<any>) => void;
|
2019-06-19 14:40:52 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
const DebounceAutocomplete: React.ComponentType<
|
|
|
|
DebounceProps<string>
|
|
|
|
> = Debounce;
|
|
|
|
|
2019-08-09 11:14:35 +00:00
|
|
|
export const MultiAutocompleteSelectFieldComponent = withStyles(styles, {
|
2019-06-19 14:40:52 +00:00
|
|
|
name: "MultiAutocompleteSelectField"
|
|
|
|
})(
|
|
|
|
({
|
2019-08-09 11:14:35 +00:00
|
|
|
allowCustomValues,
|
2019-06-19 14:40:52 +00:00
|
|
|
choices,
|
|
|
|
classes,
|
2019-08-09 11:14:35 +00:00
|
|
|
displayValues,
|
2019-06-19 14:40:52 +00:00
|
|
|
helperText,
|
|
|
|
label,
|
|
|
|
loading,
|
|
|
|
name,
|
|
|
|
placeholder,
|
|
|
|
value,
|
|
|
|
fetchChoices,
|
2019-08-27 13:29:00 +00:00
|
|
|
onChange,
|
|
|
|
...props
|
2019-08-09 11:14:35 +00:00
|
|
|
}: MultiAutocompleteSelectFieldProps & WithStyles<typeof styles>) => {
|
2019-06-19 14:40:52 +00:00
|
|
|
const handleSelect = (
|
2019-08-09 11:14:35 +00:00
|
|
|
item: string,
|
|
|
|
downshiftOpts?: ControllerStateAndHelpers
|
2019-06-19 14:40:52 +00:00
|
|
|
) => {
|
2019-08-09 11:14:35 +00:00
|
|
|
if (downshiftOpts) {
|
|
|
|
downshiftOpts.reset({ inputValue: "" });
|
|
|
|
}
|
|
|
|
onChange({
|
|
|
|
target: { name, value: item }
|
|
|
|
} as any);
|
2019-06-19 14:40:52 +00:00
|
|
|
};
|
2019-08-09 11:14:35 +00:00
|
|
|
const suggestions = choices.filter(choice => !value.includes(choice.value));
|
2019-06-19 14:40:52 +00:00
|
|
|
|
2019-08-09 11:14:35 +00:00
|
|
|
return (
|
|
|
|
<>
|
|
|
|
<Downshift
|
|
|
|
onInputValueChange={fetchChoices}
|
|
|
|
onSelect={handleSelect}
|
|
|
|
itemToString={() => ""}
|
|
|
|
>
|
|
|
|
{({
|
|
|
|
getInputProps,
|
|
|
|
getItemProps,
|
|
|
|
isOpen,
|
|
|
|
toggleMenu,
|
|
|
|
highlightedIndex,
|
|
|
|
inputValue
|
|
|
|
}) => (
|
2019-08-27 13:29:00 +00:00
|
|
|
<div className={classes.container} {...props}>
|
2019-08-09 11:14:35 +00:00
|
|
|
<TextField
|
|
|
|
InputProps={{
|
|
|
|
...getInputProps({
|
|
|
|
placeholder
|
|
|
|
}),
|
|
|
|
endAdornment: (
|
|
|
|
<div>
|
|
|
|
{loading ? (
|
|
|
|
<CircularProgress size={20} />
|
|
|
|
) : (
|
|
|
|
<ArrowDropdownIcon onClick={toggleMenu} />
|
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
),
|
|
|
|
id: undefined,
|
|
|
|
onClick: toggleMenu
|
|
|
|
}}
|
|
|
|
helperText={helperText}
|
|
|
|
label={label}
|
|
|
|
fullWidth={true}
|
|
|
|
/>
|
|
|
|
{isOpen && (!!inputValue || !!choices.length) && (
|
|
|
|
<Paper className={classes.paper} square>
|
|
|
|
{choices.length > 0 ||
|
|
|
|
displayValues.length > 0 ||
|
|
|
|
allowCustomValues ? (
|
|
|
|
<>
|
|
|
|
{displayValues.map(value => (
|
|
|
|
<MenuItem
|
|
|
|
className={classes.menuItem}
|
|
|
|
key={value.value}
|
|
|
|
selected={true}
|
|
|
|
component="div"
|
|
|
|
{...getItemProps({
|
|
|
|
item: value.value
|
|
|
|
})}
|
2019-08-27 13:29:00 +00:00
|
|
|
data-tc="multiautocomplete-select-option"
|
2019-08-09 11:14:35 +00:00
|
|
|
>
|
|
|
|
<Checkbox
|
|
|
|
className={classes.checkbox}
|
|
|
|
checked={true}
|
|
|
|
disableRipple
|
|
|
|
/>
|
|
|
|
<span className={classes.menuItemLabel}>
|
|
|
|
{value.label}
|
|
|
|
</span>
|
|
|
|
</MenuItem>
|
|
|
|
))}
|
|
|
|
{displayValues.length > 0 && suggestions.length > 0 && (
|
|
|
|
<Hr className={classes.hr} />
|
|
|
|
)}
|
|
|
|
{suggestions.map((suggestion, index) => (
|
|
|
|
<MenuItem
|
|
|
|
className={classes.menuItem}
|
|
|
|
key={suggestion.value}
|
|
|
|
selected={highlightedIndex === index + value.length}
|
|
|
|
component="div"
|
|
|
|
{...getItemProps({
|
|
|
|
item: suggestion.value
|
|
|
|
})}
|
2019-08-27 13:29:00 +00:00
|
|
|
data-tc="multiautocomplete-select-option"
|
2019-08-09 11:14:35 +00:00
|
|
|
>
|
|
|
|
<Checkbox
|
|
|
|
checked={value.includes(suggestion.value)}
|
|
|
|
className={classes.checkbox}
|
|
|
|
disableRipple
|
|
|
|
/>
|
|
|
|
<span className={classes.menuItemLabel}>
|
|
|
|
{suggestion.label}
|
|
|
|
</span>
|
|
|
|
</MenuItem>
|
|
|
|
))}
|
|
|
|
{allowCustomValues &&
|
|
|
|
inputValue &&
|
|
|
|
!choices.find(
|
|
|
|
choice =>
|
|
|
|
choice.label.toLowerCase() ===
|
|
|
|
inputValue.toLowerCase()
|
|
|
|
) && (
|
|
|
|
<MenuItem
|
|
|
|
className={classes.menuItem}
|
|
|
|
key={"customValue"}
|
|
|
|
component="div"
|
|
|
|
{...getItemProps({
|
|
|
|
item: inputValue
|
|
|
|
})}
|
2019-08-27 13:29:00 +00:00
|
|
|
data-tc="multiautocomplete-select-option"
|
2019-08-09 11:14:35 +00:00
|
|
|
>
|
|
|
|
<span className={classes.menuItemLabel}>
|
2019-08-26 21:54:03 +00:00
|
|
|
<FormattedMessage
|
|
|
|
defaultMessage="Add new value: {value}"
|
|
|
|
description="add custom option to select input"
|
|
|
|
values={{
|
|
|
|
value: inputValue
|
|
|
|
}}
|
|
|
|
/>
|
2019-08-09 11:14:35 +00:00
|
|
|
</span>
|
|
|
|
</MenuItem>
|
|
|
|
)}
|
|
|
|
</>
|
|
|
|
) : (
|
|
|
|
!loading && (
|
2019-08-27 13:29:00 +00:00
|
|
|
<MenuItem
|
|
|
|
disabled={true}
|
|
|
|
component="div"
|
|
|
|
data-tc="multiautocomplete-select-no-options"
|
|
|
|
>
|
2019-08-26 21:54:03 +00:00
|
|
|
<FormattedMessage defaultMessage="No results found" />
|
2019-08-09 11:14:35 +00:00
|
|
|
</MenuItem>
|
|
|
|
)
|
|
|
|
)}
|
|
|
|
</Paper>
|
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
</Downshift>
|
|
|
|
<div className={classes.chipContainer}>
|
|
|
|
{displayValues.map(value => (
|
|
|
|
<div className={classes.chip} key={value.value}>
|
|
|
|
<div className={classes.chipInner}>
|
2019-09-12 14:38:40 +00:00
|
|
|
<Typography className={classes.chipLabel}>
|
2019-08-09 11:14:35 +00:00
|
|
|
{value.label}
|
|
|
|
</Typography>
|
|
|
|
<IconButton
|
|
|
|
className={classes.chipClose}
|
|
|
|
onClick={() => handleSelect(value.value)}
|
|
|
|
>
|
|
|
|
<CloseIcon fontSize="small" />
|
|
|
|
</IconButton>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
))}
|
|
|
|
</div>
|
|
|
|
</>
|
2019-06-19 14:40:52 +00:00
|
|
|
);
|
2019-08-09 11:14:35 +00:00
|
|
|
}
|
|
|
|
);
|
|
|
|
const MultiAutocompleteSelectField: React.FC<
|
|
|
|
MultiAutocompleteSelectFieldProps
|
|
|
|
> = ({ choices, fetchChoices, ...props }) => {
|
|
|
|
const [query, setQuery] = React.useState("");
|
|
|
|
if (fetchChoices) {
|
2019-06-19 14:40:52 +00:00
|
|
|
return (
|
|
|
|
<DebounceAutocomplete debounceFn={fetchChoices}>
|
2019-08-09 11:14:35 +00:00
|
|
|
{debounceFn => (
|
|
|
|
<MultiAutocompleteSelectFieldComponent
|
|
|
|
choices={choices}
|
|
|
|
{...props}
|
|
|
|
fetchChoices={debounceFn}
|
|
|
|
/>
|
2019-06-19 14:40:52 +00:00
|
|
|
)}
|
|
|
|
</DebounceAutocomplete>
|
|
|
|
);
|
|
|
|
}
|
2019-08-09 11:14:35 +00:00
|
|
|
|
|
|
|
const sortedChoices = choices.sort((a, b) => {
|
|
|
|
const ratingA = compareTwoStrings(query, a.label);
|
|
|
|
const ratingB = compareTwoStrings(query, b.label);
|
|
|
|
if (ratingA > ratingB) {
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
if (ratingA < ratingB) {
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
return 0;
|
|
|
|
});
|
|
|
|
|
|
|
|
return (
|
|
|
|
<MultiAutocompleteSelectFieldComponent
|
|
|
|
fetchChoices={q => setQuery(q || "")}
|
|
|
|
choices={sortedChoices}
|
|
|
|
{...props}
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
};
|
2019-06-19 14:40:52 +00:00
|
|
|
MultiAutocompleteSelectField.displayName = "MultiAutocompleteSelectField";
|
|
|
|
export default MultiAutocompleteSelectField;
|