Fix attribute empty option
This commit is contained in:
parent
c60d5bff56
commit
c15aaa833b
8 changed files with 134 additions and 23 deletions
|
@ -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}>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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)}
|
||||||
|
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
|
@ -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,
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -42,7 +42,7 @@ export function createAttributeChangeHandler(
|
||||||
]);
|
]);
|
||||||
|
|
||||||
triggerChange();
|
triggerChange();
|
||||||
changeAttributeData(attributeId, [value]);
|
changeAttributeData(attributeId, value === "" ? [] : [value]);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue