Merge pull request #212 from mirumee/fix/autocomplete-ux

Improve autocomplete ux
This commit is contained in:
Marcin Gębala 2019-10-17 09:18:51 +02:00 committed by GitHub
commit 3936e68a36
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
67 changed files with 3226 additions and 1455 deletions

View file

@ -37,3 +37,4 @@ All notable, unreleased changes to this project will be documented in this file.
- Do not send customer invitation email - #211 by @dominik-zeglen - Do not send customer invitation email - #211 by @dominik-zeglen
- Send address update mutation only once - #210 by @dominik-zeglen - Send address update mutation only once - #210 by @dominik-zeglen
- Update sale details design - #207 by @dominik-zeglen - Update sale details design - #207 by @dominik-zeglen
- Improve autocomplete UX - #212 by @dominik-zeglen

View file

@ -0,0 +1,3 @@
<svg width="15" height="10" viewBox="0 0 15 10" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.23278 6.21084L13 -6.40628e-08L14.4656 1.3609L7.23278 9.15006L-1.15036e-05 1.3609L1.46558 -5.68248e-07L7.23278 6.21084Z" fill="#06847B"/>
</svg>

After

Width:  |  Height:  |  Size: 292 B

View file

@ -1,6 +1,6 @@
msgid "" msgid ""
msgstr "" msgstr ""
"POT-Creation-Date: 2019-10-15T15:56:00.137Z\n" "POT-Creation-Date: 2019-10-16T15:55:00.250Z\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
@ -267,15 +267,11 @@ msgctxt "description"
msgid "Add new menu item to begin creating menu" msgid "Add new menu item to begin creating menu"
msgstr "" msgstr ""
#: build/locale/src/components/MultiAutocompleteSelectField/MultiAutocompleteSelectField.json #: build/locale/src/components/MultiAutocompleteSelectField/MultiAutocompleteSelectFieldContent.json
#. [src.components.MultiAutocompleteSelectField.1477537381] - add custom option to select input #. [src.components.MultiAutocompleteSelectField.1477537381] - add custom select input option
#. defaultMessage is: #. defaultMessage is:
#. Add new value: {value} #. Add new value: {value}
msgctxt "add custom option to select input" #: build/locale/src/components/SingleAutocompleteSelectField/SingleAutocompleteSelectFieldContent.json
msgid "Add new value: {value}"
msgstr ""
#: build/locale/src/components/SingleAutocompleteSelectField/SingleAutocompleteSelectField.json
#. [src.components.SingleAutocompleteSelectField.1477537381] - add custom select input option #. [src.components.SingleAutocompleteSelectField.1477537381] - add custom select input option
#. defaultMessage is: #. defaultMessage is:
#. Add new value: {value} #. Add new value: {value}
@ -3411,14 +3407,6 @@ msgctxt "dialog header"
msgid "Edit Billing Address" msgid "Edit Billing Address"
msgstr "" msgstr ""
#: build/locale/src/orders/components/OrderCustomerEditDialog/OrderCustomerEditDialog.json
#. [src.orders.components.OrderCustomerEditDialog.1549172886] - dialog header
#. defaultMessage is:
#. Edit Customer Details
msgctxt "dialog header"
msgid "Edit Customer Details"
msgstr ""
#: build/locale/src/navigation/components/MenuItemDialog/MenuItemDialog.json #: build/locale/src/navigation/components/MenuItemDialog/MenuItemDialog.json
#. [menuItemDialogEditItem] - edit menu item, header #. [menuItemDialogEditItem] - edit menu item, header
#. defaultMessage is: #. defaultMessage is:
@ -4863,7 +4851,7 @@ msgctxt "description"
msgid "No results" msgid "No results"
msgstr "" msgstr ""
#: build/locale/src/components/MultiAutocompleteSelectField/MultiAutocompleteSelectField.json #: build/locale/src/components/MultiAutocompleteSelectField/MultiAutocompleteSelectFieldContent.json
#. [src.components.MultiAutocompleteSelectField.4205644805] #. [src.components.MultiAutocompleteSelectField.4205644805]
#. defaultMessage is: #. defaultMessage is:
#. No results found #. No results found
@ -4875,7 +4863,7 @@ msgstr ""
#. [src.components.RadioGroupField.4205644805] #. [src.components.RadioGroupField.4205644805]
#. defaultMessage is: #. defaultMessage is:
#. No results found #. No results found
#: build/locale/src/components/SingleAutocompleteSelectField/SingleAutocompleteSelectField.json #: build/locale/src/components/SingleAutocompleteSelectField/SingleAutocompleteSelectFieldContent.json
#. [src.components.SingleAutocompleteSelectField.4205644805] #. [src.components.SingleAutocompleteSelectField.4205644805]
#. defaultMessage is: #. defaultMessage is:
#. No results found #. No results found
@ -5047,7 +5035,7 @@ msgctxt "description"
msgid "No. of Products" msgid "No. of Products"
msgstr "" msgstr ""
#: build/locale/src/components/SingleAutocompleteSelectField/SingleAutocompleteSelectField.json #: build/locale/src/components/SingleAutocompleteSelectField/SingleAutocompleteSelectFieldContent.json
#. [src.components.SingleAutocompleteSelectField.3069107721] #. [src.components.SingleAutocompleteSelectField.3069107721]
#. defaultMessage is: #. defaultMessage is:
#. None #. None

View file

@ -62,7 +62,6 @@
"react-sortable-tree": "^2.6.2", "react-sortable-tree": "^2.6.2",
"react-svg": "^2.2.11", "react-svg": "^2.2.11",
"slugify": "^1.3.4", "slugify": "^1.3.4",
"string-similarity": "^1.2.2",
"typescript": "^3.5.3", "typescript": "^3.5.3",
"url-join": "^4.0.1", "url-join": "^4.0.1",
"use-react-router": "^1.0.7" "use-react-router": "^1.0.7"
@ -99,7 +98,6 @@
"@types/react-test-renderer": "^16.8.2", "@types/react-test-renderer": "^16.8.2",
"@types/storybook__addon-storyshots": "^3.4.9", "@types/storybook__addon-storyshots": "^3.4.9",
"@types/storybook__react": "^4.0.2", "@types/storybook__react": "^4.0.2",
"@types/string-similarity": "^1.2.1",
"@types/url-join": "^0.8.3", "@types/url-join": "^0.8.3",
"@types/webappsec-credential-management": "^0.5.1", "@types/webappsec-credential-management": "^0.5.1",
"babel-core": "^7.0.0-bridge.0", "babel-core": "^7.0.0-bridge.0",

View file

@ -299,7 +299,7 @@ export const CollectionDetails: React.StatelessComponent<
}) })
} }
products={maybe(() => products={maybe(() =>
result.data.products.edges result.data.search.edges
.map(edge => edge.node) .map(edge => edge.node)
.filter(suggestedProduct => suggestedProduct.id) .filter(suggestedProduct => suggestedProduct.id)
)} )}

View file

@ -19,11 +19,11 @@ import ConfirmButton, {
import FormSpacer from "@saleor/components/FormSpacer"; import FormSpacer from "@saleor/components/FormSpacer";
import useSearchQuery from "@saleor/hooks/useSearchQuery"; import useSearchQuery from "@saleor/hooks/useSearchQuery";
import { buttonMessages } from "@saleor/intl"; import { buttonMessages } from "@saleor/intl";
import { SearchCategories_categories_edges_node } from "../../containers/SearchCategories/types/SearchCategories"; import { SearchCategories_search_edges_node } from "../../containers/SearchCategories/types/SearchCategories";
import Checkbox from "../Checkbox"; import Checkbox from "../Checkbox";
export interface FormData { export interface FormData {
categories: SearchCategories_categories_edges_node[]; categories: SearchCategories_search_edges_node[];
query: string; query: string;
} }
@ -45,22 +45,20 @@ const styles = createStyles({
}); });
interface AssignCategoriesDialogProps extends WithStyles<typeof styles> { interface AssignCategoriesDialogProps extends WithStyles<typeof styles> {
categories: SearchCategories_categories_edges_node[]; categories: SearchCategories_search_edges_node[];
confirmButtonState: ConfirmButtonTransitionState; confirmButtonState: ConfirmButtonTransitionState;
open: boolean; open: boolean;
loading: boolean; loading: boolean;
onClose: () => void; onClose: () => void;
onFetch: (value: string) => void; onFetch: (value: string) => void;
onSubmit: (data: SearchCategories_categories_edges_node[]) => void; onSubmit: (data: SearchCategories_search_edges_node[]) => void;
} }
function handleCategoryAssign( function handleCategoryAssign(
product: SearchCategories_categories_edges_node, product: SearchCategories_search_edges_node,
isSelected: boolean, isSelected: boolean,
selectedCategories: SearchCategories_categories_edges_node[], selectedCategories: SearchCategories_search_edges_node[],
setSelectedCategories: ( setSelectedCategories: (data: SearchCategories_search_edges_node[]) => void
data: SearchCategories_categories_edges_node[]
) => void
) { ) {
if (isSelected) { if (isSelected) {
setSelectedCategories( setSelectedCategories(
@ -89,7 +87,7 @@ const AssignCategoriesDialog = withStyles(styles, {
const intl = useIntl(); const intl = useIntl();
const [query, onQueryChange] = useSearchQuery(onFetch); const [query, onQueryChange] = useSearchQuery(onFetch);
const [selectedCategories, setSelectedCategories] = React.useState< const [selectedCategories, setSelectedCategories] = React.useState<
SearchCategories_categories_edges_node[] SearchCategories_search_edges_node[]
>([]); >([]);
const handleSubmit = () => onSubmit(selectedCategories); const handleSubmit = () => onSubmit(selectedCategories);

View file

@ -15,7 +15,7 @@ import { FormattedMessage, useIntl } from "react-intl";
import useSearchQuery from "@saleor/hooks/useSearchQuery"; import useSearchQuery from "@saleor/hooks/useSearchQuery";
import { buttonMessages } from "@saleor/intl"; import { buttonMessages } from "@saleor/intl";
import { SearchCollections_collections_edges_node } from "../../containers/SearchCollections/types/SearchCollections"; import { SearchCollections_search_edges_node } from "../../containers/SearchCollections/types/SearchCollections";
import Checkbox from "../Checkbox"; import Checkbox from "../Checkbox";
import ConfirmButton, { import ConfirmButton, {
ConfirmButtonTransitionState ConfirmButtonTransitionState
@ -23,7 +23,7 @@ import ConfirmButton, {
import FormSpacer from "../FormSpacer"; import FormSpacer from "../FormSpacer";
export interface FormData { export interface FormData {
collections: SearchCollections_collections_edges_node[]; collections: SearchCollections_search_edges_node[];
query: string; query: string;
} }
@ -45,22 +45,20 @@ const styles = createStyles({
}); });
interface AssignCollectionDialogProps extends WithStyles<typeof styles> { interface AssignCollectionDialogProps extends WithStyles<typeof styles> {
collections: SearchCollections_collections_edges_node[]; collections: SearchCollections_search_edges_node[];
confirmButtonState: ConfirmButtonTransitionState; confirmButtonState: ConfirmButtonTransitionState;
open: boolean; open: boolean;
loading: boolean; loading: boolean;
onClose: () => void; onClose: () => void;
onFetch: (value: string) => void; onFetch: (value: string) => void;
onSubmit: (data: SearchCollections_collections_edges_node[]) => void; onSubmit: (data: SearchCollections_search_edges_node[]) => void;
} }
function handleCollectionAssign( function handleCollectionAssign(
product: SearchCollections_collections_edges_node, product: SearchCollections_search_edges_node,
isSelected: boolean, isSelected: boolean,
selectedCollections: SearchCollections_collections_edges_node[], selectedCollections: SearchCollections_search_edges_node[],
setSelectedCollections: ( setSelectedCollections: (data: SearchCollections_search_edges_node[]) => void
data: SearchCollections_collections_edges_node[]
) => void
) { ) {
if (isSelected) { if (isSelected) {
setSelectedCollections( setSelectedCollections(
@ -89,7 +87,7 @@ const AssignCollectionDialog = withStyles(styles, {
const intl = useIntl(); const intl = useIntl();
const [query, onQueryChange] = useSearchQuery(onFetch); const [query, onQueryChange] = useSearchQuery(onFetch);
const [selectedCollections, setSelectedCollections] = React.useState< const [selectedCollections, setSelectedCollections] = React.useState<
SearchCollections_collections_edges_node[] SearchCollections_search_edges_node[]
>([]); >([]);
const handleSubmit = () => onSubmit(selectedCollections); const handleSubmit = () => onSubmit(selectedCollections);

View file

@ -21,11 +21,11 @@ import TableCellAvatar from "@saleor/components/TableCellAvatar";
import useSearchQuery from "@saleor/hooks/useSearchQuery"; import useSearchQuery from "@saleor/hooks/useSearchQuery";
import { buttonMessages } from "@saleor/intl"; import { buttonMessages } from "@saleor/intl";
import { maybe } from "@saleor/misc"; import { maybe } from "@saleor/misc";
import { SearchProducts_products_edges_node } from "../../containers/SearchProducts/types/SearchProducts"; import { SearchProducts_search_edges_node } from "../../containers/SearchProducts/types/SearchProducts";
import Checkbox from "../Checkbox"; import Checkbox from "../Checkbox";
export interface FormData { export interface FormData {
products: SearchProducts_products_edges_node[]; products: SearchProducts_search_edges_node[];
query: string; query: string;
} }
@ -53,18 +53,18 @@ const styles = createStyles({
export interface AssignProductDialogProps { export interface AssignProductDialogProps {
confirmButtonState: ConfirmButtonTransitionState; confirmButtonState: ConfirmButtonTransitionState;
open: boolean; open: boolean;
products: SearchProducts_products_edges_node[]; products: SearchProducts_search_edges_node[];
loading: boolean; loading: boolean;
onClose: () => void; onClose: () => void;
onFetch: (value: string) => void; onFetch: (value: string) => void;
onSubmit: (data: SearchProducts_products_edges_node[]) => void; onSubmit: (data: SearchProducts_search_edges_node[]) => void;
} }
function handleProductAssign( function handleProductAssign(
product: SearchProducts_products_edges_node, product: SearchProducts_search_edges_node,
isSelected: boolean, isSelected: boolean,
selectedProducts: SearchProducts_products_edges_node[], selectedProducts: SearchProducts_search_edges_node[],
setSelectedProducts: (data: SearchProducts_products_edges_node[]) => void setSelectedProducts: (data: SearchProducts_search_edges_node[]) => void
) { ) {
if (isSelected) { if (isSelected) {
setSelectedProducts( setSelectedProducts(
@ -93,7 +93,7 @@ const AssignProductDialog = withStyles(styles, {
const intl = useIntl(); const intl = useIntl();
const [query, onQueryChange] = useSearchQuery(onFetch); const [query, onQueryChange] = useSearchQuery(onFetch);
const [selectedProducts, setSelectedProducts] = React.useState< const [selectedProducts, setSelectedProducts] = React.useState<
SearchProducts_products_edges_node[] SearchProducts_search_edges_node[]
>([]); >([]);
const handleSubmit = () => onSubmit(selectedProducts); const handleSubmit = () => onSubmit(selectedProducts);

View file

@ -0,0 +1,89 @@
import { storiesOf } from "@storybook/react";
import React from "react";
import { countries } from "@saleor/fixtures";
import useMultiAutocomplete from "@saleor/hooks/useMultiAutocomplete";
import CardDecorator from "@saleor/storybook/CardDecorator";
import Decorator from "@saleor/storybook/Decorator";
import { ChoiceProvider } from "@saleor/storybook/mock";
import MultiAutocompleteSelectField, {
MultiAutocompleteSelectFieldProps
} from "./MultiAutocompleteSelectField";
import MultiAutocompleteSelectFieldContent, {
MultiAutocompleteSelectFieldContentProps
} from "./MultiAutocompleteSelectFieldContent";
const suggestions = countries.map(c => ({ label: c.name, value: c.code }));
const props: MultiAutocompleteSelectFieldProps = {
choices: undefined,
displayValues: [],
label: "Country",
loading: false,
name: "country",
onChange: () => undefined,
placeholder: "Select country",
value: undefined
};
const Story: React.FC<
Partial<
MultiAutocompleteSelectFieldProps & {
enableLoadMore: boolean;
}
>
> = ({ allowCustomValues, enableLoadMore }) => {
const { change, data: countries } = useMultiAutocomplete([suggestions[0]]);
return (
<ChoiceProvider choices={suggestions}>
{({ choices, fetchChoices, fetchMore, hasMore, loading }) => (
<MultiAutocompleteSelectField
{...props}
displayValues={countries}
choices={choices}
fetchChoices={fetchChoices}
helperText={`Value: ${countries
.map(country => country.value)
.join(", ")}`}
onChange={event => change(event, choices)}
value={countries.map(country => country.value)}
loading={loading}
hasMore={enableLoadMore ? hasMore : false}
onFetchMore={enableLoadMore ? fetchMore : undefined}
allowCustomValues={allowCustomValues}
/>
)}
</ChoiceProvider>
);
};
const contentProps: MultiAutocompleteSelectFieldContentProps = {
choices: suggestions.slice(0, 10),
displayCustomValue: false,
displayValues: [suggestions[0]],
getItemProps: () => undefined,
hasMore: false,
highlightedIndex: 0,
inputValue: suggestions[0].label,
loading: false,
onFetchMore: () => undefined
};
storiesOf("Generics / Multiple select with autocomplete", module)
.addDecorator(CardDecorator)
.addDecorator(Decorator)
.add("default", () => (
<MultiAutocompleteSelectFieldContent {...contentProps} />
))
.add("can load more", () => (
<MultiAutocompleteSelectFieldContent {...contentProps} hasMore={true} />
))
.add("no data", () => (
<MultiAutocompleteSelectFieldContent {...contentProps} choices={[]} />
))
.add("interactive", () => <Story />)
.add("interactive with custom option", () => (
<Story allowCustomValues={true} />
))
.add("interactive with load more", () => <Story enableLoadMore={true} />);

View file

@ -1,7 +1,4 @@
import CircularProgress from "@material-ui/core/CircularProgress";
import IconButton from "@material-ui/core/IconButton"; import IconButton from "@material-ui/core/IconButton";
import MenuItem from "@material-ui/core/MenuItem";
import Paper from "@material-ui/core/Paper";
import { import {
createStyles, createStyles,
Theme, Theme,
@ -12,27 +9,19 @@ import TextField from "@material-ui/core/TextField";
import Typography from "@material-ui/core/Typography"; import Typography from "@material-ui/core/Typography";
import CloseIcon from "@material-ui/icons/Close"; import CloseIcon from "@material-ui/icons/Close";
import Downshift, { ControllerStateAndHelpers } from "downshift"; import Downshift, { ControllerStateAndHelpers } from "downshift";
import { filter } from "fuzzaldrin";
import React from "react"; import React from "react";
import { FormattedMessage } from "react-intl";
import { compareTwoStrings } from "string-similarity";
import { fade } from "@material-ui/core/styles/colorManipulator"; import { fade } from "@material-ui/core/styles/colorManipulator";
import Checkbox from "@saleor/components/Checkbox";
import Debounce, { DebounceProps } from "@saleor/components/Debounce"; import Debounce, { DebounceProps } from "@saleor/components/Debounce";
import ArrowDropdownIcon from "@saleor/icons/ArrowDropdown"; import ArrowDropdownIcon from "@saleor/icons/ArrowDropdown";
import Hr from "../Hr"; import { FetchMoreProps } from "@saleor/types";
import MultiAutocompleteSelectFieldContent, {
export interface MultiAutocompleteChoiceType { MultiAutocompleteChoiceType
label: string; } from "./MultiAutocompleteSelectFieldContent";
value: string;
}
const styles = (theme: Theme) => const styles = (theme: Theme) =>
createStyles({ createStyles({
checkbox: {
height: 24,
width: 20
},
chip: { chip: {
width: "100%" width: "100%"
}, },
@ -66,49 +55,11 @@ const styles = (theme: Theme) =>
container: { container: {
flexGrow: 1, flexGrow: 1,
position: "relative" position: "relative"
},
hr: {
margin: `${theme.spacing.unit}px 0`
},
menuItem: {
"&: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,
display: "grid",
gridColumnGap: theme.spacing.unit + "px",
gridTemplateColumns: "30px 1fr",
height: "auto",
padding: 0,
whiteSpace: "normal"
},
menuItemLabel: {
overflowWrap: "break-word"
},
paper: {
left: 0,
marginTop: theme.spacing.unit,
padding: theme.spacing.unit,
position: "absolute",
right: 0,
zIndex: 2
} }
}); });
export interface MultiAutocompleteSelectFieldProps { export interface MultiAutocompleteSelectFieldProps
extends Partial<FetchMoreProps> {
allowCustomValues?: boolean; allowCustomValues?: boolean;
displayValues: MultiAutocompleteChoiceType[]; displayValues: MultiAutocompleteChoiceType[];
name: string; name: string;
@ -134,6 +85,7 @@ export const MultiAutocompleteSelectFieldComponent = withStyles(styles, {
choices, choices,
classes, classes,
displayValues, displayValues,
hasMore,
helperText, helperText,
label, label,
loading, loading,
@ -142,6 +94,7 @@ export const MultiAutocompleteSelectFieldComponent = withStyles(styles, {
value, value,
fetchChoices, fetchChoices,
onChange, onChange,
onFetchMore,
...props ...props
}: MultiAutocompleteSelectFieldProps & WithStyles<typeof styles>) => { }: MultiAutocompleteSelectFieldProps & WithStyles<typeof styles>) => {
const handleSelect = ( const handleSelect = (
@ -155,7 +108,6 @@ export const MultiAutocompleteSelectFieldComponent = withStyles(styles, {
target: { name, value: item } target: { name, value: item }
} as any); } as any);
}; };
const suggestions = choices.filter(choice => !value.includes(choice.value));
return ( return (
<> <>
@ -171,123 +123,53 @@ export const MultiAutocompleteSelectFieldComponent = withStyles(styles, {
toggleMenu, toggleMenu,
highlightedIndex, highlightedIndex,
inputValue inputValue
}) => ( }) => {
<div className={classes.container} {...props}> const displayCustomValue =
<TextField inputValue &&
InputProps={{ inputValue.length > 0 &&
...getInputProps({ allowCustomValues &&
placeholder !choices.find(
}), choice =>
endAdornment: ( choice.label.toLowerCase() === inputValue.toLowerCase()
<div> );
{loading ? (
<CircularProgress size={20} /> return (
) : ( <div className={classes.container} {...props}>
<TextField
InputProps={{
...getInputProps({
placeholder
}),
endAdornment: (
<div>
<ArrowDropdownIcon onClick={toggleMenu} /> <ArrowDropdownIcon onClick={toggleMenu} />
)} </div>
</div> ),
), id: undefined,
id: undefined, onClick: toggleMenu
onClick: toggleMenu }}
}} helperText={helperText}
helperText={helperText} label={label}
label={label} fullWidth={true}
fullWidth={true} />
/> {isOpen && (!!inputValue || !!choices.length) && (
{isOpen && (!!inputValue || !!choices.length) && ( <MultiAutocompleteSelectFieldContent
<Paper className={classes.paper} square> choices={choices.filter(
{choices.length > 0 || choice => !value.includes(choice.value)
displayValues.length > 0 || )}
allowCustomValues ? ( displayCustomValue={displayCustomValue}
<> displayValues={displayValues}
{displayValues.map(value => ( getItemProps={getItemProps}
<MenuItem hasMore={hasMore}
className={classes.menuItem} highlightedIndex={highlightedIndex}
key={value.value} loading={loading}
selected={true} inputValue={inputValue}
component="div" onFetchMore={onFetchMore}
{...getItemProps({ />
item: value.value )}
})} </div>
data-tc="multiautocomplete-select-option" );
> }}
<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
})}
data-tc="multiautocomplete-select-option"
>
<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
})}
data-tc="multiautocomplete-select-option"
>
<span className={classes.menuItemLabel}>
<FormattedMessage
defaultMessage="Add new value: {value}"
description="add custom option to select input"
values={{
value: inputValue
}}
/>
</span>
</MenuItem>
)}
</>
) : (
!loading && (
<MenuItem
disabled={true}
component="div"
data-tc="multiautocomplete-select-no-options"
>
<FormattedMessage defaultMessage="No results found" />
</MenuItem>
)
)}
</Paper>
)}
</div>
)}
</Downshift> </Downshift>
<div className={classes.chipContainer}> <div className={classes.chipContainer}>
{displayValues.map(value => ( {displayValues.map(value => (
@ -328,22 +210,12 @@ const MultiAutocompleteSelectField: React.FC<
); );
} }
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 ( return (
<MultiAutocompleteSelectFieldComponent <MultiAutocompleteSelectFieldComponent
fetchChoices={q => setQuery(q || "")} fetchChoices={q => setQuery(q || "")}
choices={sortedChoices} choices={filter(choices, query, {
key: "label"
})}
{...props} {...props}
/> />
); );

View file

@ -0,0 +1,293 @@
import CircularProgress from "@material-ui/core/CircularProgress";
import MenuItem from "@material-ui/core/MenuItem";
import Paper from "@material-ui/core/Paper";
import { Theme } from "@material-ui/core/styles";
import AddIcon from "@material-ui/icons/Add";
import { makeStyles } from "@material-ui/styles";
import classNames from "classnames";
import { GetItemPropsOptions } from "downshift";
import React from "react";
import SVG from "react-inlinesvg";
import { FormattedMessage } from "react-intl";
import chevronDown from "@assets/images/ChevronDown.svg";
import Checkbox from "@saleor/components/Checkbox";
import useElementScroll, {
isScrolledToBottom
} from "@saleor/hooks/useElementScroll";
import { FetchMoreProps } from "@saleor/types";
import Hr from "../Hr";
const menuItemHeight = 46;
const maxMenuItems = 5;
const offset = 24;
export interface MultiAutocompleteChoiceType {
label: string;
value: any;
}
export interface MultiAutocompleteSelectFieldContentProps
extends Partial<FetchMoreProps> {
choices: MultiAutocompleteChoiceType[];
displayCustomValue: boolean;
displayValues: MultiAutocompleteChoiceType[];
getItemProps: (options: GetItemPropsOptions) => void;
highlightedIndex: number;
inputValue: string;
}
const useStyles = makeStyles(
(theme: Theme) => ({
addIcon: {
height: 24,
margin: 9,
width: 20
},
arrowContainer: {
position: "relative"
},
arrowInnerContainer: {
alignItems: "center",
background: theme.palette.grey[50],
bottom: 0,
display: "flex",
height: 30,
justifyContent: "center",
opacity: 1,
position: "absolute",
transition: theme.transitions.duration.short + "ms",
width: "100%"
},
checkbox: {
height: 24,
width: 20
},
content: {
maxHeight: menuItemHeight * maxMenuItems + theme.spacing.unit * 2,
overflow: "scroll",
padding: 8
},
hide: {
opacity: 0
},
hr: {
margin: `${theme.spacing.unit}px 0`
},
menuItem: {
"&: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,
display: "grid",
gridColumnGap: theme.spacing.unit + "px",
gridTemplateColumns: "30px 1fr",
height: "auto",
padding: 0,
whiteSpace: "normal"
},
menuItemLabel: {
overflowWrap: "break-word"
},
progress: {},
progressContainer: {
display: "flex",
justifyContent: "center"
},
root: {
borderBottomLeftRadius: 8,
borderBottomRightRadius: 8,
left: 0,
marginTop: theme.spacing.unit,
overflow: "hidden",
position: "absolute",
right: 0,
zIndex: 22
}
}),
{
name: "MultiAutocompleteSelectFieldContent"
}
);
function getChoiceIndex(
index: number,
displayValues: MultiAutocompleteChoiceType[],
displayCustomValue: boolean
) {
let choiceIndex = index;
if (displayCustomValue) {
choiceIndex += 2;
}
if (displayValues.length > 0) {
choiceIndex += 1 + displayValues.length;
}
return choiceIndex;
}
const MultiAutocompleteSelectFieldContent: React.FC<
MultiAutocompleteSelectFieldContentProps
> = props => {
const {
choices,
displayCustomValue,
displayValues,
getItemProps,
hasMore,
highlightedIndex,
loading,
inputValue,
onFetchMore
} = props;
const classes = useStyles(props);
const anchor = React.useRef<HTMLDivElement>();
const scrollPosition = useElementScroll(anchor);
const [calledForMore, setCalledForMore] = React.useState(false);
const scrolledToBottom = isScrolledToBottom(anchor, scrollPosition, offset);
React.useEffect(() => {
if (!calledForMore && onFetchMore && scrolledToBottom) {
onFetchMore();
setCalledForMore(true);
}
}, [scrolledToBottom]);
React.useEffect(() => {
if (calledForMore && !loading) {
setCalledForMore(false);
}
}, [loading]);
return (
<Paper className={classes.root}>
<div className={classes.content} ref={anchor}>
{choices.length > 0 ||
displayValues.length > 0 ||
displayCustomValue ? (
<>
{displayCustomValue && (
<MenuItem
className={classes.menuItem}
key="customValue"
component="div"
{...getItemProps({
item: inputValue
})}
data-tc="multiautocomplete-select-option"
>
<AddIcon className={classes.addIcon} color="primary" />
<FormattedMessage
defaultMessage="Add new value: {value}"
description="add custom select input option"
values={{
value: inputValue
}}
/>
</MenuItem>
)}
{(choices.length > 0 || displayValues.length > 0) &&
displayCustomValue && <Hr className={classes.hr} />}
{displayValues.map(value => (
<MenuItem
className={classes.menuItem}
key={value.value}
selected={true}
component="div"
{...getItemProps({
item: value.value
})}
data-tc="multiautocomplete-select-option"
>
<Checkbox
className={classes.checkbox}
checked={true}
disableRipple
/>
<span className={classes.menuItemLabel}>{value.label}</span>
</MenuItem>
))}
{displayValues.length > 0 && choices.length > 0 && (
<Hr className={classes.hr} />
)}
{choices.map((suggestion, index) => {
const choiceIndex = getChoiceIndex(
index,
displayValues,
displayCustomValue
);
return (
<MenuItem
className={classes.menuItem}
key={suggestion.value}
selected={highlightedIndex === choiceIndex}
component="div"
{...getItemProps({
index: choiceIndex,
item: suggestion.value
})}
data-tc="multiautocomplete-select-option"
>
<Checkbox
checked={false}
className={classes.checkbox}
disableRipple
/>
<span className={classes.menuItemLabel}>
{suggestion.label}
</span>
</MenuItem>
);
})}
{hasMore && (
<>
<Hr className={classes.hr} />
<div className={classes.progressContainer}>
<CircularProgress className={classes.progress} size={24} />
</div>
</>
)}
</>
) : (
<MenuItem
disabled={true}
component="div"
data-tc="multiautocomplete-select-no-options"
>
<FormattedMessage defaultMessage="No results found" />
</MenuItem>
)}
</div>
<div className={classes.arrowContainer}>
<div
className={classNames(classes.arrowInnerContainer, {
// Needs to be explicitely compared to false because
// scrolledToBottom can be either true, false or undefined
[classes.hide]: scrolledToBottom !== false
})}
>
<SVG src={chevronDown} />
</div>
</div>
</Paper>
);
};
MultiAutocompleteSelectFieldContent.displayName =
"MultiAutocompleteSelectFieldContent";
export default MultiAutocompleteSelectFieldContent;

View file

@ -1,2 +1,3 @@
export { default } from "./MultiAutocompleteSelectField"; export { default } from "./MultiAutocompleteSelectField";
export * from "./MultiAutocompleteSelectField"; export * from "./MultiAutocompleteSelectField";
export * from "./MultiAutocompleteSelectFieldContent";

View file

@ -0,0 +1,104 @@
import { storiesOf } from "@storybook/react";
import React from "react";
import Form from "@saleor/components/Form";
import { countries } from "@saleor/fixtures";
import CardDecorator from "@saleor/storybook/CardDecorator";
import Decorator from "@saleor/storybook/Decorator";
import { ChoiceProvider } from "@saleor/storybook/mock";
import createSingleAutocompleteSelectHandler from "@saleor/utils/handlers/singleAutocompleteSelectChangeHandler";
import SingleAutocompleteSelectField, {
SingleAutocompleteSelectFieldProps
} from "./SingleAutocompleteSelectField";
import SingleAutocompleteSelectFieldContent, {
SingleAutocompleteSelectFieldContentProps
} from "./SingleAutocompleteSelectFieldContent";
const suggestions = countries.map(c => ({ label: c.name, value: c.code }));
const props: SingleAutocompleteSelectFieldProps = {
choices: undefined,
displayValue: undefined,
label: "Country",
loading: false,
name: "country",
onChange: () => undefined,
placeholder: "Select country",
value: suggestions[0].value
};
const Story: React.FC<
Partial<
SingleAutocompleteSelectFieldProps & {
enableLoadMore: boolean;
}
>
> = ({ allowCustomValues, emptyOption, enableLoadMore }) => {
const [displayValue, setDisplayValue] = React.useState(suggestions[0].label);
return (
<Form initial={{ country: suggestions[0].value }}>
{({ change, data }) => (
<ChoiceProvider choices={suggestions}>
{({ choices, fetchChoices, fetchMore, hasMore, loading }) => {
const handleSelect = createSingleAutocompleteSelectHandler(
change,
setDisplayValue,
choices
);
return (
<SingleAutocompleteSelectField
{...props}
displayValue={displayValue}
choices={choices}
fetchChoices={fetchChoices}
helperText={`Value: ${data.country}`}
loading={loading}
onChange={handleSelect}
value={data.country}
hasMore={enableLoadMore ? hasMore : false}
onFetchMore={enableLoadMore ? fetchMore : undefined}
allowCustomValues={allowCustomValues}
emptyOption={emptyOption}
/>
);
}}
</ChoiceProvider>
)}
</Form>
);
};
const contentProps: SingleAutocompleteSelectFieldContentProps = {
choices: suggestions.slice(0, 10),
displayCustomValue: false,
emptyOption: false,
getItemProps: () => undefined,
hasMore: false,
highlightedIndex: 0,
inputValue: suggestions[0].label,
isCustomValueSelected: false,
loading: false,
onFetchMore: () => undefined,
selectedItem: suggestions[0].value
};
storiesOf("Generics / Select with autocomplete", module)
.addDecorator(CardDecorator)
.addDecorator(Decorator)
.add("default", () => (
<SingleAutocompleteSelectFieldContent {...contentProps} />
))
.add("can load more", () => (
<SingleAutocompleteSelectFieldContent {...contentProps} hasMore={true} />
))
.add("no data", () => (
<SingleAutocompleteSelectFieldContent {...contentProps} choices={[]} />
))
.add("interactive", () => <Story />)
.add("interactive with custom option", () => (
<Story allowCustomValues={true} />
))
.add("interactive with empty option", () => <Story emptyOption={true} />)
.add("interactive with load more", () => <Story enableLoadMore={true} />);

View file

@ -1,59 +1,34 @@
import { Omit } from "@material-ui/core";
import CircularProgress from "@material-ui/core/CircularProgress";
import { InputProps } from "@material-ui/core/Input"; import { InputProps } from "@material-ui/core/Input";
import MenuItem from "@material-ui/core/MenuItem"; import { createStyles, withStyles, WithStyles } from "@material-ui/core/styles";
import Paper from "@material-ui/core/Paper";
import {
createStyles,
Theme,
withStyles,
WithStyles
} from "@material-ui/core/styles";
import TextField from "@material-ui/core/TextField"; import TextField from "@material-ui/core/TextField";
import Typography from "@material-ui/core/Typography";
import Downshift from "downshift"; import Downshift from "downshift";
import { filter } from "fuzzaldrin";
import React from "react"; import React from "react";
import { FormattedMessage } from "react-intl"; import SingleAutocompleteSelectFieldContent, {
import { compareTwoStrings } from "string-similarity"; SingleAutocompleteChoiceType
} from "./SingleAutocompleteSelectFieldContent";
import useStateFromProps from "@saleor/hooks/useStateFromProps"; import useStateFromProps from "@saleor/hooks/useStateFromProps";
import { FetchMoreProps } from "@saleor/types";
import ArrowDropdownIcon from "../../icons/ArrowDropdown"; import ArrowDropdownIcon from "../../icons/ArrowDropdown";
import Debounce, { DebounceProps } from "../Debounce"; import Debounce, { DebounceProps } from "../Debounce";
const styles = (theme: Theme) => const styles = createStyles({
createStyles({ container: {
container: { flexGrow: 1,
flexGrow: 1, position: "relative"
position: "relative" }
}, });
menuItem: {
height: "auto",
whiteSpace: "normal"
},
paper: {
borderRadius: 4,
left: 0,
marginTop: theme.spacing.unit,
padding: 8,
position: "absolute",
right: 0,
zIndex: 22
}
});
export interface SingleAutocompleteChoiceType { export interface SingleAutocompleteSelectFieldProps
label: string; extends Partial<FetchMoreProps> {
value: any;
}
export interface SingleAutocompleteSelectFieldProps {
error?: boolean; error?: boolean;
name: string; name: string;
displayValue: string; displayValue: string;
emptyOption?: boolean; emptyOption?: boolean;
choices: SingleAutocompleteChoiceType[]; choices: SingleAutocompleteChoiceType[];
value?: string; value: string;
disabled?: boolean; disabled?: boolean;
loading?: boolean;
placeholder?: string; placeholder?: string;
allowCustomValues?: boolean; allowCustomValues?: boolean;
helperText?: string; helperText?: string;
@ -63,13 +38,6 @@ export interface SingleAutocompleteSelectFieldProps {
onChange: (event: React.ChangeEvent<any>) => void; onChange: (event: React.ChangeEvent<any>) => void;
} }
interface SingleAutocompleteSelectFieldState {
choices: Array<{
label: string;
value: string;
}>;
}
const DebounceAutocomplete: React.ComponentType< const DebounceAutocomplete: React.ComponentType<
DebounceProps<string> DebounceProps<string>
> = Debounce; > = Debounce;
@ -85,6 +53,7 @@ const SingleAutocompleteSelectFieldComponent = withStyles(styles, {
displayValue, displayValue,
emptyOption, emptyOption,
error, error,
hasMore,
helperText, helperText,
label, label,
loading, loading,
@ -94,9 +63,11 @@ const SingleAutocompleteSelectFieldComponent = withStyles(styles, {
InputProps, InputProps,
fetchChoices, fetchChoices,
onChange, onChange,
onFetchMore,
...props ...props
}: SingleAutocompleteSelectFieldProps & WithStyles<typeof styles>) => { }: SingleAutocompleteSelectFieldProps & WithStyles<typeof styles>) => {
const [prevDisplayValue] = useStateFromProps(displayValue); const [prevDisplayValue] = useStateFromProps(displayValue);
const handleChange = item => const handleChange = item =>
onChange({ onChange({
target: { target: {
@ -131,11 +102,21 @@ const SingleAutocompleteSelectFieldComponent = withStyles(styles, {
choices && selectedItem choices && selectedItem
? choices.filter(c => c.value === selectedItem).length === 0 ? choices.filter(c => c.value === selectedItem).length === 0
: false; : false;
const hasInputValueChanged = prevDisplayValue !== displayValue;
if (prevDisplayValue !== displayValue) { if (hasInputValueChanged) {
reset({ inputValue: displayValue }); reset({ inputValue: displayValue });
} }
const displayCustomValue =
inputValue &&
inputValue.length > 0 &&
allowCustomValues &&
!choices.find(
choice =>
choice.label.toLowerCase() === inputValue.toLowerCase()
);
return ( return (
<div className={classes.container} {...props}> <div className={classes.container} {...props}>
<TextField <TextField
@ -146,11 +127,7 @@ const SingleAutocompleteSelectFieldComponent = withStyles(styles, {
}), }),
endAdornment: ( endAdornment: (
<div> <div>
{loading ? ( <ArrowDropdownIcon onClick={toggleMenu} />
<CircularProgress size={20} />
) : (
<ArrowDropdownIcon onClick={toggleMenu} />
)}
</div> </div>
), ),
error, error,
@ -165,82 +142,19 @@ const SingleAutocompleteSelectFieldComponent = withStyles(styles, {
fullWidth={true} fullWidth={true}
/> />
{isOpen && (!!inputValue || !!choices.length) && ( {isOpen && (!!inputValue || !!choices.length) && (
<Paper className={classes.paper} square> <SingleAutocompleteSelectFieldContent
{choices.length > 0 || allowCustomValues ? ( choices={choices}
<> displayCustomValue={displayCustomValue}
{emptyOption && ( emptyOption={emptyOption}
<MenuItem getItemProps={getItemProps}
className={classes.menuItem} hasMore={hasMore}
component="div" highlightedIndex={highlightedIndex}
{...getItemProps({ loading={loading}
item: "" inputValue={inputValue}
})} isCustomValueSelected={isCustomValueSelected}
data-tc="singleautocomplete-select-option" selectedItem={selectedItem}
> onFetchMore={onFetchMore}
<Typography color="textSecondary"> />
<FormattedMessage defaultMessage="None" />
</Typography>
</MenuItem>
)}
{choices.map((suggestion, index) => {
const choiceIndex = index + (emptyOption ? 1 : 0);
return (
<MenuItem
className={classes.menuItem}
key={JSON.stringify(suggestion)}
selected={
highlightedIndex === choiceIndex ||
selectedItem === suggestion.value
}
component="div"
{...getItemProps({
index: choiceIndex,
item: suggestion.value
})}
data-tc="singleautocomplete-select-option"
>
{suggestion.label}
</MenuItem>
);
})}
{allowCustomValues &&
!!inputValue &&
!choices.find(
choice =>
choice.label.toLowerCase() ===
inputValue.toLowerCase()
) && (
<MenuItem
className={classes.menuItem}
key={"customValue"}
selected={isCustomValueSelected}
component="div"
{...getItemProps({
item: inputValue
})}
data-tc="singleautocomplete-select-option"
>
<FormattedMessage
defaultMessage="Add new value: {value}"
description="add custom select input option"
values={{
value: inputValue
}}
/>
</MenuItem>
)}
</>
) : (
<MenuItem
disabled={true}
component="div"
data-tc="singleautocomplete-select-no-options"
>
<FormattedMessage defaultMessage="No results found" />
</MenuItem>
)}
</Paper>
)} )}
</div> </div>
); );
@ -252,40 +166,32 @@ const SingleAutocompleteSelectFieldComponent = withStyles(styles, {
} }
); );
export class SingleAutocompleteSelectField extends React.Component< const SingleAutocompleteSelectField: React.FC<
Omit<SingleAutocompleteSelectFieldProps, "classes">, SingleAutocompleteSelectFieldProps
SingleAutocompleteSelectFieldState > = ({ choices, fetchChoices, ...props }) => {
> { const [query, setQuery] = React.useState("");
state = { choices: this.props.choices }; if (fetchChoices) {
handleInputChange = (value: string) =>
this.setState((_, props) => ({
choices: props.choices
.sort((a, b) => {
const ratingA = compareTwoStrings(value || "", a.label);
const ratingB = compareTwoStrings(value || "", b.label);
if (ratingA > ratingB) {
return -1;
}
if (ratingA < ratingB) {
return 1;
}
return 0;
})
.slice(0, 5)
}));
render() {
if (!!this.props.fetchChoices) {
return <SingleAutocompleteSelectFieldComponent {...this.props} />;
}
return ( return (
<SingleAutocompleteSelectFieldComponent <DebounceAutocomplete debounceFn={fetchChoices}>
{...this.props} {debounceFn => (
choices={this.state.choices} <SingleAutocompleteSelectFieldComponent
fetchChoices={this.handleInputChange} choices={choices}
/> {...props}
fetchChoices={debounceFn}
/>
)}
</DebounceAutocomplete>
); );
} }
}
return (
<SingleAutocompleteSelectFieldComponent
fetchChoices={q => setQuery(q || "")}
choices={filter(choices, query, {
key: "label"
})}
{...props}
/>
);
};
export default SingleAutocompleteSelectField; export default SingleAutocompleteSelectField;

View file

@ -0,0 +1,250 @@
import CircularProgress from "@material-ui/core/CircularProgress";
import MenuItem from "@material-ui/core/MenuItem";
import Paper from "@material-ui/core/Paper";
import { Theme } from "@material-ui/core/styles";
import Typography from "@material-ui/core/Typography";
import { makeStyles } from "@material-ui/styles";
import classNames from "classnames";
import { GetItemPropsOptions } from "downshift";
import React from "react";
import SVG from "react-inlinesvg";
import { FormattedMessage } from "react-intl";
import chevronDown from "@assets/images/ChevronDown.svg";
import useElementScroll, {
isScrolledToBottom
} from "@saleor/hooks/useElementScroll";
import { FetchMoreProps } from "@saleor/types";
import Hr from "../Hr";
const menuItemHeight = 46;
const maxMenuItems = 5;
const offset = 24;
export interface SingleAutocompleteChoiceType {
label: string;
value: any;
}
export interface SingleAutocompleteSelectFieldContentProps
extends Partial<FetchMoreProps> {
choices: SingleAutocompleteChoiceType[];
displayCustomValue: boolean;
emptyOption: boolean;
getItemProps: (options: GetItemPropsOptions) => void;
highlightedIndex: number;
inputValue: string;
isCustomValueSelected: boolean;
selectedItem: any;
}
const useStyles = makeStyles(
(theme: Theme) => ({
arrowContainer: {
position: "relative"
},
arrowInnerContainer: {
alignItems: "center",
background: theme.palette.grey[50],
bottom: 0,
display: "flex",
height: 30,
justifyContent: "center",
opacity: 1,
position: "absolute",
transition: theme.transitions.duration.short + "ms",
width: "100%"
},
content: {
maxHeight: menuItemHeight * maxMenuItems + theme.spacing.unit * 2,
overflow: "scroll",
padding: 8
},
hide: {
opacity: 0
},
hr: {
margin: `${theme.spacing.unit}px 0`
},
menuItem: {
height: "auto",
whiteSpace: "normal"
},
progress: {},
progressContainer: {
display: "flex",
justifyContent: "center"
},
root: {
borderBottomLeftRadius: 8,
borderBottomRightRadius: 8,
left: 0,
marginTop: theme.spacing.unit,
overflow: "hidden",
position: "absolute",
right: 0,
zIndex: 22
}
}),
{
name: "SingleAutocompleteSelectFieldContent"
}
);
function getChoiceIndex(
index: number,
emptyValue: boolean,
customValue: boolean
) {
let choiceIndex = index;
if (emptyValue) {
choiceIndex += 1;
}
if (customValue) {
choiceIndex += 2;
}
return choiceIndex;
}
const SingleAutocompleteSelectFieldContent: React.FC<
SingleAutocompleteSelectFieldContentProps
> = props => {
const {
choices,
displayCustomValue,
emptyOption,
getItemProps,
hasMore,
highlightedIndex,
loading,
inputValue,
isCustomValueSelected,
selectedItem,
onFetchMore
} = props;
const classes = useStyles(props);
const anchor = React.useRef<HTMLDivElement>();
const scrollPosition = useElementScroll(anchor);
const [calledForMore, setCalledForMore] = React.useState(false);
const scrolledToBottom = isScrolledToBottom(anchor, scrollPosition, offset);
React.useEffect(() => {
if (!calledForMore && onFetchMore && scrolledToBottom) {
onFetchMore();
setCalledForMore(true);
}
}, [scrolledToBottom]);
React.useEffect(() => {
if (calledForMore && !loading) {
setCalledForMore(false);
}
}, [loading]);
return (
<Paper className={classes.root}>
<div className={classes.content} ref={anchor}>
{choices.length > 0 || displayCustomValue ? (
<>
{emptyOption && (
<MenuItem
className={classes.menuItem}
component="div"
{...getItemProps({
item: ""
})}
data-tc="singleautocomplete-select-option"
>
<Typography color="textSecondary">
<FormattedMessage defaultMessage="None" />
</Typography>
</MenuItem>
)}
{displayCustomValue && (
<MenuItem
className={classes.menuItem}
key={"customValue"}
selected={isCustomValueSelected}
component="div"
{...getItemProps({
item: inputValue
})}
data-tc="singleautocomplete-select-option"
>
<FormattedMessage
defaultMessage="Add new value: {value}"
description="add custom select input option"
values={{
value: inputValue
}}
/>
</MenuItem>
)}
{choices.length > 0 && displayCustomValue && (
<Hr className={classes.hr} />
)}
{choices.map((suggestion, index) => {
const choiceIndex = getChoiceIndex(
index,
emptyOption,
displayCustomValue
);
return (
<MenuItem
className={classes.menuItem}
key={JSON.stringify(suggestion)}
selected={
highlightedIndex === choiceIndex ||
selectedItem === suggestion.value
}
component="div"
{...getItemProps({
index: choiceIndex,
item: suggestion.value
})}
data-tc="singleautocomplete-select-option"
>
{suggestion.label}
</MenuItem>
);
})}
{hasMore && (
<>
<Hr className={classes.hr} />
<div className={classes.progressContainer}>
<CircularProgress className={classes.progress} size={24} />
</div>
</>
)}
</>
) : (
<MenuItem
disabled={true}
component="div"
data-tc="singleautocomplete-select-no-options"
>
<FormattedMessage defaultMessage="No results found" />
</MenuItem>
)}
</div>
<div className={classes.arrowContainer}>
<div
className={classNames(classes.arrowInnerContainer, {
// Needs to be explicitely compared to false because
// scrolledToBottom can be either true, false or undefined
[classes.hide]: scrolledToBottom !== false
})}
>
<SVG src={chevronDown} />
</div>
</div>
</Paper>
);
};
SingleAutocompleteSelectFieldContent.displayName =
"SingleAutocompleteSelectFieldContent";
export default SingleAutocompleteSelectFieldContent;

View file

@ -1,2 +1,3 @@
export { default } from "./SingleAutocompleteSelectField"; export { default } from "./SingleAutocompleteSelectField";
export * from "./SingleAutocompleteSelectField"; export * from "./SingleAutocompleteSelectField";
export * from "./SingleAutocompleteSelectFieldContent";

View file

@ -6,7 +6,7 @@ export const API_URI = process.env.API_URI || "/graphql/";
export const DEFAULT_INITIAL_SEARCH_DATA: SearchQueryVariables = { export const DEFAULT_INITIAL_SEARCH_DATA: SearchQueryVariables = {
after: null, after: null,
first: 5, first: 20,
query: "" query: ""
}; };

View file

@ -10,26 +10,30 @@ export interface SearchQueryVariables {
query: string; query: string;
} }
interface BaseSearchProps<
TQuery,
TQueryVariables extends SearchQueryVariables
> {
children: (props: {
loadMore: () => void;
search: (query: string) => void;
result: TypedQueryResult<TQuery, TQueryVariables>;
}) => React.ReactElement<any>;
variables: TQueryVariables;
}
function BaseSearch<TQuery, TQueryVariables extends SearchQueryVariables>( function BaseSearch<TQuery, TQueryVariables extends SearchQueryVariables>(
query: DocumentNode query: DocumentNode,
loadMoreFn: (result: TypedQueryResult<TQuery, TQueryVariables>) => void
) { ) {
const Query = TypedQuery<TQuery, TQueryVariables>(query); const Query = TypedQuery<TQuery, TQueryVariables>(query);
interface BaseSearchProps {
children: (props: {
search: (query: string) => void;
result: TypedQueryResult<TQuery, TQueryVariables>;
}) => React.ReactElement<any>;
variables: TQueryVariables;
}
interface BaseSearchState {
query: string;
}
class BaseSearchComponent extends React.Component< class BaseSearchComponent extends React.Component<
BaseSearchProps, BaseSearchProps<TQuery, TQueryVariables>,
BaseSearchState SearchQueryVariables
> { > {
state: BaseSearchState = { state: SearchQueryVariables = {
first: this.props.variables.first,
query: this.props.variables.query query: this.props.variables.query
}; };
@ -54,7 +58,13 @@ function BaseSearch<TQuery, TQueryVariables extends SearchQueryVariables>(
query: this.state.query query: this.state.query
}} }}
> >
{result => children({ search, result })} {result =>
children({
loadMore: () => loadMoreFn(result),
result,
search
})
}
</Query> </Query>
)} )}
</Debounce> </Debounce>
@ -63,4 +73,5 @@ function BaseSearch<TQuery, TQueryVariables extends SearchQueryVariables>(
} }
return BaseSearchComponent; return BaseSearchComponent;
} }
export default BaseSearch; export default BaseSearch;

View file

@ -1,24 +1,29 @@
import gql from "graphql-tag"; import gql from "graphql-tag";
import BaseSearch from "../BaseSearch"; import { pageInfoFragment } from "@saleor/queries";
import TopLevelSearch from "../TopLevelSearch";
import { import {
SearchCategories, SearchCategories,
SearchCategoriesVariables SearchCategoriesVariables
} from "./types/SearchCategories"; } from "./types/SearchCategories";
export const searchCategories = gql` export const searchCategories = gql`
${pageInfoFragment}
query SearchCategories($after: String, $first: Int!, $query: String!) { query SearchCategories($after: String, $first: Int!, $query: String!) {
categories(after: $after, first: $first, query: $query) { search: categories(after: $after, first: $first, query: $query) {
edges { edges {
node { node {
id id
name name
} }
} }
pageInfo {
...PageInfoFragment
}
} }
} }
`; `;
export default BaseSearch<SearchCategories, SearchCategoriesVariables>( export default TopLevelSearch<SearchCategories, SearchCategoriesVariables>(
searchCategories searchCategories
); );

View file

@ -6,24 +6,33 @@
// GraphQL query operation: SearchCategories // GraphQL query operation: SearchCategories
// ==================================================== // ====================================================
export interface SearchCategories_categories_edges_node { export interface SearchCategories_search_edges_node {
__typename: "Category"; __typename: "Category";
id: string; id: string;
name: string; name: string;
} }
export interface SearchCategories_categories_edges { export interface SearchCategories_search_edges {
__typename: "CategoryCountableEdge"; __typename: "CategoryCountableEdge";
node: SearchCategories_categories_edges_node; node: SearchCategories_search_edges_node;
} }
export interface SearchCategories_categories { export interface SearchCategories_search_pageInfo {
__typename: "PageInfo";
endCursor: string | null;
hasNextPage: boolean;
hasPreviousPage: boolean;
startCursor: string | null;
}
export interface SearchCategories_search {
__typename: "CategoryCountableConnection"; __typename: "CategoryCountableConnection";
edges: SearchCategories_categories_edges[]; edges: SearchCategories_search_edges[];
pageInfo: SearchCategories_search_pageInfo;
} }
export interface SearchCategories { export interface SearchCategories {
categories: SearchCategories_categories | null; search: SearchCategories_search | null;
} }
export interface SearchCategoriesVariables { export interface SearchCategoriesVariables {

View file

@ -1,24 +1,29 @@
import gql from "graphql-tag"; import gql from "graphql-tag";
import BaseSearch from "../BaseSearch"; import { pageInfoFragment } from "@saleor/queries";
import TopLevelSearch from "../TopLevelSearch";
import { import {
SearchCollections, SearchCollections,
SearchCollectionsVariables SearchCollectionsVariables
} from "./types/SearchCollections"; } from "./types/SearchCollections";
export const searchCollections = gql` export const searchCollections = gql`
${pageInfoFragment}
query SearchCollections($after: String, $first: Int!, $query: String!) { query SearchCollections($after: String, $first: Int!, $query: String!) {
collections(after: $after, first: $first, query: $query) { search: collections(after: $after, first: $first, query: $query) {
edges { edges {
node { node {
id id
name name
} }
} }
pageInfo {
...PageInfoFragment
}
} }
} }
`; `;
export default BaseSearch<SearchCollections, SearchCollectionsVariables>( export default TopLevelSearch<SearchCollections, SearchCollectionsVariables>(
searchCollections searchCollections
); );

View file

@ -6,24 +6,33 @@
// GraphQL query operation: SearchCollections // GraphQL query operation: SearchCollections
// ==================================================== // ====================================================
export interface SearchCollections_collections_edges_node { export interface SearchCollections_search_edges_node {
__typename: "Collection"; __typename: "Collection";
id: string; id: string;
name: string; name: string;
} }
export interface SearchCollections_collections_edges { export interface SearchCollections_search_edges {
__typename: "CollectionCountableEdge"; __typename: "CollectionCountableEdge";
node: SearchCollections_collections_edges_node; node: SearchCollections_search_edges_node;
} }
export interface SearchCollections_collections { export interface SearchCollections_search_pageInfo {
__typename: "PageInfo";
endCursor: string | null;
hasNextPage: boolean;
hasPreviousPage: boolean;
startCursor: string | null;
}
export interface SearchCollections_search {
__typename: "CollectionCountableConnection"; __typename: "CollectionCountableConnection";
edges: SearchCollections_collections_edges[]; edges: SearchCollections_search_edges[];
pageInfo: SearchCollections_search_pageInfo;
} }
export interface SearchCollections { export interface SearchCollections {
collections: SearchCollections_collections | null; search: SearchCollections_search | null;
} }
export interface SearchCollectionsVariables { export interface SearchCollectionsVariables {

View file

@ -1,24 +1,29 @@
import gql from "graphql-tag"; import gql from "graphql-tag";
import BaseSearch from "../BaseSearch"; import { pageInfoFragment } from "@saleor/queries";
import TopLevelSearch from "../TopLevelSearch";
import { import {
SearchCustomers, SearchCustomers,
SearchCustomersVariables SearchCustomersVariables
} from "./types/SearchCustomers"; } from "./types/SearchCustomers";
export const searchCustomers = gql` export const searchCustomers = gql`
${pageInfoFragment}
query SearchCustomers($after: String, $first: Int!, $query: String!) { query SearchCustomers($after: String, $first: Int!, $query: String!) {
customers(after: $after, first: $first, query: $query) { search: customers(after: $after, first: $first, query: $query) {
edges { edges {
node { node {
id id
email email
} }
} }
pageInfo {
...PageInfoFragment
}
} }
} }
`; `;
export default BaseSearch<SearchCustomers, SearchCustomersVariables>( export default TopLevelSearch<SearchCustomers, SearchCustomersVariables>(
searchCustomers searchCustomers
); );

View file

@ -6,24 +6,33 @@
// GraphQL query operation: SearchCustomers // GraphQL query operation: SearchCustomers
// ==================================================== // ====================================================
export interface SearchCustomers_customers_edges_node { export interface SearchCustomers_search_edges_node {
__typename: "User"; __typename: "User";
id: string; id: string;
email: string; email: string;
} }
export interface SearchCustomers_customers_edges { export interface SearchCustomers_search_edges {
__typename: "UserCountableEdge"; __typename: "UserCountableEdge";
node: SearchCustomers_customers_edges_node; node: SearchCustomers_search_edges_node;
} }
export interface SearchCustomers_customers { export interface SearchCustomers_search_pageInfo {
__typename: "PageInfo";
endCursor: string | null;
hasNextPage: boolean;
hasPreviousPage: boolean;
startCursor: string | null;
}
export interface SearchCustomers_search {
__typename: "UserCountableConnection"; __typename: "UserCountableConnection";
edges: SearchCustomers_customers_edges[]; edges: SearchCustomers_search_edges[];
pageInfo: SearchCustomers_search_pageInfo;
} }
export interface SearchCustomers { export interface SearchCustomers {
customers: SearchCustomers_customers | null; search: SearchCustomers_search | null;
} }
export interface SearchCustomersVariables { export interface SearchCustomersVariables {

View file

@ -1,19 +1,24 @@
import gql from "graphql-tag"; import gql from "graphql-tag";
import BaseSearch from "../BaseSearch"; import { pageInfoFragment } from "@saleor/queries";
import TopLevelSearch from "../TopLevelSearch";
import { SearchPages, SearchPagesVariables } from "./types/SearchPages"; import { SearchPages, SearchPagesVariables } from "./types/SearchPages";
export const searchPages = gql` export const searchPages = gql`
${pageInfoFragment}
query SearchPages($after: String, $first: Int!, $query: String!) { query SearchPages($after: String, $first: Int!, $query: String!) {
pages(after: $after, first: $first, query: $query) { search: pages(after: $after, first: $first, query: $query) {
edges { edges {
node { node {
id id
title title
} }
} }
pageInfo {
...PageInfoFragment
}
} }
} }
`; `;
export default BaseSearch<SearchPages, SearchPagesVariables>(searchPages); export default TopLevelSearch<SearchPages, SearchPagesVariables>(searchPages);

View file

@ -6,24 +6,33 @@
// GraphQL query operation: SearchPages // GraphQL query operation: SearchPages
// ==================================================== // ====================================================
export interface SearchPages_pages_edges_node { export interface SearchPages_search_edges_node {
__typename: "Page"; __typename: "Page";
id: string; id: string;
title: string; title: string;
} }
export interface SearchPages_pages_edges { export interface SearchPages_search_edges {
__typename: "PageCountableEdge"; __typename: "PageCountableEdge";
node: SearchPages_pages_edges_node; node: SearchPages_search_edges_node;
} }
export interface SearchPages_pages { export interface SearchPages_search_pageInfo {
__typename: "PageInfo";
endCursor: string | null;
hasNextPage: boolean;
hasPreviousPage: boolean;
startCursor: string | null;
}
export interface SearchPages_search {
__typename: "PageCountableConnection"; __typename: "PageCountableConnection";
edges: SearchPages_pages_edges[]; edges: SearchPages_search_edges[];
pageInfo: SearchPages_search_pageInfo;
} }
export interface SearchPages { export interface SearchPages {
pages: SearchPages_pages | null; search: SearchPages_search | null;
} }
export interface SearchPagesVariables { export interface SearchPagesVariables {

View file

@ -1,14 +1,20 @@
import gql from "graphql-tag"; import gql from "graphql-tag";
import BaseSearch from "../BaseSearch"; import { pageInfoFragment } from "@saleor/queries";
import TopLevelSearch from "../TopLevelSearch";
import { import {
SearchProductTypes, SearchProductTypes,
SearchProductTypesVariables SearchProductTypesVariables
} from "./types/SearchProductTypes"; } from "./types/SearchProductTypes";
export const searchProductTypes = gql` export const searchProductTypes = gql`
${pageInfoFragment}
query SearchProductTypes($after: String, $first: Int!, $query: String!) { query SearchProductTypes($after: String, $first: Int!, $query: String!) {
productTypes(after: $after, first: $first, filter: { search: $query }) { search: productTypes(
after: $after
first: $first
filter: { search: $query }
) {
edges { edges {
node { node {
id id
@ -28,10 +34,13 @@ export const searchProductTypes = gql`
} }
} }
} }
pageInfo {
...PageInfoFragment
}
} }
} }
`; `;
export default BaseSearch<SearchProductTypes, SearchProductTypesVariables>( export default TopLevelSearch<SearchProductTypes, SearchProductTypesVariables>(
searchProductTypes searchProductTypes
); );

View file

@ -8,43 +8,52 @@ import { AttributeInputTypeEnum } from "./../../../types/globalTypes";
// GraphQL query operation: SearchProductTypes // GraphQL query operation: SearchProductTypes
// ==================================================== // ====================================================
export interface SearchProductTypes_productTypes_edges_node_productAttributes_values { export interface SearchProductTypes_search_edges_node_productAttributes_values {
__typename: "AttributeValue"; __typename: "AttributeValue";
id: string; id: string;
name: string | null; name: string | null;
slug: string | null; slug: string | null;
} }
export interface SearchProductTypes_productTypes_edges_node_productAttributes { export interface SearchProductTypes_search_edges_node_productAttributes {
__typename: "Attribute"; __typename: "Attribute";
id: string; id: string;
inputType: AttributeInputTypeEnum | null; inputType: AttributeInputTypeEnum | null;
slug: string | null; slug: string | null;
name: string | null; name: string | null;
valueRequired: boolean; valueRequired: boolean;
values: (SearchProductTypes_productTypes_edges_node_productAttributes_values | null)[] | null; values: (SearchProductTypes_search_edges_node_productAttributes_values | null)[] | null;
} }
export interface SearchProductTypes_productTypes_edges_node { export interface SearchProductTypes_search_edges_node {
__typename: "ProductType"; __typename: "ProductType";
id: string; id: string;
name: string; name: string;
hasVariants: boolean; hasVariants: boolean;
productAttributes: (SearchProductTypes_productTypes_edges_node_productAttributes | null)[] | null; productAttributes: (SearchProductTypes_search_edges_node_productAttributes | null)[] | null;
} }
export interface SearchProductTypes_productTypes_edges { export interface SearchProductTypes_search_edges {
__typename: "ProductTypeCountableEdge"; __typename: "ProductTypeCountableEdge";
node: SearchProductTypes_productTypes_edges_node; node: SearchProductTypes_search_edges_node;
} }
export interface SearchProductTypes_productTypes { export interface SearchProductTypes_search_pageInfo {
__typename: "PageInfo";
endCursor: string | null;
hasNextPage: boolean;
hasPreviousPage: boolean;
startCursor: string | null;
}
export interface SearchProductTypes_search {
__typename: "ProductTypeCountableConnection"; __typename: "ProductTypeCountableConnection";
edges: SearchProductTypes_productTypes_edges[]; edges: SearchProductTypes_search_edges[];
pageInfo: SearchProductTypes_search_pageInfo;
} }
export interface SearchProductTypes { export interface SearchProductTypes {
productTypes: SearchProductTypes_productTypes | null; search: SearchProductTypes_search | null;
} }
export interface SearchProductTypesVariables { export interface SearchProductTypesVariables {

View file

@ -1,14 +1,16 @@
import gql from "graphql-tag"; import gql from "graphql-tag";
import BaseSearch from "../BaseSearch"; import { pageInfoFragment } from "@saleor/queries";
import TopLevelSearch from "../TopLevelSearch";
import { import {
SearchProducts, SearchProducts,
SearchProductsVariables SearchProductsVariables
} from "./types/SearchProducts"; } from "./types/SearchProducts";
export const searchProducts = gql` export const searchProducts = gql`
${pageInfoFragment}
query SearchProducts($after: String, $first: Int!, $query: String!) { query SearchProducts($after: String, $first: Int!, $query: String!) {
products(after: $after, first: $first, query: $query) { search: products(after: $after, first: $first, query: $query) {
edges { edges {
node { node {
id id
@ -18,10 +20,13 @@ export const searchProducts = gql`
} }
} }
} }
pageInfo {
...PageInfoFragment
}
} }
} }
`; `;
export default BaseSearch<SearchProducts, SearchProductsVariables>( export default TopLevelSearch<SearchProducts, SearchProductsVariables>(
searchProducts searchProducts
); );

View file

@ -6,30 +6,39 @@
// GraphQL query operation: SearchProducts // GraphQL query operation: SearchProducts
// ==================================================== // ====================================================
export interface SearchProducts_products_edges_node_thumbnail { export interface SearchProducts_search_edges_node_thumbnail {
__typename: "Image"; __typename: "Image";
url: string; url: string;
} }
export interface SearchProducts_products_edges_node { export interface SearchProducts_search_edges_node {
__typename: "Product"; __typename: "Product";
id: string; id: string;
name: string; name: string;
thumbnail: SearchProducts_products_edges_node_thumbnail | null; thumbnail: SearchProducts_search_edges_node_thumbnail | null;
} }
export interface SearchProducts_products_edges { export interface SearchProducts_search_edges {
__typename: "ProductCountableEdge"; __typename: "ProductCountableEdge";
node: SearchProducts_products_edges_node; node: SearchProducts_search_edges_node;
} }
export interface SearchProducts_products { export interface SearchProducts_search_pageInfo {
__typename: "PageInfo";
endCursor: string | null;
hasNextPage: boolean;
hasPreviousPage: boolean;
startCursor: string | null;
}
export interface SearchProducts_search {
__typename: "ProductCountableConnection"; __typename: "ProductCountableConnection";
edges: SearchProducts_products_edges[]; edges: SearchProducts_search_edges[];
pageInfo: SearchProducts_search_pageInfo;
} }
export interface SearchProducts { export interface SearchProducts {
products: SearchProducts_products | null; search: SearchProducts_search | null;
} }
export interface SearchProductsVariables { export interface SearchProductsVariables {

View file

@ -0,0 +1,47 @@
import { DocumentNode } from "graphql";
import { PageInfoFragment } from "@saleor/types/PageInfoFragment";
import BaseSearch, { SearchQueryVariables } from "./BaseSearch";
export interface SearchQuery {
search: {
edges: Array<{
node: any;
}>;
pageInfo: PageInfoFragment;
};
}
function TopLevelSearch<
TQuery extends SearchQuery,
TQueryVariables extends SearchQueryVariables
>(query: DocumentNode) {
return BaseSearch<TQuery, TQueryVariables>(query, result => {
if (result.data.search.pageInfo.hasNextPage) {
result.loadMore(
(prev, next) => {
if (
prev.search.pageInfo.endCursor === next.search.pageInfo.endCursor
) {
return prev;
}
return {
...prev,
search: {
...prev.search,
edges: [...prev.search.edges, ...next.search.edges],
pageInfo: next.search.pageInfo
}
};
},
{
...result.variables,
after: result.data.search.pageInfo.endCursor
}
);
}
});
}
export default TopLevelSearch;

View file

@ -371,7 +371,7 @@ export const SaleDetails: React.StatelessComponent<SaleDetailsProps> = ({
}) })
} }
products={maybe(() => products={maybe(() =>
searchProductsOpts.data.products.edges searchProductsOpts.data.search.edges
.map(edge => edge.node) .map(edge => edge.node)
.filter( .filter(
suggestedProduct => suggestedProduct.id suggestedProduct => suggestedProduct.id
@ -389,7 +389,7 @@ export const SaleDetails: React.StatelessComponent<SaleDetailsProps> = ({
}) => ( }) => (
<AssignCategoriesDialog <AssignCategoriesDialog
categories={maybe(() => categories={maybe(() =>
searchCategoriesOpts.data.categories.edges searchCategoriesOpts.data.search.edges
.map(edge => edge.node) .map(edge => edge.node)
.filter( .filter(
suggestedCategory => suggestedCategory =>
@ -426,7 +426,7 @@ export const SaleDetails: React.StatelessComponent<SaleDetailsProps> = ({
}) => ( }) => (
<AssignCollectionDialog <AssignCollectionDialog
collections={maybe(() => collections={maybe(() =>
searchCollectionsOpts.data.collections.edges searchCollectionsOpts.data.search.edges
.map(edge => edge.node) .map(edge => edge.node)
.filter( .filter(
suggestedCategory => suggestedCategory =>

View file

@ -429,7 +429,7 @@ export const VoucherDetails: React.StatelessComponent<VoucherDetailsProps> = ({
}) => ( }) => (
<AssignCategoriesDialog <AssignCategoriesDialog
categories={maybe(() => categories={maybe(() =>
searchCategoriesOpts.data.categories.edges searchCategoriesOpts.data.search.edges
.map(edge => edge.node) .map(edge => edge.node)
.filter( .filter(
suggestedCategory => suggestedCategory =>
@ -466,7 +466,7 @@ export const VoucherDetails: React.StatelessComponent<VoucherDetailsProps> = ({
}) => ( }) => (
<AssignCollectionDialog <AssignCollectionDialog
collections={maybe(() => collections={maybe(() =>
searchCollectionsOpts.data.collections.edges searchCollectionsOpts.data.search.edges
.map(edge => edge.node) .map(edge => edge.node)
.filter( .filter(
suggestedCategory => suggestedCategory =>
@ -544,7 +544,7 @@ export const VoucherDetails: React.StatelessComponent<VoucherDetailsProps> = ({
}) })
} }
products={maybe(() => products={maybe(() =>
searchProductsOpts.data.products.edges searchProductsOpts.data.search.edges
.map(edge => edge.node) .map(edge => edge.node)
.filter( .filter(
suggestedProduct => suggestedProduct.id suggestedProduct => suggestedProduct.id

View file

@ -44,11 +44,249 @@ export const listActionsProps: ListActions = {
}; };
export const countries = [ export const countries = [
{ code: "AF", label: "Afghanistan" }, { name: "Afghanistan", code: "AF" },
{ code: "AX", label: "Åland Islands" }, { name: "Åland Islands", code: "AX" },
{ code: "AL", label: "Albania" }, { name: "Albania", code: "AL" },
{ code: "DZ", label: "Algeria" }, { name: "Algeria", code: "DZ" },
{ code: "AS", label: "American Samoa" } { name: "American Samoa", code: "AS" },
{ name: "AndorrA", code: "AD" },
{ name: "Angola", code: "AO" },
{ name: "Anguilla", code: "AI" },
{ name: "Antarctica", code: "AQ" },
{ name: "Antigua and Barbuda", code: "AG" },
{ name: "Argentina", code: "AR" },
{ name: "Armenia", code: "AM" },
{ name: "Aruba", code: "AW" },
{ name: "Australia", code: "AU" },
{ name: "Austria", code: "AT" },
{ name: "Azerbaijan", code: "AZ" },
{ name: "Bahamas", code: "BS" },
{ name: "Bahrain", code: "BH" },
{ name: "Bangladesh", code: "BD" },
{ name: "Barbados", code: "BB" },
{ name: "Belarus", code: "BY" },
{ name: "Belgium", code: "BE" },
{ name: "Belize", code: "BZ" },
{ name: "Benin", code: "BJ" },
{ name: "Bermuda", code: "BM" },
{ name: "Bhutan", code: "BT" },
{ name: "Bolivia", code: "BO" },
{ name: "Bosnia and Herzegovina", code: "BA" },
{ name: "Botswana", code: "BW" },
{ name: "Bouvet Island", code: "BV" },
{ name: "Brazil", code: "BR" },
{ name: "British Indian Ocean Territory", code: "IO" },
{ name: "Brunei Darussalam", code: "BN" },
{ name: "Bulgaria", code: "BG" },
{ name: "Burkina Faso", code: "BF" },
{ name: "Burundi", code: "BI" },
{ name: "Cambodia", code: "KH" },
{ name: "Cameroon", code: "CM" },
{ name: "Canada", code: "CA" },
{ name: "Cape Verde", code: "CV" },
{ name: "Cayman Islands", code: "KY" },
{ name: "Central African Republic", code: "CF" },
{ name: "Chad", code: "TD" },
{ name: "Chile", code: "CL" },
{ name: "China", code: "CN" },
{ name: "Christmas Island", code: "CX" },
{ name: "Cocos (Keeling) Islands", code: "CC" },
{ name: "Colombia", code: "CO" },
{ name: "Comoros", code: "KM" },
{ name: "Congo", code: "CG" },
{ name: "Congo, The Democratic Republic of the", code: "CD" },
{ name: "Cook Islands", code: "CK" },
{ name: "Costa Rica", code: "CR" },
{ name: "Cote D'Ivoire", code: "CI" },
{ name: "Croatia", code: "HR" },
{ name: "Cuba", code: "CU" },
{ name: "Cyprus", code: "CY" },
{ name: "Czech Republic", code: "CZ" },
{ name: "Denmark", code: "DK" },
{ name: "Djibouti", code: "DJ" },
{ name: "Dominica", code: "DM" },
{ name: "Dominican Republic", code: "DO" },
{ name: "Ecuador", code: "EC" },
{ name: "Egypt", code: "EG" },
{ name: "El Salvador", code: "SV" },
{ name: "Equatorial Guinea", code: "GQ" },
{ name: "Eritrea", code: "ER" },
{ name: "Estonia", code: "EE" },
{ name: "Ethiopia", code: "ET" },
{ name: "Falkland Islands (Malvinas)", code: "FK" },
{ name: "Faroe Islands", code: "FO" },
{ name: "Fiji", code: "FJ" },
{ name: "Finland", code: "FI" },
{ name: "France", code: "FR" },
{ name: "French Guiana", code: "GF" },
{ name: "French Polynesia", code: "PF" },
{ name: "French Southern Territories", code: "TF" },
{ name: "Gabon", code: "GA" },
{ name: "Gambia", code: "GM" },
{ name: "Georgia", code: "GE" },
{ name: "Germany", code: "DE" },
{ name: "Ghana", code: "GH" },
{ name: "Gibraltar", code: "GI" },
{ name: "Greece", code: "GR" },
{ name: "Greenland", code: "GL" },
{ name: "Grenada", code: "GD" },
{ name: "Guadeloupe", code: "GP" },
{ name: "Guam", code: "GU" },
{ name: "Guatemala", code: "GT" },
{ name: "Guernsey", code: "GG" },
{ name: "Guinea", code: "GN" },
{ name: "Guinea-Bissau", code: "GW" },
{ name: "Guyana", code: "GY" },
{ name: "Haiti", code: "HT" },
{ name: "Heard Island and Mcdonald Islands", code: "HM" },
{ name: "Holy See (Vatican City State)", code: "VA" },
{ name: "Honduras", code: "HN" },
{ name: "Hong Kong", code: "HK" },
{ name: "Hungary", code: "HU" },
{ name: "Iceland", code: "IS" },
{ name: "India", code: "IN" },
{ name: "Indonesia", code: "ID" },
{ name: "Iran, Islamic Republic Of", code: "IR" },
{ name: "Iraq", code: "IQ" },
{ name: "Ireland", code: "IE" },
{ name: "Isle of Man", code: "IM" },
{ name: "Israel", code: "IL" },
{ name: "Italy", code: "IT" },
{ name: "Jamaica", code: "JM" },
{ name: "Japan", code: "JP" },
{ name: "Jersey", code: "JE" },
{ name: "Jordan", code: "JO" },
{ name: "Kazakhstan", code: "KZ" },
{ name: "Kenya", code: "KE" },
{ name: "Kiribati", code: "KI" },
{ name: "Korea, Democratic People'S Republic of", code: "KP" },
{ name: "Korea, Republic of", code: "KR" },
{ name: "Kuwait", code: "KW" },
{ name: "Kyrgyzstan", code: "KG" },
{ name: "Lao People'S Democratic Republic", code: "LA" },
{ name: "Latvia", code: "LV" },
{ name: "Lebanon", code: "LB" },
{ name: "Lesotho", code: "LS" },
{ name: "Liberia", code: "LR" },
{ name: "Libyan Arab Jamahiriya", code: "LY" },
{ name: "Liechtenstein", code: "LI" },
{ name: "Lithuania", code: "LT" },
{ name: "Luxembourg", code: "LU" },
{ name: "Macao", code: "MO" },
{ name: "Macedonia, The Former Yugoslav Republic of", code: "MK" },
{ name: "Madagascar", code: "MG" },
{ name: "Malawi", code: "MW" },
{ name: "Malaysia", code: "MY" },
{ name: "Maldives", code: "MV" },
{ name: "Mali", code: "ML" },
{ name: "Malta", code: "MT" },
{ name: "Marshall Islands", code: "MH" },
{ name: "Martinique", code: "MQ" },
{ name: "Mauritania", code: "MR" },
{ name: "Mauritius", code: "MU" },
{ name: "Mayotte", code: "YT" },
{ name: "Mexico", code: "MX" },
{ name: "Micronesia, Federated States of", code: "FM" },
{ name: "Moldova, Republic of", code: "MD" },
{ name: "Monaco", code: "MC" },
{ name: "Mongolia", code: "MN" },
{ name: "Montserrat", code: "MS" },
{ name: "Morocco", code: "MA" },
{ name: "Mozambique", code: "MZ" },
{ name: "Myanmar", code: "MM" },
{ name: "Namibia", code: "NA" },
{ name: "Nauru", code: "NR" },
{ name: "Nepal", code: "NP" },
{ name: "Netherlands", code: "NL" },
{ name: "Netherlands Antilles", code: "AN" },
{ name: "New Caledonia", code: "NC" },
{ name: "New Zealand", code: "NZ" },
{ name: "Nicaragua", code: "NI" },
{ name: "Niger", code: "NE" },
{ name: "Nigeria", code: "NG" },
{ name: "Niue", code: "NU" },
{ name: "Norfolk Island", code: "NF" },
{ name: "Northern Mariana Islands", code: "MP" },
{ name: "Norway", code: "NO" },
{ name: "Oman", code: "OM" },
{ name: "Pakistan", code: "PK" },
{ name: "Palau", code: "PW" },
{ name: "Palestinian Territory, Occupied", code: "PS" },
{ name: "Panama", code: "PA" },
{ name: "Papua New Guinea", code: "PG" },
{ name: "Paraguay", code: "PY" },
{ name: "Peru", code: "PE" },
{ name: "Philippines", code: "PH" },
{ name: "Pitcairn", code: "PN" },
{ name: "Poland", code: "PL" },
{ name: "Portugal", code: "PT" },
{ name: "Puerto Rico", code: "PR" },
{ name: "Qatar", code: "QA" },
{ name: "Reunion", code: "RE" },
{ name: "Romania", code: "RO" },
{ name: "Russian Federation", code: "RU" },
{ name: "RWANDA", code: "RW" },
{ name: "Saint Helena", code: "SH" },
{ name: "Saint Kitts and Nevis", code: "KN" },
{ name: "Saint Lucia", code: "LC" },
{ name: "Saint Pierre and Miquelon", code: "PM" },
{ name: "Saint Vincent and the Grenadines", code: "VC" },
{ name: "Samoa", code: "WS" },
{ name: "San Marino", code: "SM" },
{ name: "Sao Tome and Principe", code: "ST" },
{ name: "Saudi Arabia", code: "SA" },
{ name: "Senegal", code: "SN" },
{ name: "Serbia and Montenegro", code: "CS" },
{ name: "Seychelles", code: "SC" },
{ name: "Sierra Leone", code: "SL" },
{ name: "Singapore", code: "SG" },
{ name: "Slovakia", code: "SK" },
{ name: "Slovenia", code: "SI" },
{ name: "Solomon Islands", code: "SB" },
{ name: "Somalia", code: "SO" },
{ name: "South Africa", code: "ZA" },
{ name: "South Georgia and the South Sandwich Islands", code: "GS" },
{ name: "Spain", code: "ES" },
{ name: "Sri Lanka", code: "LK" },
{ name: "Sudan", code: "SD" },
{ name: "Suriname", code: "SR" },
{ name: "Svalbard and Jan Mayen", code: "SJ" },
{ name: "Swaziland", code: "SZ" },
{ name: "Sweden", code: "SE" },
{ name: "Switzerland", code: "CH" },
{ name: "Syrian Arab Republic", code: "SY" },
{ name: "Taiwan, Province of China", code: "TW" },
{ name: "Tajikistan", code: "TJ" },
{ name: "Tanzania, United Republic of", code: "TZ" },
{ name: "Thailand", code: "TH" },
{ name: "Timor-Leste", code: "TL" },
{ name: "Togo", code: "TG" },
{ name: "Tokelau", code: "TK" },
{ name: "Tonga", code: "TO" },
{ name: "Trinidad and Tobago", code: "TT" },
{ name: "Tunisia", code: "TN" },
{ name: "Turkey", code: "TR" },
{ name: "Turkmenistan", code: "TM" },
{ name: "Turks and Caicos Islands", code: "TC" },
{ name: "Tuvalu", code: "TV" },
{ name: "Uganda", code: "UG" },
{ name: "Ukraine", code: "UA" },
{ name: "United Arab Emirates", code: "AE" },
{ name: "United Kingdom", code: "GB" },
{ name: "United States", code: "US" },
{ name: "United States Minor Outlying Islands", code: "UM" },
{ name: "Uruguay", code: "UY" },
{ name: "Uzbekistan", code: "UZ" },
{ name: "Vanuatu", code: "VU" },
{ name: "Venezuela", code: "VE" },
{ name: "Viet Nam", code: "VN" },
{ name: "Virgin Islands, British", code: "VG" },
{ name: "Virgin Islands, U.S.", code: "VI" },
{ name: "Wallis and Futuna", code: "WF" },
{ name: "Western Sahara", code: "EH" },
{ name: "Yemen", code: "YE" },
{ name: "Zambia", code: "ZM" },
{ name: "Zimbabwe", code: "ZW" }
]; ];
export const tabPageProps: TabPageProps = { export const tabPageProps: TabPageProps = {

View file

@ -1,20 +1,30 @@
import throttle from "lodash-es/throttle"; import throttle from "lodash-es/throttle";
import { MutableRefObject, useEffect, useState } from "react"; import { MutableRefObject, useEffect, useState } from "react";
function getPosition(anchor?: HTMLElement) { export type Position = Record<"x" | "y", number>;
function getPosition(anchor?: HTMLElement): Position {
if (!!anchor) { if (!!anchor) {
return { return {
x: anchor.scrollLeft, x: anchor.scrollLeft,
y: anchor.scrollTop y: anchor.scrollTop
}; };
} }
return { return undefined;
x: 0,
y: 0
};
} }
function useElementScroll(anchor: MutableRefObject<HTMLElement>) { export function isScrolledToBottom(
anchor: MutableRefObject<HTMLElement>,
position: Position,
offset: number = 0
) {
return !!anchor.current && position
? position.y + anchor.current.clientHeight + offset >=
anchor.current.scrollHeight
: undefined;
}
function useElementScroll(anchor: MutableRefObject<HTMLElement>): Position {
const [scroll, setScroll] = useState(getPosition(anchor.current)); const [scroll, setScroll] = useState(getPosition(anchor.current));
useEffect(() => { useEffect(() => {
@ -29,6 +39,10 @@ function useElementScroll(anchor: MutableRefObject<HTMLElement>) {
} }
}, [anchor.current]); }, [anchor.current]);
useEffect(() => {
setTimeout(() => setScroll(getPosition(anchor.current)), 100);
}, []);
return scroll; return scroll;
} }
export default useElementScroll; export default useElementScroll;

View file

@ -14,9 +14,9 @@ import ConfirmButton, {
ConfirmButtonTransitionState ConfirmButtonTransitionState
} from "@saleor/components/ConfirmButton"; } from "@saleor/components/ConfirmButton";
import FormSpacer from "@saleor/components/FormSpacer"; import FormSpacer from "@saleor/components/FormSpacer";
import { SearchCategories_categories_edges_node } from "@saleor/containers/SearchCategories/types/SearchCategories"; import { SearchCategories_search_edges_node } from "@saleor/containers/SearchCategories/types/SearchCategories";
import { SearchCollections_collections_edges_node } from "@saleor/containers/SearchCollections/types/SearchCollections"; import { SearchCollections_search_edges_node } from "@saleor/containers/SearchCollections/types/SearchCollections";
import { SearchPages_pages_edges_node } from "@saleor/containers/SearchPages/types/SearchPages"; import { SearchPages_search_edges_node } from "@saleor/containers/SearchPages/types/SearchPages";
import useModalDialogErrors from "@saleor/hooks/useModalDialogErrors"; import useModalDialogErrors from "@saleor/hooks/useModalDialogErrors";
import useModalDialogOpen from "@saleor/hooks/useModalDialogOpen"; import useModalDialogOpen from "@saleor/hooks/useModalDialogOpen";
import useStateFromProps from "@saleor/hooks/useStateFromProps"; import useStateFromProps from "@saleor/hooks/useStateFromProps";
@ -43,9 +43,9 @@ export interface MenuItemDialogProps {
initialDisplayValue?: string; initialDisplayValue?: string;
loading: boolean; loading: boolean;
open: boolean; open: boolean;
collections: SearchCollections_collections_edges_node[]; collections: SearchCollections_search_edges_node[];
categories: SearchCategories_categories_edges_node[]; categories: SearchCategories_search_edges_node[];
pages: SearchPages_pages_edges_node[]; pages: SearchPages_search_edges_node[];
onClose: () => void; onClose: () => void;
onSubmit: (data: MenuItemDialogFormData) => void; onSubmit: (data: MenuItemDialogFormData) => void;
onQueryChange: (query: string) => void; onQueryChange: (query: string) => void;

View file

@ -111,7 +111,7 @@ const MenuDetails: React.FC<MenuDetailsProps> = ({ id, params }) => {
const categories = maybe( const categories = maybe(
() => () =>
categorySearch.result.data.categories.edges.map( categorySearch.result.data.search.edges.map(
edge => edge.node edge => edge.node
), ),
[] []
@ -119,7 +119,7 @@ const MenuDetails: React.FC<MenuDetailsProps> = ({ id, params }) => {
const collections = maybe( const collections = maybe(
() => () =>
collectionSearch.result.data.collections.edges.map( collectionSearch.result.data.search.edges.map(
edge => edge.node edge => edge.node
), ),
[] []
@ -127,7 +127,7 @@ const MenuDetails: React.FC<MenuDetailsProps> = ({ id, params }) => {
const pages = maybe( const pages = maybe(
() => () =>
pageSearch.result.data.pages.edges.map( pageSearch.result.data.search.edges.map(
edge => edge.node edge => edge.node
), ),
[] []

View file

@ -20,8 +20,9 @@ import SingleAutocompleteSelectField from "@saleor/components/SingleAutocomplete
import Skeleton from "@saleor/components/Skeleton"; import Skeleton from "@saleor/components/Skeleton";
import useStateFromProps from "@saleor/hooks/useStateFromProps"; import useStateFromProps from "@saleor/hooks/useStateFromProps";
import { buttonMessages } from "@saleor/intl"; import { buttonMessages } from "@saleor/intl";
import { FetchMoreProps } from "@saleor/types";
import createSingleAutocompleteSelectHandler from "@saleor/utils/handlers/singleAutocompleteSelectChangeHandler"; import createSingleAutocompleteSelectHandler from "@saleor/utils/handlers/singleAutocompleteSelectChangeHandler";
import { SearchCustomers_customers_edges_node } from "../../../containers/SearchCustomers/types/SearchCustomers"; import { SearchCustomers_search_edges_node } from "../../../containers/SearchCustomers/types/SearchCustomers";
import { customerUrl } from "../../../customers/urls"; import { customerUrl } from "../../../customers/urls";
import { createHref, maybe } from "../../../misc"; import { createHref, maybe } from "../../../misc";
import { OrderDetails_order } from "../../types/OrderDetails"; import { OrderDetails_order } from "../../types/OrderDetails";
@ -48,9 +49,9 @@ const styles = (theme: Theme) =>
} }
}); });
export interface OrderCustomerProps extends WithStyles<typeof styles> { export interface OrderCustomerProps extends Partial<FetchMoreProps> {
order: OrderDetails_order; order: OrderDetails_order;
users?: SearchCustomers_customers_edges_node[]; users?: SearchCustomers_search_edges_node[];
loading?: boolean; loading?: boolean;
canEditAddresses: boolean; canEditAddresses: boolean;
canEditCustomer: boolean; canEditCustomer: boolean;
@ -67,14 +68,16 @@ const OrderCustomer = withStyles(styles, { name: "OrderCustomer" })(
canEditAddresses, canEditAddresses,
canEditCustomer, canEditCustomer,
fetchUsers, fetchUsers,
hasMore: hasMoreUsers,
loading, loading,
order, order,
users, users,
onCustomerEdit, onCustomerEdit,
onBillingAddressEdit, onBillingAddressEdit,
onFetchMore: onFetchMoreUsers,
onProfileView, onProfileView,
onShippingAddressEdit onShippingAddressEdit
}: OrderCustomerProps) => { }: OrderCustomerProps & WithStyles<typeof styles>) => {
const intl = useIntl(); const intl = useIntl();
const user = maybe(() => order.user); const user = maybe(() => order.user);
@ -138,11 +141,13 @@ const OrderCustomer = withStyles(styles, { name: "OrderCustomer" })(
choices={userChoices} choices={userChoices}
displayValue={userDisplayName} displayValue={userDisplayName}
fetchChoices={fetchUsers} fetchChoices={fetchUsers}
hasMore={hasMoreUsers}
loading={loading} loading={loading}
placeholder={intl.formatMessage({ placeholder={intl.formatMessage({
defaultMessage: "Search Customers" defaultMessage: "Search Customers"
})} })}
onChange={handleUserChange} onChange={handleUserChange}
onFetchMore={onFetchMoreUsers}
name="query" name="query"
value={data.query} value={data.query}
/> />

View file

@ -1,116 +0,0 @@
import Button from "@material-ui/core/Button";
import Dialog from "@material-ui/core/Dialog";
import DialogActions from "@material-ui/core/DialogActions";
import DialogContent from "@material-ui/core/DialogContent";
import DialogTitle from "@material-ui/core/DialogTitle";
import {
createStyles,
Theme,
withStyles,
WithStyles
} from "@material-ui/core/styles";
import React from "react";
import { FormattedMessage } from "react-intl";
import ConfirmButton, {
ConfirmButtonTransitionState
} from "@saleor/components/ConfirmButton";
import { SingleAutocompleteSelectField } from "@saleor/components/SingleAutocompleteSelectField";
import { buttonMessages } from "@saleor/intl";
const styles = (theme: Theme) =>
createStyles({
dialog: {
overflowY: "visible"
},
root: {
overflowY: "visible",
width: theme.breakpoints.values.sm
},
select: {
flex: 1,
marginRight: theme.spacing.unit * 2
},
textRight: {
textAlign: "right"
}
});
interface OrderCustomerEditDialogProps extends WithStyles<typeof styles> {
confirmButtonState: ConfirmButtonTransitionState;
open: boolean;
user: string;
userDisplayValue: string;
users?: Array<{
id: string;
email: string;
}>;
loading?: boolean;
fetchUsers(value: string);
onChange(event: React.ChangeEvent<any>);
onClose?();
onConfirm?(event: React.FormEvent<any>);
}
const OrderCustomerEditDialog = withStyles(styles, {
name: "OrderCustomerEditDialog"
})(
({
classes,
confirmButtonState,
open,
loading,
user,
userDisplayValue,
users,
fetchUsers,
onChange,
onClose,
onConfirm
}: OrderCustomerEditDialogProps) => {
const choices =
!loading && users
? users.map(v => ({
label: v.email,
value: v.id
}))
: [];
return (
<Dialog onClose={onClose} open={open} classes={{ paper: classes.dialog }}>
<DialogTitle>
<FormattedMessage
defaultMessage="Edit Customer Details"
description="dialog header"
/>
</DialogTitle>
<DialogContent className={classes.root}>
<SingleAutocompleteSelectField
choices={choices}
allowCustomValues
loading={loading}
displayValue={userDisplayValue}
name="user"
value={user}
fetchChoices={fetchUsers}
onChange={onChange}
/>
</DialogContent>
<DialogActions>
<Button onClick={onClose}>
<FormattedMessage {...buttonMessages.cancel} />
</Button>
<ConfirmButton
transitionState={confirmButtonState}
color="primary"
variant="contained"
onClick={onConfirm}
>
<FormattedMessage {...buttonMessages.confirm} />
</ConfirmButton>
</DialogActions>
</Dialog>
);
}
);
OrderCustomerEditDialog.displayName = "OrderCustomerEditDialog";
export default OrderCustomerEditDialog;

View file

@ -1,2 +0,0 @@
export { default } from "./OrderCustomerEditDialog";
export * from "./OrderCustomerEditDialog";

View file

@ -18,7 +18,8 @@ import PageHeader from "@saleor/components/PageHeader";
import SaveButtonBar from "@saleor/components/SaveButtonBar"; import SaveButtonBar from "@saleor/components/SaveButtonBar";
import Skeleton from "@saleor/components/Skeleton"; import Skeleton from "@saleor/components/Skeleton";
import { sectionNames } from "@saleor/intl"; import { sectionNames } from "@saleor/intl";
import { SearchCustomers_customers_edges_node } from "../../../containers/SearchCustomers/types/SearchCustomers"; import { FetchMoreProps } from "@saleor/types";
import { SearchCustomers_search_edges_node } from "../../../containers/SearchCustomers/types/SearchCustomers";
import { maybe } from "../../../misc"; import { maybe } from "../../../misc";
import { DraftOrderInput } from "../../../types/globalTypes"; import { DraftOrderInput } from "../../../types/globalTypes";
import { OrderDetails_order } from "../../types/OrderDetails"; import { OrderDetails_order } from "../../types/OrderDetails";
@ -38,10 +39,10 @@ const styles = (theme: Theme) =>
} }
}); });
export interface OrderDraftPageProps extends WithStyles<typeof styles> { export interface OrderDraftPageProps extends FetchMoreProps {
disabled: boolean; disabled: boolean;
order: OrderDetails_order; order: OrderDetails_order;
users: SearchCustomers_customers_edges_node[]; users: SearchCustomers_search_edges_node[];
usersLoading: boolean; usersLoading: boolean;
countries: Array<{ countries: Array<{
code: string; code: string;
@ -72,12 +73,14 @@ const OrderDraftPage = withStyles(styles, { name: "OrderDraftPage" })(
classes, classes,
disabled, disabled,
fetchUsers, fetchUsers,
hasMore,
saveButtonBarState, saveButtonBarState,
onBack, onBack,
onBillingAddressEdit, onBillingAddressEdit,
onCustomerEdit, onCustomerEdit,
onDraftFinalize, onDraftFinalize,
onDraftRemove, onDraftRemove,
onFetchMore,
onNoteAdd, onNoteAdd,
onOrderLineAdd, onOrderLineAdd,
onOrderLineChange, onOrderLineChange,
@ -88,7 +91,7 @@ const OrderDraftPage = withStyles(styles, { name: "OrderDraftPage" })(
order, order,
users, users,
usersLoading usersLoading
}: OrderDraftPageProps) => { }: OrderDraftPageProps & WithStyles<typeof styles>) => {
const intl = useIntl(); const intl = useIntl();
return ( return (
@ -139,14 +142,16 @@ const OrderDraftPage = withStyles(styles, { name: "OrderDraftPage" })(
<OrderCustomer <OrderCustomer
canEditAddresses={true} canEditAddresses={true}
canEditCustomer={true} canEditCustomer={true}
fetchUsers={fetchUsers}
hasMore={hasMore}
loading={usersLoading}
order={order} order={order}
users={users} users={users}
loading={usersLoading}
fetchUsers={fetchUsers}
onBillingAddressEdit={onBillingAddressEdit} onBillingAddressEdit={onBillingAddressEdit}
onCustomerEdit={onCustomerEdit} onCustomerEdit={onCustomerEdit}
onShippingAddressEdit={onShippingAddressEdit} onFetchMore={onFetchMore}
onProfileView={onProfileView} onProfileView={onProfileView}
onShippingAddressEdit={onShippingAddressEdit}
/> />
</div> </div>
</Grid> </Grid>

View file

@ -30,8 +30,8 @@ import { buttonMessages } from "@saleor/intl";
import { maybe, renderCollection } from "@saleor/misc"; import { maybe, renderCollection } from "@saleor/misc";
import { FetchMoreProps } from "@saleor/types"; import { FetchMoreProps } from "@saleor/types";
import { import {
SearchOrderVariant_products_edges_node, SearchOrderVariant_search_edges_node,
SearchOrderVariant_products_edges_node_variants SearchOrderVariant_search_edges_node_variants
} from "../../types/SearchOrderVariant"; } from "../../types/SearchOrderVariant";
const styles = (theme: Theme) => const styles = (theme: Theme) =>
@ -79,21 +79,21 @@ const styles = (theme: Theme) =>
}); });
type SetVariantsAction = ( type SetVariantsAction = (
data: SearchOrderVariant_products_edges_node_variants[] data: SearchOrderVariant_search_edges_node_variants[]
) => void; ) => void;
interface OrderProductAddDialogProps extends FetchMoreProps { interface OrderProductAddDialogProps extends FetchMoreProps {
confirmButtonState: ConfirmButtonTransitionState; confirmButtonState: ConfirmButtonTransitionState;
open: boolean; open: boolean;
products: SearchOrderVariant_products_edges_node[]; products: SearchOrderVariant_search_edges_node[];
onClose: () => void; onClose: () => void;
onFetch: (query: string) => void; onFetch: (query: string) => void;
onSubmit: (data: SearchOrderVariant_products_edges_node_variants[]) => void; onSubmit: (data: SearchOrderVariant_search_edges_node_variants[]) => void;
} }
function hasAllVariantsSelected( function hasAllVariantsSelected(
productVariants: SearchOrderVariant_products_edges_node_variants[], productVariants: SearchOrderVariant_search_edges_node_variants[],
selectedVariantsToProductsMap: SearchOrderVariant_products_edges_node_variants[] selectedVariantsToProductsMap: SearchOrderVariant_search_edges_node_variants[]
): boolean { ): boolean {
return productVariants.reduce( return productVariants.reduce(
(acc, productVariant) => (acc, productVariant) =>
@ -106,8 +106,8 @@ function hasAllVariantsSelected(
} }
function isVariantSelected( function isVariantSelected(
variant: SearchOrderVariant_products_edges_node_variants, variant: SearchOrderVariant_search_edges_node_variants,
selectedVariantsToProductsMap: SearchOrderVariant_products_edges_node_variants[] selectedVariantsToProductsMap: SearchOrderVariant_search_edges_node_variants[]
): boolean { ): boolean {
return !!selectedVariantsToProductsMap.find( return !!selectedVariantsToProductsMap.find(
selectedVariant => selectedVariant.id === variant.id selectedVariant => selectedVariant.id === variant.id
@ -115,10 +115,10 @@ function isVariantSelected(
} }
const onProductAdd = ( const onProductAdd = (
product: SearchOrderVariant_products_edges_node, product: SearchOrderVariant_search_edges_node,
productIndex: number, productIndex: number,
productsWithAllVariantsSelected: boolean[], productsWithAllVariantsSelected: boolean[],
variants: SearchOrderVariant_products_edges_node_variants[], variants: SearchOrderVariant_search_edges_node_variants[],
setVariants: SetVariantsAction setVariants: SetVariantsAction
) => ) =>
productsWithAllVariantsSelected[productIndex] productsWithAllVariantsSelected[productIndex]
@ -141,10 +141,10 @@ const onProductAdd = (
]); ]);
const onVariantAdd = ( const onVariantAdd = (
variant: SearchOrderVariant_products_edges_node_variants, variant: SearchOrderVariant_search_edges_node_variants,
variantIndex: number, variantIndex: number,
productIndex: number, productIndex: number,
variants: SearchOrderVariant_products_edges_node_variants[], variants: SearchOrderVariant_search_edges_node_variants[],
selectedVariantsToProductsMap: boolean[][], selectedVariantsToProductsMap: boolean[][],
setVariants: SetVariantsAction setVariants: SetVariantsAction
) => ) =>
@ -172,7 +172,7 @@ const OrderProductAddDialog = withStyles(styles, {
const intl = useIntl(); const intl = useIntl();
const [query, onQueryChange] = useSearchQuery(onFetch); const [query, onQueryChange] = useSearchQuery(onFetch);
const [variants, setVariants] = React.useState< const [variants, setVariants] = React.useState<
SearchOrderVariant_products_edges_node_variants[] SearchOrderVariant_search_edges_node_variants[]
>([]); >([]);
const selectedVariantsToProductsMap = products const selectedVariantsToProductsMap = products

View file

@ -1,5 +1,5 @@
import { MessageDescriptor } from "react-intl"; import { MessageDescriptor } from "react-intl";
import { SearchCustomers_customers_edges_node } from "../containers/SearchCustomers/types/SearchCustomers"; import { SearchCustomers_search_edges_node } from "../containers/SearchCustomers/types/SearchCustomers";
import { transformOrderStatus, transformPaymentStatus } from "../misc"; import { transformOrderStatus, transformPaymentStatus } from "../misc";
import { import {
FulfillmentStatus, FulfillmentStatus,
@ -11,7 +11,7 @@ import {
import { OrderDetails_order } from "./types/OrderDetails"; import { OrderDetails_order } from "./types/OrderDetails";
import { OrderList_orders_edges_node } from "./types/OrderList"; import { OrderList_orders_edges_node } from "./types/OrderList";
export const clients: SearchCustomers_customers_edges_node[] = [ export const clients: SearchCustomers_search_edges_node[] = [
{ {
__typename: "User" as "User", __typename: "User" as "User",
email: "test.client1@example.com", email: "test.client1@example.com",

View file

@ -1,6 +1,6 @@
import gql from "graphql-tag"; import gql from "graphql-tag";
import BaseSearch from "../containers/BaseSearch"; import TopLevelSearch from "../containers/TopLevelSearch";
import { TypedQuery } from "../queries"; import { TypedQuery } from "../queries";
import { OrderDetails, OrderDetailsVariables } from "./types/OrderDetails"; import { OrderDetails, OrderDetailsVariables } from "./types/OrderDetails";
import { import {
@ -286,7 +286,7 @@ export const TypedOrderDetailsQuery = TypedQuery<
export const searchOrderVariant = gql` export const searchOrderVariant = gql`
query SearchOrderVariant($first: Int!, $query: String!, $after: String) { query SearchOrderVariant($first: Int!, $query: String!, $after: String) {
products(query: $query, first: $first, after: $after) { search: products(query: $query, first: $first, after: $after) {
edges { edges {
node { node {
id id
@ -314,7 +314,7 @@ export const searchOrderVariant = gql`
} }
} }
`; `;
export const SearchOrderVariant = BaseSearch< export const SearchOrderVariant = TopLevelSearch<
SearchOrderVariantType, SearchOrderVariantType,
SearchOrderVariantVariables SearchOrderVariantVariables
>(searchOrderVariant); >(searchOrderVariant);

View file

@ -6,39 +6,39 @@
// GraphQL query operation: SearchOrderVariant // GraphQL query operation: SearchOrderVariant
// ==================================================== // ====================================================
export interface SearchOrderVariant_products_edges_node_thumbnail { export interface SearchOrderVariant_search_edges_node_thumbnail {
__typename: "Image"; __typename: "Image";
url: string; url: string;
} }
export interface SearchOrderVariant_products_edges_node_variants_price { export interface SearchOrderVariant_search_edges_node_variants_price {
__typename: "Money"; __typename: "Money";
amount: number; amount: number;
currency: string; currency: string;
} }
export interface SearchOrderVariant_products_edges_node_variants { export interface SearchOrderVariant_search_edges_node_variants {
__typename: "ProductVariant"; __typename: "ProductVariant";
id: string; id: string;
name: string; name: string;
sku: string; sku: string;
price: SearchOrderVariant_products_edges_node_variants_price | null; price: SearchOrderVariant_search_edges_node_variants_price | null;
} }
export interface SearchOrderVariant_products_edges_node { export interface SearchOrderVariant_search_edges_node {
__typename: "Product"; __typename: "Product";
id: string; id: string;
name: string; name: string;
thumbnail: SearchOrderVariant_products_edges_node_thumbnail | null; thumbnail: SearchOrderVariant_search_edges_node_thumbnail | null;
variants: (SearchOrderVariant_products_edges_node_variants | null)[] | null; variants: (SearchOrderVariant_search_edges_node_variants | null)[] | null;
} }
export interface SearchOrderVariant_products_edges { export interface SearchOrderVariant_search_edges {
__typename: "ProductCountableEdge"; __typename: "ProductCountableEdge";
node: SearchOrderVariant_products_edges_node; node: SearchOrderVariant_search_edges_node;
} }
export interface SearchOrderVariant_products_pageInfo { export interface SearchOrderVariant_search_pageInfo {
__typename: "PageInfo"; __typename: "PageInfo";
endCursor: string | null; endCursor: string | null;
hasNextPage: boolean; hasNextPage: boolean;
@ -46,14 +46,14 @@ export interface SearchOrderVariant_products_pageInfo {
startCursor: string | null; startCursor: string | null;
} }
export interface SearchOrderVariant_products { export interface SearchOrderVariant_search {
__typename: "ProductCountableConnection"; __typename: "ProductCountableConnection";
edges: SearchOrderVariant_products_edges[]; edges: SearchOrderVariant_search_edges[];
pageInfo: SearchOrderVariant_products_pageInfo; pageInfo: SearchOrderVariant_search_pageInfo;
} }
export interface SearchOrderVariant { export interface SearchOrderVariant {
products: SearchOrderVariant_products | null; search: SearchOrderVariant_search | null;
} }
export interface SearchOrderVariantVariables { export interface SearchOrderVariantVariables {

View file

@ -93,7 +93,11 @@ export const OrderDetails: React.StatelessComponent<OrderDetailsProps> = ({
); );
return ( return (
<SearchCustomers variables={DEFAULT_INITIAL_SEARCH_DATA}> <SearchCustomers variables={DEFAULT_INITIAL_SEARCH_DATA}>
{({ search: searchUsers, result: users }) => ( {({
loadMore: loadMoreCustomers,
search: searchUsers,
result: users
}) => (
<OrderDetailsMessages> <OrderDetailsMessages>
{orderMessages => ( {orderMessages => (
<OrderOperations <OrderOperations
@ -400,12 +404,18 @@ export const OrderDetails: React.StatelessComponent<OrderDetailsProps> = ({
} }
users={maybe( users={maybe(
() => () =>
users.data.customers.edges.map( users.data.search.edges.map(
edge => edge.node edge => edge.node
), ),
[] []
)} )}
hasMore={maybe(
() => users.data.search.pageInfo.hasNextPage,
false
)}
onFetchMore={loadMoreCustomers}
fetchUsers={searchUsers} fetchUsers={searchUsers}
loading={users.loading}
usersLoading={users.loading} usersLoading={users.loading}
onCustomerEdit={data => onCustomerEdit={data =>
orderDraftUpdate.mutate({ orderDraftUpdate.mutate({
@ -511,74 +521,46 @@ export const OrderDetails: React.StatelessComponent<OrderDetailsProps> = ({
variables={DEFAULT_INITIAL_SEARCH_DATA} variables={DEFAULT_INITIAL_SEARCH_DATA}
> >
{({ {({
loadMore,
search: variantSearch, search: variantSearch,
result: variantSearchOpts result: variantSearchOpts
}) => { }) => (
const fetchMore = () => <OrderProductAddDialog
variantSearchOpts.loadMore( confirmButtonState={getMutationState(
(prev, next) => { orderLinesAdd.opts.called,
if ( orderLinesAdd.opts.loading,
prev.products.pageInfo.endCursor === maybe(
next.products.pageInfo.endCursor
) {
return prev;
}
return {
...prev,
products: {
...prev.products,
edges: [
...prev.products.edges,
...next.products.edges
],
pageInfo: next.products.pageInfo
}
};
},
{
after:
variantSearchOpts.data.products
.pageInfo.endCursor
}
);
return (
<OrderProductAddDialog
confirmButtonState={getMutationState(
orderLinesAdd.opts.called,
orderLinesAdd.opts.loading,
maybe(
() =>
orderLinesAdd.opts.data
.draftOrderLinesCreate.errors
)
)}
loading={variantSearchOpts.loading}
open={params.action === "add-order-line"}
hasMore={maybe(
() => () =>
variantSearchOpts.data.products orderLinesAdd.opts.data
.pageInfo.hasNextPage .draftOrderLinesCreate.errors
)} )
products={maybe(() => )}
variantSearchOpts.data.products.edges.map( loading={variantSearchOpts.loading}
edge => edge.node open={params.action === "add-order-line"}
) hasMore={maybe(
)} () =>
onClose={closeModal} variantSearchOpts.data.search.pageInfo
onFetch={variantSearch} .hasNextPage
onFetchMore={fetchMore} )}
onSubmit={variants => products={maybe(() =>
orderLinesAdd.mutate({ variantSearchOpts.data.search.edges.map(
id, edge => edge.node
input: variants.map(variant => ({ )
quantity: 1, )}
variantId: variant.id onClose={closeModal}
})) onFetch={variantSearch}
}) onFetchMore={loadMore}
} onSubmit={variants =>
/> orderLinesAdd.mutate({
); id,
}} input: variants.map(variant => ({
quantity: 1,
variantId: variant.id
}))
})
}
/>
)}
</SearchOrderVariant> </SearchOrderVariant>
</> </>
)} )}

View file

@ -38,5 +38,25 @@ export const searchAttributes = gql`
`; `;
export default BaseSearch<SearchAttributes, SearchAttributesVariables>( export default BaseSearch<SearchAttributes, SearchAttributesVariables>(
searchAttributes searchAttributes,
result =>
result.loadMore(
(prev, next) => ({
...prev,
productType: {
...prev.productType,
availableAttributes: {
...prev.productType.availableAttributes,
edges: [
...prev.productType.availableAttributes.edges,
...next.productType.availableAttributes.edges
],
pageInfo: next.productType.availableAttributes.pageInfo
}
}
}),
{
after: result.data.productType.availableAttributes.pageInfo.endCursor
}
)
); );

View file

@ -1,12 +1,12 @@
import { import {
SearchProductTypes_productTypes_edges_node, SearchProductTypes_search_edges_node,
SearchProductTypes_productTypes_edges_node_productAttributes SearchProductTypes_search_edges_node_productAttributes
} from "@saleor/containers/SearchProductTypes/types/SearchProductTypes"; } from "@saleor/containers/SearchProductTypes/types/SearchProductTypes";
import { AttributeInputTypeEnum } from "../types/globalTypes"; import { AttributeInputTypeEnum } from "../types/globalTypes";
import { ProductTypeDetails_productType } from "./types/ProductTypeDetails"; import { ProductTypeDetails_productType } from "./types/ProductTypeDetails";
import { ProductTypeList_productTypes_edges_node } from "./types/ProductTypeList"; import { ProductTypeList_productTypes_edges_node } from "./types/ProductTypeList";
export const attributes: SearchProductTypes_productTypes_edges_node_productAttributes[] = [ export const attributes: SearchProductTypes_search_edges_node_productAttributes[] = [
{ {
node: { node: {
__typename: "Attribute" as "Attribute", __typename: "Attribute" as "Attribute",
@ -469,8 +469,7 @@ export const attributes: SearchProductTypes_productTypes_edges_node_productAttri
].map(edge => edge.node); ].map(edge => edge.node);
export const productTypes: Array< export const productTypes: Array<
SearchProductTypes_productTypes_edges_node & SearchProductTypes_search_edges_node & ProductTypeList_productTypes_edges_node
ProductTypeList_productTypes_edges_node
> = [ > = [
{ {
__typename: "ProductType" as "ProductType", __typename: "ProductType" as "ProductType",

View file

@ -13,9 +13,9 @@ import PageHeader from "@saleor/components/PageHeader";
import SaveButtonBar from "@saleor/components/SaveButtonBar"; import SaveButtonBar from "@saleor/components/SaveButtonBar";
import SeoForm from "@saleor/components/SeoForm"; import SeoForm from "@saleor/components/SeoForm";
import VisibilityCard from "@saleor/components/VisibilityCard"; import VisibilityCard from "@saleor/components/VisibilityCard";
import { SearchCategories_categories_edges_node } from "@saleor/containers/SearchCategories/types/SearchCategories"; import { SearchCategories_search_edges_node } from "@saleor/containers/SearchCategories/types/SearchCategories";
import { SearchCollections_collections_edges_node } from "@saleor/containers/SearchCollections/types/SearchCollections"; import { SearchCollections_search_edges_node } from "@saleor/containers/SearchCollections/types/SearchCollections";
import { SearchProductTypes_productTypes_edges_node_productAttributes } from "@saleor/containers/SearchProductTypes/types/SearchProductTypes"; import { SearchProductTypes_search_edges_node_productAttributes } from "@saleor/containers/SearchProductTypes/types/SearchProductTypes";
import useDateLocalize from "@saleor/hooks/useDateLocalize"; import useDateLocalize from "@saleor/hooks/useDateLocalize";
import useFormset from "@saleor/hooks/useFormset"; import useFormset from "@saleor/hooks/useFormset";
import useStateFromProps from "@saleor/hooks/useStateFromProps"; import useStateFromProps from "@saleor/hooks/useStateFromProps";
@ -27,7 +27,7 @@ import {
} from "@saleor/products/utils/data"; } from "@saleor/products/utils/data";
import createMultiAutocompleteSelectHandler from "@saleor/utils/handlers/multiAutocompleteSelectChangeHandler"; import createMultiAutocompleteSelectHandler from "@saleor/utils/handlers/multiAutocompleteSelectChangeHandler";
import createSingleAutocompleteSelectHandler from "@saleor/utils/handlers/singleAutocompleteSelectChangeHandler"; import createSingleAutocompleteSelectHandler from "@saleor/utils/handlers/singleAutocompleteSelectChangeHandler";
import { UserError } from "../../../types"; import { FetchMoreProps, UserError } from "../../../types";
import { import {
createAttributeChangeHandler, createAttributeChangeHandler,
createAttributeMultiChangeHandler, createAttributeMultiChangeHandler,
@ -63,15 +63,18 @@ export interface ProductCreatePageSubmitData extends FormData {
interface ProductCreatePageProps { interface ProductCreatePageProps {
errors: UserError[]; errors: UserError[];
collections: SearchCollections_collections_edges_node[]; collections: SearchCollections_search_edges_node[];
categories: SearchCategories_categories_edges_node[]; categories: SearchCategories_search_edges_node[];
currency: string; currency: string;
disabled: boolean; disabled: boolean;
fetchMoreCategories: FetchMoreProps;
fetchMoreCollections: FetchMoreProps;
fetchMoreProductTypes: FetchMoreProps;
productTypes?: Array<{ productTypes?: Array<{
id: string; id: string;
name: string; name: string;
hasVariants: boolean; hasVariants: boolean;
productAttributes: SearchProductTypes_productTypes_edges_node_productAttributes[]; productAttributes: SearchProductTypes_search_edges_node_productAttributes[];
}>; }>;
header: string; header: string;
saveButtonBarState: ConfirmButtonTransitionState; saveButtonBarState: ConfirmButtonTransitionState;
@ -92,6 +95,9 @@ export const ProductCreatePage: React.StatelessComponent<
errors: userErrors, errors: userErrors,
fetchCategories, fetchCategories,
fetchCollections, fetchCollections,
fetchMoreCategories,
fetchMoreCollections,
fetchMoreProductTypes,
header, header,
productTypes: productTypeChoiceList, productTypes: productTypeChoiceList,
saveButtonBarState, saveButtonBarState,
@ -271,6 +277,9 @@ export const ProductCreatePage: React.StatelessComponent<
errors={errors} errors={errors}
fetchCategories={fetchCategories} fetchCategories={fetchCategories}
fetchCollections={fetchCollections} fetchCollections={fetchCollections}
fetchMoreCategories={fetchMoreCategories}
fetchMoreCollections={fetchMoreCollections}
fetchMoreProductTypes={fetchMoreProductTypes}
fetchProductTypes={fetchProductTypes} fetchProductTypes={fetchProductTypes}
productType={productType} productType={productType}
productTypeInputDisplayValue={productType.name} productTypeInputDisplayValue={productType.name}

View file

@ -22,7 +22,7 @@ import SingleAutocompleteSelectField, {
} from "@saleor/components/SingleAutocompleteSelectField"; } from "@saleor/components/SingleAutocompleteSelectField";
import { ChangeEvent } from "@saleor/hooks/useForm"; import { ChangeEvent } from "@saleor/hooks/useForm";
import { maybe } from "@saleor/misc"; import { maybe } from "@saleor/misc";
import { FormErrors } from "@saleor/types"; import { FetchMoreProps, FormErrors } from "@saleor/types";
interface ProductType { interface ProductType {
hasVariants: boolean; hasVariants: boolean;
@ -62,6 +62,9 @@ interface ProductOrganizationProps extends WithStyles<typeof styles> {
productTypes?: SingleAutocompleteChoiceType[]; productTypes?: SingleAutocompleteChoiceType[];
fetchCategories: (query: string) => void; fetchCategories: (query: string) => void;
fetchCollections: (query: string) => void; fetchCollections: (query: string) => void;
fetchMoreCategories: FetchMoreProps;
fetchMoreCollections: FetchMoreProps;
fetchMoreProductTypes?: FetchMoreProps;
fetchProductTypes?: (data: string) => void; fetchProductTypes?: (data: string) => void;
onCategoryChange: (event: ChangeEvent) => void; onCategoryChange: (event: ChangeEvent) => void;
onCollectionChange: (event: ChangeEvent) => void; onCollectionChange: (event: ChangeEvent) => void;
@ -81,6 +84,9 @@ const ProductOrganization = withStyles(styles, { name: "ProductOrganization" })(
errors, errors,
fetchCategories, fetchCategories,
fetchCollections, fetchCollections,
fetchMoreCategories,
fetchMoreCollections,
fetchMoreProductTypes,
fetchProductTypes, fetchProductTypes,
productType, productType,
productTypeInputDisplayValue, productTypeInputDisplayValue,
@ -115,6 +121,7 @@ const ProductOrganization = withStyles(styles, { name: "ProductOrganization" })(
onChange={onProductTypeChange} onChange={onProductTypeChange}
fetchChoices={fetchProductTypes} fetchChoices={fetchProductTypes}
data-tc="product-type" data-tc="product-type"
{...fetchMoreProductTypes}
/> />
) : ( ) : (
<> <>
@ -160,6 +167,7 @@ const ProductOrganization = withStyles(styles, { name: "ProductOrganization" })(
onChange={onCategoryChange} onChange={onCategoryChange}
fetchChoices={fetchCategories} fetchChoices={fetchCategories}
data-tc="category" data-tc="category"
{...fetchMoreCategories}
/> />
<FormSpacer /> <FormSpacer />
<Hr /> <Hr />
@ -180,6 +188,7 @@ const ProductOrganization = withStyles(styles, { name: "ProductOrganization" })(
onChange={onCollectionChange} onChange={onCollectionChange}
fetchChoices={fetchCollections} fetchChoices={fetchCollections}
data-tc="collections" data-tc="collections"
{...fetchMoreCollections}
/> />
</CardContent> </CardContent>
</Card> </Card>

View file

@ -12,14 +12,14 @@ import PageHeader from "@saleor/components/PageHeader";
import SaveButtonBar from "@saleor/components/SaveButtonBar"; import SaveButtonBar from "@saleor/components/SaveButtonBar";
import SeoForm from "@saleor/components/SeoForm"; import SeoForm from "@saleor/components/SeoForm";
import VisibilityCard from "@saleor/components/VisibilityCard"; import VisibilityCard from "@saleor/components/VisibilityCard";
import { SearchCategories_categories_edges_node } from "@saleor/containers/SearchCategories/types/SearchCategories"; import { SearchCategories_search_edges_node } from "@saleor/containers/SearchCategories/types/SearchCategories";
import { SearchCollections_collections_edges_node } from "@saleor/containers/SearchCollections/types/SearchCollections"; import { SearchCollections_search_edges_node } from "@saleor/containers/SearchCollections/types/SearchCollections";
import useDateLocalize from "@saleor/hooks/useDateLocalize"; import useDateLocalize from "@saleor/hooks/useDateLocalize";
import useFormset from "@saleor/hooks/useFormset"; import useFormset from "@saleor/hooks/useFormset";
import useStateFromProps from "@saleor/hooks/useStateFromProps"; import useStateFromProps from "@saleor/hooks/useStateFromProps";
import { sectionNames } from "@saleor/intl"; import { sectionNames } from "@saleor/intl";
import { maybe } from "@saleor/misc"; import { maybe } from "@saleor/misc";
import { ListActions, UserError } from "@saleor/types"; import { FetchMoreProps, ListActions, UserError } from "@saleor/types";
import createMultiAutocompleteSelectHandler from "@saleor/utils/handlers/multiAutocompleteSelectChangeHandler"; import createMultiAutocompleteSelectHandler from "@saleor/utils/handlers/multiAutocompleteSelectChangeHandler";
import createSingleAutocompleteSelectHandler from "@saleor/utils/handlers/singleAutocompleteSelectChangeHandler"; import createSingleAutocompleteSelectHandler from "@saleor/utils/handlers/singleAutocompleteSelectChangeHandler";
import { import {
@ -50,9 +50,11 @@ import ProductVariants from "../ProductVariants";
export interface ProductUpdatePageProps extends ListActions { export interface ProductUpdatePageProps extends ListActions {
errors: UserError[]; errors: UserError[];
placeholderImage: string; placeholderImage: string;
collections: SearchCollections_collections_edges_node[]; collections: SearchCollections_search_edges_node[];
categories: SearchCategories_categories_edges_node[]; categories: SearchCategories_search_edges_node[];
disabled: boolean; disabled: boolean;
fetchMoreCategories: FetchMoreProps;
fetchMoreCollections: FetchMoreProps;
variants: ProductDetails_product_variants[]; variants: ProductDetails_product_variants[];
images: ProductDetails_product_images[]; images: ProductDetails_product_images[];
product: ProductDetails_product; product: ProductDetails_product;
@ -86,6 +88,8 @@ export const ProductUpdatePage: React.FC<ProductUpdatePageProps> = ({
errors: userErrors, errors: userErrors,
fetchCategories, fetchCategories,
fetchCollections, fetchCollections,
fetchMoreCategories,
fetchMoreCollections,
images, images,
header, header,
placeholderImage, placeholderImage,
@ -285,6 +289,8 @@ export const ProductUpdatePage: React.FC<ProductUpdatePageProps> = ({
errors={errors} errors={errors}
fetchCategories={fetchCategories} fetchCategories={fetchCategories}
fetchCollections={fetchCollections} fetchCollections={fetchCollections}
fetchMoreCategories={fetchMoreCategories}
fetchMoreCollections={fetchMoreCollections}
productType={maybe(() => product.productType)} productType={maybe(() => product.productType)}
onCategoryChange={handleCategorySelect} onCategoryChange={handleCategorySelect}
onCollectionChange={handleCollectionSelect} onCollectionChange={handleCollectionSelect}

View file

@ -2,7 +2,7 @@ import { RawDraftContentState } from "draft-js";
import { MultiAutocompleteChoiceType } from "@saleor/components/MultiAutocompleteSelectField"; import { MultiAutocompleteChoiceType } from "@saleor/components/MultiAutocompleteSelectField";
import { SingleAutocompleteChoiceType } from "@saleor/components/SingleAutocompleteSelectField"; import { SingleAutocompleteChoiceType } from "@saleor/components/SingleAutocompleteSelectField";
import { SearchProductTypes_productTypes_edges_node_productAttributes } from "@saleor/containers/SearchProductTypes/types/SearchProductTypes"; import { SearchProductTypes_search_edges_node_productAttributes } from "@saleor/containers/SearchProductTypes/types/SearchProductTypes";
import { maybe } from "@saleor/misc"; import { maybe } from "@saleor/misc";
import { import {
ProductDetails_product, ProductDetails_product,
@ -35,7 +35,7 @@ export interface ProductType {
hasVariants: boolean; hasVariants: boolean;
id: string; id: string;
name: string; name: string;
productAttributes: SearchProductTypes_productTypes_edges_node_productAttributes[]; productAttributes: SearchProductTypes_search_edges_node_productAttributes[];
} }
export function getAttributeInputFromProduct( export function getAttributeInputFromProduct(

View file

@ -33,11 +33,20 @@ export const ProductUpdate: React.StatelessComponent<
return ( return (
<SearchCategories variables={DEFAULT_INITIAL_SEARCH_DATA}> <SearchCategories variables={DEFAULT_INITIAL_SEARCH_DATA}>
{({ search: searchCategory, result: searchCategoryOpts }) => ( {({
loadMore: loadMoreCategories,
search: searchCategory,
result: searchCategoryOpts
}) => (
<SearchCollections variables={DEFAULT_INITIAL_SEARCH_DATA}> <SearchCollections variables={DEFAULT_INITIAL_SEARCH_DATA}>
{({ search: searchCollection, result: searchCollectionOpts }) => ( {({
loadMore: loadMoreCollections,
search: searchCollection,
result: searchCollectionOpts
}) => (
<SearchProductTypes variables={DEFAULT_INITIAL_SEARCH_DATA}> <SearchProductTypes variables={DEFAULT_INITIAL_SEARCH_DATA}>
{({ {({
loadMore: loadMoreProductTypes,
search: searchProductTypes, search: searchProductTypes,
result: searchProductTypesOpts result: searchProductTypesOpts
}) => { }) => {
@ -121,11 +130,11 @@ export const ProductUpdate: React.StatelessComponent<
<ProductCreatePage <ProductCreatePage
currency={maybe(() => shop.defaultCurrency)} currency={maybe(() => shop.defaultCurrency)}
categories={maybe( categories={maybe(
() => searchCategoryOpts.data.categories.edges, () => searchCategoryOpts.data.search.edges,
[] []
).map(edge => edge.node)} ).map(edge => edge.node)}
collections={maybe( collections={maybe(
() => searchCollectionOpts.data.collections.edges, () => searchCollectionOpts.data.search.edges,
[] []
).map(edge => edge.node)} ).map(edge => edge.node)}
disabled={productCreateDataLoading} disabled={productCreateDataLoading}
@ -141,13 +150,40 @@ export const ProductUpdate: React.StatelessComponent<
description: "page header" description: "page header"
})} })}
productTypes={maybe(() => productTypes={maybe(() =>
searchProductTypesOpts.data.productTypes.edges.map( searchProductTypesOpts.data.search.edges.map(
edge => edge.node edge => edge.node
) )
)} )}
onBack={handleBack} onBack={handleBack}
onSubmit={handleSubmit} onSubmit={handleSubmit}
saveButtonBarState={formTransitionState} saveButtonBarState={formTransitionState}
fetchMoreCategories={{
hasMore: maybe(
() =>
searchCategoryOpts.data.search.pageInfo
.hasNextPage
),
loading: searchCategoryOpts.loading,
onFetchMore: loadMoreCategories
}}
fetchMoreCollections={{
hasMore: maybe(
() =>
searchCollectionOpts.data.search.pageInfo
.hasNextPage
),
loading: searchCollectionOpts.loading,
onFetchMore: loadMoreCollections
}}
fetchMoreProductTypes={{
hasMore: maybe(
() =>
searchProductTypesOpts.data.search.pageInfo
.hasNextPage
),
loading: searchProductTypesOpts.loading,
onFetchMore: loadMoreProductTypes
}}
/> />
</> </>
); );

View file

@ -68,9 +68,17 @@ export const ProductUpdate: React.StatelessComponent<ProductUpdateProps> = ({
return ( return (
<SearchCategories variables={DEFAULT_INITIAL_SEARCH_DATA}> <SearchCategories variables={DEFAULT_INITIAL_SEARCH_DATA}>
{({ search: searchCategories, result: searchCategoriesOpts }) => ( {({
loadMore: loadMoreCategories,
search: searchCategories,
result: searchCategoriesOpts
}) => (
<SearchCollections variables={DEFAULT_INITIAL_SEARCH_DATA}> <SearchCollections variables={DEFAULT_INITIAL_SEARCH_DATA}>
{({ search: searchCollections, result: searchCollectionsOpts }) => ( {({
loadMore: loadMoreCollections,
search: searchCollections,
result: searchCollectionsOpts
}) => (
<TypedProductDetailsQuery <TypedProductDetailsQuery
displayLoader displayLoader
require={["product"]} require={["product"]}
@ -228,11 +236,11 @@ export const ProductUpdate: React.StatelessComponent<ProductUpdateProps> = ({
); );
const categories = maybe( const categories = maybe(
() => searchCategoriesOpts.data.categories.edges, () => searchCategoriesOpts.data.search.edges,
[] []
).map(edge => edge.node); ).map(edge => edge.node);
const collections = maybe( const collections = maybe(
() => searchCollectionsOpts.data.collections.edges, () => searchCollectionsOpts.data.search.edges,
[] []
).map(edge => edge.node); ).map(edge => edge.node);
const errors = maybe( const errors = maybe(
@ -295,6 +303,24 @@ export const ProductUpdate: React.StatelessComponent<ProductUpdateProps> = ({
selected={listElements.length} selected={listElements.length}
toggle={toggle} toggle={toggle}
toggleAll={toggleAll} toggleAll={toggleAll}
fetchMoreCategories={{
hasMore: maybe(
() =>
searchCategoriesOpts.data.search.pageInfo
.hasNextPage
),
loading: searchCategoriesOpts.loading,
onFetchMore: loadMoreCategories
}}
fetchMoreCollections={{
hasMore: maybe(
() =>
searchCollectionsOpts.data.search.pageInfo
.hasNextPage
),
loading: searchCollectionsOpts.loading,
onFetchMore: loadMoreCollections
}}
/> />
<ActionDialog <ActionDialog
open={params.action === "remove"} open={params.action === "remove"}

View file

@ -15,7 +15,7 @@ import { RequireAtLeastOne } from "./misc";
export interface LoadMore<TData, TVariables> { export interface LoadMore<TData, TVariables> {
loadMore: ( loadMore: (
mergeFunc: (prev: TData, next: TData) => TData, mergeFunc: (prev: TData, next: TData) => TData,
extraVariables: RequireAtLeastOne<TVariables> extraVariables: Partial<TVariables>
) => Promise<ApolloQueryResult<TData>>; ) => Promise<ApolloQueryResult<TData>>;
} }

View file

@ -7,6 +7,7 @@ const CardDecorator = storyFn => (
style={{ style={{
margin: "auto", margin: "auto",
overflow: "visible", overflow: "visible",
position: "relative",
width: 400 width: 400
}} }}
> >

File diff suppressed because it is too large Load diff

View file

@ -29,7 +29,6 @@ function loadStories() {
require("./stories/components/Filter"); require("./stories/components/Filter");
require("./stories/components/Money"); require("./stories/components/Money");
require("./stories/components/MoneyRange"); require("./stories/components/MoneyRange");
require("./stories/components/MultiAutocompleteSelectField");
require("./stories/components/MultiSelectField"); require("./stories/components/MultiSelectField");
require("./stories/components/NotFoundPage"); require("./stories/components/NotFoundPage");
require("./stories/components/PageHeader"); require("./stories/components/PageHeader");
@ -39,7 +38,6 @@ function loadStories() {
require("./stories/components/RichTextEditor"); require("./stories/components/RichTextEditor");
require("./stories/components/SaveButtonBar"); require("./stories/components/SaveButtonBar");
require("./stories/components/SaveFilterTabDialog"); require("./stories/components/SaveFilterTabDialog");
require("./stories/components/SingleAutocompleteSelectField");
require("./stories/components/SingleSelectField"); require("./stories/components/SingleSelectField");
require("./stories/components/Skeleton"); require("./stories/components/Skeleton");
require("./stories/components/StatusLabel"); require("./stories/components/StatusLabel");
@ -125,7 +123,6 @@ function loadStories() {
require("./stories/orders/OrderBulkCancelDialog"); require("./stories/orders/OrderBulkCancelDialog");
require("./stories/orders/OrderCancelDialog"); require("./stories/orders/OrderCancelDialog");
require("./stories/orders/OrderCustomer"); require("./stories/orders/OrderCustomer");
require("./stories/orders/OrderCustomerEditDialog");
require("./stories/orders/OrderDetailsPage"); require("./stories/orders/OrderDetailsPage");
require("./stories/orders/OrderDraftCancelDialog"); require("./stories/orders/OrderDraftCancelDialog");
require("./stories/orders/OrderDraftFinalizeDialog"); require("./stories/orders/OrderDraftFinalizeDialog");

View file

@ -1,38 +1,41 @@
import React from "react"; import React from "react";
import { SingleAutocompleteChoiceType } from "@saleor/components/SingleAutocompleteSelectField/SingleAutocompleteSelectFieldContent";
interface ChoiceProviderProps { interface ChoiceProviderProps {
children: (( children: (props: {
props: { choices: SingleAutocompleteChoiceType[];
choices: Array<{ hasMore: boolean;
label: string; loading: boolean;
value: string; fetchChoices: (value: string) => void;
}>; fetchMore: () => void;
loading: boolean; }) => React.ReactElement<any>;
fetchChoices(value: string); choices: SingleAutocompleteChoiceType[];
}
) => React.ReactElement<any>);
choices: Array<{
label: string;
value: string;
}>;
} }
interface ChoiceProviderState { interface ChoiceProviderState {
choices: Array<{ choices: SingleAutocompleteChoiceType[];
label: string; filteredChoices: SingleAutocompleteChoiceType[];
value: string; first: number;
}>;
loading: boolean; loading: boolean;
timeout: any; timeout: any;
} }
const step = 5;
export class ChoiceProvider extends React.Component< export class ChoiceProvider extends React.Component<
ChoiceProviderProps, ChoiceProviderProps,
ChoiceProviderState ChoiceProviderState
> { > {
state = { choices: [], loading: false, timeout: null }; state = {
choices: [],
filteredChoices: [],
first: step,
loading: false,
timeout: null
};
handleChange = inputValue => { handleChange = (inputValue: string) => {
if (this.state.loading) { if (!!this.state.timeout) {
clearTimeout(this.state.timeout); clearTimeout(this.state.timeout);
} }
const timeout = setTimeout(() => this.fetchChoices(inputValue), 500); const timeout = setTimeout(() => this.fetchChoices(inputValue), 500);
@ -42,22 +45,35 @@ export class ChoiceProvider extends React.Component<
}); });
}; };
fetchChoices = inputValue => { handleFetchMore = () => {
let count = 0; if (!!this.state.timeout) {
clearTimeout(this.state.timeout);
}
const timeout = setTimeout(this.fetchMore, 500);
this.setState({ this.setState({
choices: this.props.choices.filter(suggestion => { loading: true,
const keep = timeout
(!inputValue || });
suggestion.label.toLowerCase().indexOf(inputValue.toLowerCase()) !== };
-1) &&
count < 5;
if (keep) { fetchMore = () =>
count += 1; this.setState(prevState => ({
} filteredChoices: prevState.choices.slice(0, prevState.first + step),
first: prevState.first + step,
loading: false,
timeout: null
}));
return keep; fetchChoices = (inputValue: string) => {
}), const choices = this.props.choices.filter(
suggestion =>
!inputValue ||
suggestion.label.toLowerCase().indexOf(inputValue.toLowerCase()) !== -1
);
this.setState({
choices,
filteredChoices: choices.slice(0, step),
first: step,
loading: false, loading: false,
timeout: null timeout: null
}); });
@ -65,8 +81,10 @@ export class ChoiceProvider extends React.Component<
render() { render() {
return this.props.children({ return this.props.children({
choices: this.state.choices, choices: this.state.filteredChoices,
fetchChoices: this.handleChange, fetchChoices: this.handleChange,
fetchMore: this.handleFetchMore,
hasMore: this.state.choices.length > this.state.filteredChoices.length,
loading: this.state.loading loading: this.state.loading
}); });
} }

View file

@ -1,69 +0,0 @@
import { storiesOf } from "@storybook/react";
import React from "react";
import MultiAutocompleteSelectField, {
MultiAutocompleteSelectFieldProps
} from "@saleor/components/MultiAutocompleteSelectField";
import useMultiAutocomplete from "@saleor/hooks/useMultiAutocomplete";
import CardDecorator from "../../CardDecorator";
import Decorator from "../../Decorator";
import { ChoiceProvider } from "../../mock";
const suggestions = [
"Afghanistan",
"Burundi",
"Comoros",
"Egypt",
"Equatorial Guinea",
"Greenland",
"Isle of Man",
"Israel",
"Italy",
"United States",
"Wallis and Futuna",
"Zimbabwe"
].map(c => ({ label: c, value: c.toLocaleLowerCase().replace(/\s+/, "_") }));
const props: MultiAutocompleteSelectFieldProps = {
choices: undefined,
displayValues: [],
label: "Country",
loading: false,
name: "country",
onChange: () => undefined,
placeholder: "Select country",
value: undefined
};
const Story: React.FC<
Partial<MultiAutocompleteSelectFieldProps>
> = storyProps => {
const { change, data: countries } = useMultiAutocomplete([suggestions[0]]);
return (
<ChoiceProvider choices={suggestions}>
{({ choices, loading, fetchChoices }) => (
<MultiAutocompleteSelectField
{...props}
displayValues={countries}
choices={choices}
fetchChoices={fetchChoices}
helperText={`Value: ${countries
.map(country => country.label)
.join(", ")}`}
onChange={event => change(event, choices)}
value={countries.map(country => country.value)}
loading={loading}
{...storyProps}
/>
)}
</ChoiceProvider>
);
};
storiesOf("Generics / MultiAutocompleteSelectField", module)
.addDecorator(CardDecorator)
.addDecorator(Decorator)
.add("with loaded data", () => <Story />)
.add("with loading data", () => <Story loading={true} />)
.add("with custom option", () => <Story allowCustomValues={true} />);

View file

@ -1,81 +0,0 @@
import { storiesOf } from "@storybook/react";
import React from "react";
import Form from "@saleor/components/Form";
import SingleAutocompleteSelectField, {
SingleAutocompleteSelectFieldProps
} from "@saleor/components/SingleAutocompleteSelectField";
import { maybe } from "@saleor/misc";
import CardDecorator from "../../CardDecorator";
import Decorator from "../../Decorator";
import { ChoiceProvider } from "../../mock";
const suggestions = [
"Afghanistan",
"Burundi",
"Comoros",
"Egypt",
"Equatorial Guinea",
"Greenland",
"Isle of Man",
"Israel",
"Italy",
"United States",
"Wallis and Futuna",
"Zimbabwe"
].map(c => ({ label: c, value: c.toLocaleLowerCase().replace(/\s+/, "_") }));
const props: SingleAutocompleteSelectFieldProps = {
choices: undefined,
displayValue: undefined,
label: "Country",
loading: false,
name: "country",
onChange: () => undefined,
placeholder: "Select country"
};
const Story: React.FC<
Partial<SingleAutocompleteSelectFieldProps>
> = storyProps => {
const [displayValue, setDisplayValue] = React.useState(suggestions[0].label);
return (
<Form initial={{ country: suggestions[0].value }}>
{({ change, data }) => (
<ChoiceProvider choices={suggestions}>
{({ choices, loading, fetchChoices }) => {
const handleSelect = (event: React.ChangeEvent<any>) => {
const value: string = event.target.value;
const match = choices.find(choice => choice.value === value);
const label = maybe(() => match.label, value);
setDisplayValue(label);
change(event);
};
return (
<SingleAutocompleteSelectField
{...props}
displayValue={displayValue}
choices={choices}
fetchChoices={fetchChoices}
helperText={`Value: ${data.country}`}
onChange={handleSelect}
value={data.country}
loading={loading}
{...storyProps}
/>
);
}}
</ChoiceProvider>
)}
</Form>
);
};
storiesOf("Generics / SingleAutocompleteSelectField", module)
.addDecorator(CardDecorator)
.addDecorator(Decorator)
.add("with loaded data", () => <Story />)
.add("with loading data", () => <Story loading={true} />)
.add("with custom option", () => <Story allowCustomValues={true} />);

View file

@ -11,7 +11,10 @@ import Decorator from "../../Decorator";
const props: CustomerAddressDialogProps = { const props: CustomerAddressDialogProps = {
address: customer.addresses[0], address: customer.addresses[0],
confirmButtonState: "default", confirmButtonState: "default",
countries, countries: countries.map(c => ({
code: c.code,
label: c.name
})),
errors: [], errors: [],
onClose: () => undefined, onClose: () => undefined,
onConfirm: () => undefined, onConfirm: () => undefined,

View file

@ -1,24 +0,0 @@
import { storiesOf } from "@storybook/react";
import React from "react";
import OrderCustomerEditDialog from "../../../orders/components/OrderCustomerEditDialog";
import { clients as users } from "../../../orders/fixtures";
import Decorator from "../../Decorator";
const user = users[0];
storiesOf("Orders / OrderCustomerEditDialog", module)
.addDecorator(Decorator)
.add("default", () => (
<OrderCustomerEditDialog
confirmButtonState="default"
fetchUsers={undefined}
onChange={undefined}
onClose={undefined}
onConfirm={undefined}
open={true}
user={user.id}
userDisplayValue={user.email}
users={users}
/>
));

View file

@ -3,6 +3,7 @@ import { storiesOf } from "@storybook/react";
import React from "react"; import React from "react";
import placeholderImage from "@assets/images/placeholder60x60.png"; import placeholderImage from "@assets/images/placeholder60x60.png";
import { fetchMoreProps } from "@saleor/fixtures";
import OrderDraftPage, { import OrderDraftPage, {
OrderDraftPageProps OrderDraftPageProps
} from "../../../orders/components/OrderDraftPage"; } from "../../../orders/components/OrderDraftPage";
@ -12,6 +13,7 @@ import Decorator from "../../Decorator";
const order = draftOrder(placeholderImage); const order = draftOrder(placeholderImage);
const props: Omit<OrderDraftPageProps, "classes"> = { const props: Omit<OrderDraftPageProps, "classes"> = {
...fetchMoreProps,
countries, countries,
disabled: false, disabled: false,
fetchUsers: () => undefined, fetchUsers: () => undefined,

View file

@ -1,15 +1,14 @@
import { storiesOf } from "@storybook/react"; import { storiesOf } from "@storybook/react";
import React from "react"; import React from "react";
import { fetchMoreProps } from "@saleor/fixtures";
import ProductCreatePage, { import ProductCreatePage, {
ProductCreatePageSubmitData ProductCreatePageSubmitData
} from "../../../products/components/ProductCreatePage"; } from "../../../products/components/ProductCreatePage";
import { formError } from "../../misc";
import { product as productFixture } from "../../../products/fixtures"; import { product as productFixture } from "../../../products/fixtures";
import { productTypes } from "../../../productTypes/fixtures"; import { productTypes } from "../../../productTypes/fixtures";
import Decorator from "../../Decorator"; import Decorator from "../../Decorator";
import { formError } from "../../misc";
const product = productFixture(""); const product = productFixture("");
@ -25,6 +24,9 @@ storiesOf("Views / Products / Create product", module)
fetchCategories={() => undefined} fetchCategories={() => undefined}
fetchCollections={() => undefined} fetchCollections={() => undefined}
fetchProductTypes={() => undefined} fetchProductTypes={() => undefined}
fetchMoreCategories={fetchMoreProps}
fetchMoreCollections={fetchMoreProps}
fetchMoreProductTypes={fetchMoreProps}
productTypes={productTypes} productTypes={productTypes}
categories={[product.category]} categories={[product.category]}
onBack={() => undefined} onBack={() => undefined}
@ -42,6 +44,9 @@ storiesOf("Views / Products / Create product", module)
fetchCategories={() => undefined} fetchCategories={() => undefined}
fetchCollections={() => undefined} fetchCollections={() => undefined}
fetchProductTypes={() => undefined} fetchProductTypes={() => undefined}
fetchMoreCategories={fetchMoreProps}
fetchMoreCollections={fetchMoreProps}
fetchMoreProductTypes={fetchMoreProps}
productTypes={productTypes} productTypes={productTypes}
categories={[product.category]} categories={[product.category]}
onBack={() => undefined} onBack={() => undefined}
@ -61,6 +66,9 @@ storiesOf("Views / Products / Create product", module)
fetchCategories={() => undefined} fetchCategories={() => undefined}
fetchCollections={() => undefined} fetchCollections={() => undefined}
fetchProductTypes={() => undefined} fetchProductTypes={() => undefined}
fetchMoreCategories={fetchMoreProps}
fetchMoreCollections={fetchMoreProps}
fetchMoreProductTypes={fetchMoreProps}
productTypes={productTypes} productTypes={productTypes}
categories={[product.category]} categories={[product.category]}
onBack={() => undefined} onBack={() => undefined}

View file

@ -3,7 +3,7 @@ import React from "react";
import placeholderImage from "@assets/images/placeholder255x255.png"; import placeholderImage from "@assets/images/placeholder255x255.png";
import { collections } from "@saleor/collections/fixtures"; import { collections } from "@saleor/collections/fixtures";
import { listActionsProps } from "@saleor/fixtures"; import { fetchMoreProps, listActionsProps } from "@saleor/fixtures";
import ProductUpdatePage, { import ProductUpdatePage, {
ProductUpdatePageProps ProductUpdatePageProps
} from "@saleor/products/components/ProductUpdatePage"; } from "@saleor/products/components/ProductUpdatePage";
@ -20,6 +20,8 @@ const props: ProductUpdatePageProps = {
errors: [], errors: [],
fetchCategories: () => undefined, fetchCategories: () => undefined,
fetchCollections: () => undefined, fetchCollections: () => undefined,
fetchMoreCategories: fetchMoreProps,
fetchMoreCollections: fetchMoreProps,
header: product.name, header: product.name,
images: product.images, images: product.images,
onBack: () => undefined, onBack: () => undefined,

View file

@ -10,7 +10,8 @@ function createSingleAutocompleteSelectHandler(
change(event); change(event);
const value = event.target.value; const value = event.target.value;
setSelected(choices.find(category => category.value === value).label); const choice = choices.find(category => category.value === value)
setSelected(choice ? choice.label : value);
}; };
} }