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 { 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}>
|
||||
|
|
|
@ -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);
|
||||
anchor.current.scrollTo({
|
||||
top: 0
|
||||
});
|
||||
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>
|
||||
|
|
|
@ -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(
|
||||
() =>
|
||||
attribute.data.values.find(
|
||||
value => value.slug === attribute.value[0]
|
||||
).name,
|
||||
attribute.value[0]
|
||||
)}
|
||||
displayValue={
|
||||
attribute.data.values.find(
|
||||
value => value.slug === attribute.value[0]
|
||||
)?.name || ""
|
||||
}
|
||||
emptyOption={!attribute.data.isRequired}
|
||||
error={!!error}
|
||||
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,
|
||||
attributes
|
||||
)}
|
||||
emptyOption
|
||||
error={!!error}
|
||||
helperText={getProductVariantAttributeErrorMessage(
|
||||
error,
|
||||
|
|
|
@ -24,7 +24,7 @@ export const product: (
|
|||
inputType: AttributeInputTypeEnum.DROPDOWN,
|
||||
name: "Borders",
|
||||
slug: "Borders",
|
||||
valueRequired: true,
|
||||
valueRequired: false,
|
||||
values: [
|
||||
{
|
||||
__typename: "AttributeValue",
|
||||
|
|
|
@ -42,7 +42,7 @@ export function createAttributeChangeHandler(
|
|||
]);
|
||||
|
||||
triggerChange();
|
||||
changeAttributeData(attributeId, [value]);
|
||||
changeAttributeData(attributeId, value === "" ? [] : [value]);
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -92,6 +92,7 @@ storiesOf("Views / Products / Product edit", module)
|
|||
{...props}
|
||||
product={{
|
||||
...product,
|
||||
|
||||
productType: {
|
||||
...product.productType,
|
||||
hasVariants: false
|
||||
|
|
Loading…
Reference in a new issue