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

View file

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

View file

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

View file

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

View file

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

View file

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