Fix attribute empty option

This commit is contained in:
dominik-zeglen 2020-10-12 12:45:07 +02:00
parent c60d5bff56
commit c15aaa833b
8 changed files with 134 additions and 23 deletions

View file

@ -4,8 +4,13 @@ import TextField from "@material-ui/core/TextField";
import useStateFromProps from "@saleor/hooks/useStateFromProps"; import useStateFromProps from "@saleor/hooks/useStateFromProps";
import { FetchMoreProps } from "@saleor/types"; import { FetchMoreProps } from "@saleor/types";
import classNames from "classnames"; import classNames from "classnames";
import Downshift from "downshift"; import Downshift, {
ControllerStateAndHelpers,
DownshiftState,
StateChangeOptions
} from "downshift";
import { filter } from "fuzzaldrin"; import { filter } from "fuzzaldrin";
import { names } from "keycode";
import React from "react"; import React from "react";
import ArrowDropdownIcon from "../../icons/ArrowDropdown"; import ArrowDropdownIcon from "../../icons/ArrowDropdown";
@ -76,20 +81,27 @@ const SingleAutocompleteSelectFieldComponent: React.FC<SingleAutocompleteSelectF
const [prevDisplayValue] = useStateFromProps(displayValue); const [prevDisplayValue] = useStateFromProps(displayValue);
const handleChange = item => const handleChange = (
item: string,
stateAndHelpers: ControllerStateAndHelpers
) => {
onChange({ onChange({
target: { target: {
name, name,
value: item value: item
} }
} as any); } as any);
stateAndHelpers.reset({
inputValue: item
});
};
return ( return (
<DebounceAutocomplete debounceFn={fetchChoices}> <DebounceAutocomplete debounceFn={fetchChoices}>
{debounceFn => ( {debounceFn => (
<Downshift <Downshift
defaultInputValue={displayValue} defaultInputValue={displayValue}
itemToString={() => displayValue} itemToString={() => displayValue || ""}
onInputValueChange={value => debounceFn(value)} onInputValueChange={value => debounceFn(value)}
onSelect={handleChange} onSelect={handleChange}
selectedItem={value} selectedItem={value}
@ -190,6 +202,7 @@ const SingleAutocompleteSelectField: React.FC<SingleAutocompleteSelectFieldProps
...rest ...rest
}) => { }) => {
const [query, setQuery] = React.useState(""); const [query, setQuery] = React.useState("");
if (fetchChoices) { if (fetchChoices) {
return ( return (
<DebounceAutocomplete debounceFn={fetchChoices}> <DebounceAutocomplete debounceFn={fetchChoices}>

View file

@ -35,7 +35,7 @@ export interface SingleAutocompleteSelectFieldContentProps
choices: SingleAutocompleteChoiceType[]; choices: SingleAutocompleteChoiceType[];
displayCustomValue: boolean; displayCustomValue: boolean;
emptyOption: boolean; emptyOption: boolean;
getItemProps: (options: GetItemPropsOptions) => void; getItemProps: (options: GetItemPropsOptions) => any;
highlightedIndex: number; highlightedIndex: number;
inputValue: string; inputValue: string;
isCustomValueSelected: boolean; isCustomValueSelected: boolean;
@ -164,9 +164,11 @@ const SingleAutocompleteSelectFieldContent: React.FC<SingleAutocompleteSelectFie
React.useEffect(() => { React.useEffect(() => {
setSlice(sliceSize); setSlice(sliceSize);
if (anchor.current?.scrollTo) {
anchor.current.scrollTo({ anchor.current.scrollTo({
top: 0 top: 0
}); });
}
}, [choices?.length]); }, [choices?.length]);
React.useEffect(() => { React.useEffect(() => {
@ -175,6 +177,10 @@ const SingleAutocompleteSelectFieldContent: React.FC<SingleAutocompleteSelectFie
} }
}, [loading]); }, [loading]);
const emptyOptionProps = getItemProps({
item: ""
});
return ( return (
<Paper className={classes.root}> <Paper className={classes.root}>
<div <div
@ -184,20 +190,19 @@ const SingleAutocompleteSelectFieldContent: React.FC<SingleAutocompleteSelectFie
> >
{choices.length > 0 || displayCustomValue ? ( {choices.length > 0 || displayCustomValue ? (
<> <>
{emptyOption && ( {
<MenuItem <MenuItem
className={classes.menuItem} className={classes.menuItem}
component="div" component="div"
{...getItemProps({
item: ""
})}
data-test="singleautocomplete-select-option" data-test="singleautocomplete-select-option"
data-test-type="empty"
{...emptyOptionProps}
> >
<Typography color="textSecondary"> <Typography color="textSecondary">
<FormattedMessage defaultMessage="None" /> <FormattedMessage defaultMessage="None" />
</Typography> </Typography>
</MenuItem> </MenuItem>
)} }
{add && ( {add && (
<MenuItem <MenuItem
className={classes.menuItem} className={classes.menuItem}
@ -206,6 +211,7 @@ const SingleAutocompleteSelectFieldContent: React.FC<SingleAutocompleteSelectFie
item: inputValue item: inputValue
})} })}
data-test="singleautocomplete-select-option-add" data-test="singleautocomplete-select-option-add"
data-test-type="add"
onClick={add.onClick} onClick={add.onClick}
> >
<Add color="primary" className={classes.add} /> <Add color="primary" className={classes.add} />
@ -222,6 +228,7 @@ const SingleAutocompleteSelectFieldContent: React.FC<SingleAutocompleteSelectFie
item: inputValue item: inputValue
})} })}
data-test="singleautocomplete-select-option" data-test="singleautocomplete-select-option"
data-test-type="custom"
> >
<FormattedMessage <FormattedMessage
defaultMessage="Add new value: {value}" defaultMessage="Add new value: {value}"
@ -254,6 +261,8 @@ const SingleAutocompleteSelectFieldContent: React.FC<SingleAutocompleteSelectFie
item: suggestion.value item: suggestion.value
})} })}
data-test="singleautocomplete-select-option" data-test="singleautocomplete-select-option"
data-test-value={suggestion.value}
data-test-type="option"
> >
{suggestion.label} {suggestion.label}
</MenuItem> </MenuItem>

View file

@ -15,7 +15,6 @@ import SingleAutocompleteSelectField, {
} from "@saleor/components/SingleAutocompleteSelectField"; } from "@saleor/components/SingleAutocompleteSelectField";
import { ProductErrorWithAttributesFragment } from "@saleor/fragments/types/ProductErrorWithAttributesFragment"; import { ProductErrorWithAttributesFragment } from "@saleor/fragments/types/ProductErrorWithAttributesFragment";
import { FormsetAtomicData, FormsetChange } from "@saleor/hooks/useFormset"; import { FormsetAtomicData, FormsetChange } from "@saleor/hooks/useFormset";
import { maybe } from "@saleor/misc";
import { ProductDetails_product_attributes_attribute_values } from "@saleor/products/types/ProductDetails"; import { ProductDetails_product_attributes_attribute_values } from "@saleor/products/types/ProductDetails";
import { AttributeInputTypeEnum } from "@saleor/types/globalTypes"; import { AttributeInputTypeEnum } from "@saleor/types/globalTypes";
import { getProductErrorMessage } from "@saleor/utils/errors"; import { getProductErrorMessage } from "@saleor/utils/errors";
@ -194,13 +193,11 @@ const ProductAttributes: React.FC<ProductAttributesProps> = ({
<SingleAutocompleteSelectField <SingleAutocompleteSelectField
choices={getSingleChoices(attribute.data.values)} choices={getSingleChoices(attribute.data.values)}
disabled={disabled} disabled={disabled}
displayValue={maybe( displayValue={
() =>
attribute.data.values.find( attribute.data.values.find(
value => value.slug === attribute.value[0] value => value.slug === attribute.value[0]
).name, )?.name || ""
attribute.value[0] }
)}
emptyOption={!attribute.data.isRequired} emptyOption={!attribute.data.isRequired}
error={!!error} error={!!error}
helperText={getProductErrorMessage(error, intl)} helperText={getProductErrorMessage(error, intl)}

View file

@ -0,0 +1,92 @@
import placeholderImage from "@assets/images/placeholder255x255.png";
import { collections } from "@saleor/collections/fixtures";
import { fetchMoreProps, listActionsProps } from "@saleor/fixtures";
import { product as productFixture } from "@saleor/products/fixtures";
import { taxTypes } from "@saleor/storybook/stories/taxes/fixtures";
import { warehouseList } from "@saleor/warehouses/fixtures";
import Wrapper from "@test/wrapper";
import { configure, mount } from "enzyme";
import React from "react";
import ProductUpdatePage, { ProductUpdatePageProps } from "./ProductUpdatePage";
const product = productFixture(placeholderImage);
import Adapter from "enzyme-adapter-react-16";
configure({ adapter: new Adapter() });
const onSubmit = jest.fn();
const props: ProductUpdatePageProps = {
...listActionsProps,
categories: [product.category],
collections,
defaultWeightUnit: "kg",
disabled: false,
errors: [],
fetchCategories: () => undefined,
fetchCollections: () => undefined,
fetchMoreCategories: fetchMoreProps,
fetchMoreCollections: fetchMoreProps,
header: product.name,
images: product.images,
onBack: () => undefined,
onDelete: () => undefined,
onImageDelete: () => undefined,
onImageUpload: () => undefined,
onSetDefaultVariant: () => undefined,
onSubmit,
onVariantAdd: () => undefined,
onVariantReorder: () => undefined,
onVariantShow: () => undefined,
onVariantsAdd: () => undefined,
onWarehouseConfigure: () => undefined,
placeholderImage,
product,
saveButtonBarState: "default",
taxTypes,
variants: product.variants,
warehouses: warehouseList
};
const selectors = {
dropdown: `[data-test="autocomplete-dropdown"]`,
empty: `[data-test-type="empty"]`,
input: `[data-test="product-attribute-value"] input`
};
describe("Product details page", () => {
it("can select empty option on attribute", () => {
const component = mount(
<Wrapper>
<ProductUpdatePage {...props} />
</Wrapper>
);
expect(component.find(selectors.dropdown).exists()).toBeFalsy();
component
.find(selectors.input)
.first()
.simulate("click");
expect(component.find(selectors.dropdown).exists()).toBeTruthy();
expect(component.find(selectors.empty).exists());
component
.find(selectors.empty)
.first()
.simulate("click");
expect(
component
.find(selectors.input)
.first()
.prop("value")
).toEqual("");
component
.find("form")
.first()
.simulate("submit");
expect(onSubmit.mock.calls[0][0].attributes[0].value.length).toEqual(0);
});
});

View file

@ -98,7 +98,6 @@ const ProductVariantAttributes: React.FC<ProductVariantAttributesProps> = ({
attribute.value, attribute.value,
attributes attributes
)} )}
emptyOption
error={!!error} error={!!error}
helperText={getProductVariantAttributeErrorMessage( helperText={getProductVariantAttributeErrorMessage(
error, error,

View file

@ -24,7 +24,7 @@ export const product: (
inputType: AttributeInputTypeEnum.DROPDOWN, inputType: AttributeInputTypeEnum.DROPDOWN,
name: "Borders", name: "Borders",
slug: "Borders", slug: "Borders",
valueRequired: true, valueRequired: false,
values: [ values: [
{ {
__typename: "AttributeValue", __typename: "AttributeValue",

View file

@ -42,7 +42,7 @@ export function createAttributeChangeHandler(
]); ]);
triggerChange(); triggerChange();
changeAttributeData(attributeId, [value]); changeAttributeData(attributeId, value === "" ? [] : [value]);
}; };
} }

View file

@ -92,6 +92,7 @@ storiesOf("Views / Products / Product edit", module)
{...props} {...props}
product={{ product={{
...product, ...product,
productType: { productType: {
...product.productType, ...product.productType,
hasVariants: false hasVariants: false