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
- Send address update mutation only once - #210 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 ""
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-Transfer-Encoding: 8bit\n"
"MIME-Version: 1.0\n"
@ -267,15 +267,11 @@ msgctxt "description"
msgid "Add new menu item to begin creating menu"
msgstr ""
#: build/locale/src/components/MultiAutocompleteSelectField/MultiAutocompleteSelectField.json
#. [src.components.MultiAutocompleteSelectField.1477537381] - add custom option to select input
#: build/locale/src/components/MultiAutocompleteSelectField/MultiAutocompleteSelectFieldContent.json
#. [src.components.MultiAutocompleteSelectField.1477537381] - add custom select input option
#. defaultMessage is:
#. Add new value: {value}
msgctxt "add custom option to select input"
msgid "Add new value: {value}"
msgstr ""
#: build/locale/src/components/SingleAutocompleteSelectField/SingleAutocompleteSelectField.json
#: build/locale/src/components/SingleAutocompleteSelectField/SingleAutocompleteSelectFieldContent.json
#. [src.components.SingleAutocompleteSelectField.1477537381] - add custom select input option
#. defaultMessage is:
#. Add new value: {value}
@ -3411,14 +3407,6 @@ msgctxt "dialog header"
msgid "Edit Billing Address"
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
#. [menuItemDialogEditItem] - edit menu item, header
#. defaultMessage is:
@ -4863,7 +4851,7 @@ msgctxt "description"
msgid "No results"
msgstr ""
#: build/locale/src/components/MultiAutocompleteSelectField/MultiAutocompleteSelectField.json
#: build/locale/src/components/MultiAutocompleteSelectField/MultiAutocompleteSelectFieldContent.json
#. [src.components.MultiAutocompleteSelectField.4205644805]
#. defaultMessage is:
#. No results found
@ -4875,7 +4863,7 @@ msgstr ""
#. [src.components.RadioGroupField.4205644805]
#. defaultMessage is:
#. No results found
#: build/locale/src/components/SingleAutocompleteSelectField/SingleAutocompleteSelectField.json
#: build/locale/src/components/SingleAutocompleteSelectField/SingleAutocompleteSelectFieldContent.json
#. [src.components.SingleAutocompleteSelectField.4205644805]
#. defaultMessage is:
#. No results found
@ -5047,7 +5035,7 @@ msgctxt "description"
msgid "No. of Products"
msgstr ""
#: build/locale/src/components/SingleAutocompleteSelectField/SingleAutocompleteSelectField.json
#: build/locale/src/components/SingleAutocompleteSelectField/SingleAutocompleteSelectFieldContent.json
#. [src.components.SingleAutocompleteSelectField.3069107721]
#. defaultMessage is:
#. None

View file

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

View file

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

View file

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

View file

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

View file

@ -21,11 +21,11 @@ import TableCellAvatar from "@saleor/components/TableCellAvatar";
import useSearchQuery from "@saleor/hooks/useSearchQuery";
import { buttonMessages } from "@saleor/intl";
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";
export interface FormData {
products: SearchProducts_products_edges_node[];
products: SearchProducts_search_edges_node[];
query: string;
}
@ -53,18 +53,18 @@ const styles = createStyles({
export interface AssignProductDialogProps {
confirmButtonState: ConfirmButtonTransitionState;
open: boolean;
products: SearchProducts_products_edges_node[];
products: SearchProducts_search_edges_node[];
loading: boolean;
onClose: () => void;
onFetch: (value: string) => void;
onSubmit: (data: SearchProducts_products_edges_node[]) => void;
onSubmit: (data: SearchProducts_search_edges_node[]) => void;
}
function handleProductAssign(
product: SearchProducts_products_edges_node,
product: SearchProducts_search_edges_node,
isSelected: boolean,
selectedProducts: SearchProducts_products_edges_node[],
setSelectedProducts: (data: SearchProducts_products_edges_node[]) => void
selectedProducts: SearchProducts_search_edges_node[],
setSelectedProducts: (data: SearchProducts_search_edges_node[]) => void
) {
if (isSelected) {
setSelectedProducts(
@ -93,7 +93,7 @@ const AssignProductDialog = withStyles(styles, {
const intl = useIntl();
const [query, onQueryChange] = useSearchQuery(onFetch);
const [selectedProducts, setSelectedProducts] = React.useState<
SearchProducts_products_edges_node[]
SearchProducts_search_edges_node[]
>([]);
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 MenuItem from "@material-ui/core/MenuItem";
import Paper from "@material-ui/core/Paper";
import {
createStyles,
Theme,
@ -12,27 +9,19 @@ import TextField from "@material-ui/core/TextField";
import Typography from "@material-ui/core/Typography";
import CloseIcon from "@material-ui/icons/Close";
import Downshift, { ControllerStateAndHelpers } from "downshift";
import { filter } from "fuzzaldrin";
import React from "react";
import { FormattedMessage } from "react-intl";
import { compareTwoStrings } from "string-similarity";
import { fade } from "@material-ui/core/styles/colorManipulator";
import Checkbox from "@saleor/components/Checkbox";
import Debounce, { DebounceProps } from "@saleor/components/Debounce";
import ArrowDropdownIcon from "@saleor/icons/ArrowDropdown";
import Hr from "../Hr";
export interface MultiAutocompleteChoiceType {
label: string;
value: string;
}
import { FetchMoreProps } from "@saleor/types";
import MultiAutocompleteSelectFieldContent, {
MultiAutocompleteChoiceType
} from "./MultiAutocompleteSelectFieldContent";
const styles = (theme: Theme) =>
createStyles({
checkbox: {
height: 24,
width: 20
},
chip: {
width: "100%"
},
@ -66,49 +55,11 @@ const styles = (theme: Theme) =>
container: {
flexGrow: 1,
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;
displayValues: MultiAutocompleteChoiceType[];
name: string;
@ -134,6 +85,7 @@ export const MultiAutocompleteSelectFieldComponent = withStyles(styles, {
choices,
classes,
displayValues,
hasMore,
helperText,
label,
loading,
@ -142,6 +94,7 @@ export const MultiAutocompleteSelectFieldComponent = withStyles(styles, {
value,
fetchChoices,
onChange,
onFetchMore,
...props
}: MultiAutocompleteSelectFieldProps & WithStyles<typeof styles>) => {
const handleSelect = (
@ -155,7 +108,6 @@ export const MultiAutocompleteSelectFieldComponent = withStyles(styles, {
target: { name, value: item }
} as any);
};
const suggestions = choices.filter(choice => !value.includes(choice.value));
return (
<>
@ -171,123 +123,53 @@ export const MultiAutocompleteSelectFieldComponent = withStyles(styles, {
toggleMenu,
highlightedIndex,
inputValue
}) => (
<div className={classes.container} {...props}>
<TextField
InputProps={{
...getInputProps({
placeholder
}),
endAdornment: (
<div>
{loading ? (
<CircularProgress size={20} />
) : (
}) => {
const displayCustomValue =
inputValue &&
inputValue.length > 0 &&
allowCustomValues &&
!choices.find(
choice =>
choice.label.toLowerCase() === inputValue.toLowerCase()
);
return (
<div className={classes.container} {...props}>
<TextField
InputProps={{
...getInputProps({
placeholder
}),
endAdornment: (
<div>
<ArrowDropdownIcon onClick={toggleMenu} />
)}
</div>
),
id: undefined,
onClick: toggleMenu
}}
helperText={helperText}
label={label}
fullWidth={true}
/>
{isOpen && (!!inputValue || !!choices.length) && (
<Paper className={classes.paper} square>
{choices.length > 0 ||
displayValues.length > 0 ||
allowCustomValues ? (
<>
{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 && 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>
)}
</div>
),
id: undefined,
onClick: toggleMenu
}}
helperText={helperText}
label={label}
fullWidth={true}
/>
{isOpen && (!!inputValue || !!choices.length) && (
<MultiAutocompleteSelectFieldContent
choices={choices.filter(
choice => !value.includes(choice.value)
)}
displayCustomValue={displayCustomValue}
displayValues={displayValues}
getItemProps={getItemProps}
hasMore={hasMore}
highlightedIndex={highlightedIndex}
loading={loading}
inputValue={inputValue}
onFetchMore={onFetchMore}
/>
)}
</div>
);
}}
</Downshift>
<div className={classes.chipContainer}>
{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 (
<MultiAutocompleteSelectFieldComponent
fetchChoices={q => setQuery(q || "")}
choices={sortedChoices}
choices={filter(choices, query, {
key: "label"
})}
{...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 * 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 MenuItem from "@material-ui/core/MenuItem";
import Paper from "@material-ui/core/Paper";
import {
createStyles,
Theme,
withStyles,
WithStyles
} from "@material-ui/core/styles";
import { createStyles, withStyles, WithStyles } from "@material-ui/core/styles";
import TextField from "@material-ui/core/TextField";
import Typography from "@material-ui/core/Typography";
import Downshift from "downshift";
import { filter } from "fuzzaldrin";
import React from "react";
import { FormattedMessage } from "react-intl";
import { compareTwoStrings } from "string-similarity";
import SingleAutocompleteSelectFieldContent, {
SingleAutocompleteChoiceType
} from "./SingleAutocompleteSelectFieldContent";
import useStateFromProps from "@saleor/hooks/useStateFromProps";
import { FetchMoreProps } from "@saleor/types";
import ArrowDropdownIcon from "../../icons/ArrowDropdown";
import Debounce, { DebounceProps } from "../Debounce";
const styles = (theme: Theme) =>
createStyles({
container: {
flexGrow: 1,
position: "relative"
},
menuItem: {
height: "auto",
whiteSpace: "normal"
},
paper: {
borderRadius: 4,
left: 0,
marginTop: theme.spacing.unit,
padding: 8,
position: "absolute",
right: 0,
zIndex: 22
}
});
const styles = createStyles({
container: {
flexGrow: 1,
position: "relative"
}
});
export interface SingleAutocompleteChoiceType {
label: string;
value: any;
}
export interface SingleAutocompleteSelectFieldProps {
export interface SingleAutocompleteSelectFieldProps
extends Partial<FetchMoreProps> {
error?: boolean;
name: string;
displayValue: string;
emptyOption?: boolean;
choices: SingleAutocompleteChoiceType[];
value?: string;
value: string;
disabled?: boolean;
loading?: boolean;
placeholder?: string;
allowCustomValues?: boolean;
helperText?: string;
@ -63,13 +38,6 @@ export interface SingleAutocompleteSelectFieldProps {
onChange: (event: React.ChangeEvent<any>) => void;
}
interface SingleAutocompleteSelectFieldState {
choices: Array<{
label: string;
value: string;
}>;
}
const DebounceAutocomplete: React.ComponentType<
DebounceProps<string>
> = Debounce;
@ -85,6 +53,7 @@ const SingleAutocompleteSelectFieldComponent = withStyles(styles, {
displayValue,
emptyOption,
error,
hasMore,
helperText,
label,
loading,
@ -94,9 +63,11 @@ const SingleAutocompleteSelectFieldComponent = withStyles(styles, {
InputProps,
fetchChoices,
onChange,
onFetchMore,
...props
}: SingleAutocompleteSelectFieldProps & WithStyles<typeof styles>) => {
const [prevDisplayValue] = useStateFromProps(displayValue);
const handleChange = item =>
onChange({
target: {
@ -131,11 +102,21 @@ const SingleAutocompleteSelectFieldComponent = withStyles(styles, {
choices && selectedItem
? choices.filter(c => c.value === selectedItem).length === 0
: false;
const hasInputValueChanged = prevDisplayValue !== displayValue;
if (prevDisplayValue !== displayValue) {
if (hasInputValueChanged) {
reset({ inputValue: displayValue });
}
const displayCustomValue =
inputValue &&
inputValue.length > 0 &&
allowCustomValues &&
!choices.find(
choice =>
choice.label.toLowerCase() === inputValue.toLowerCase()
);
return (
<div className={classes.container} {...props}>
<TextField
@ -146,11 +127,7 @@ const SingleAutocompleteSelectFieldComponent = withStyles(styles, {
}),
endAdornment: (
<div>
{loading ? (
<CircularProgress size={20} />
) : (
<ArrowDropdownIcon onClick={toggleMenu} />
)}
<ArrowDropdownIcon onClick={toggleMenu} />
</div>
),
error,
@ -165,82 +142,19 @@ const SingleAutocompleteSelectFieldComponent = withStyles(styles, {
fullWidth={true}
/>
{isOpen && (!!inputValue || !!choices.length) && (
<Paper className={classes.paper} square>
{choices.length > 0 || allowCustomValues ? (
<>
{emptyOption && (
<MenuItem
className={classes.menuItem}
component="div"
{...getItemProps({
item: ""
})}
data-tc="singleautocomplete-select-option"
>
<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>
<SingleAutocompleteSelectFieldContent
choices={choices}
displayCustomValue={displayCustomValue}
emptyOption={emptyOption}
getItemProps={getItemProps}
hasMore={hasMore}
highlightedIndex={highlightedIndex}
loading={loading}
inputValue={inputValue}
isCustomValueSelected={isCustomValueSelected}
selectedItem={selectedItem}
onFetchMore={onFetchMore}
/>
)}
</div>
);
@ -252,40 +166,32 @@ const SingleAutocompleteSelectFieldComponent = withStyles(styles, {
}
);
export class SingleAutocompleteSelectField extends React.Component<
Omit<SingleAutocompleteSelectFieldProps, "classes">,
SingleAutocompleteSelectFieldState
> {
state = { choices: this.props.choices };
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} />;
}
const SingleAutocompleteSelectField: React.FC<
SingleAutocompleteSelectFieldProps
> = ({ choices, fetchChoices, ...props }) => {
const [query, setQuery] = React.useState("");
if (fetchChoices) {
return (
<SingleAutocompleteSelectFieldComponent
{...this.props}
choices={this.state.choices}
fetchChoices={this.handleInputChange}
/>
<DebounceAutocomplete debounceFn={fetchChoices}>
{debounceFn => (
<SingleAutocompleteSelectFieldComponent
choices={choices}
{...props}
fetchChoices={debounceFn}
/>
)}
</DebounceAutocomplete>
);
}
}
return (
<SingleAutocompleteSelectFieldComponent
fetchChoices={q => setQuery(q || "")}
choices={filter(choices, query, {
key: "label"
})}
{...props}
/>
);
};
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 * 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 = {
after: null,
first: 5,
first: 20,
query: ""
};

View file

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

View file

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

View file

@ -6,24 +6,33 @@
// GraphQL query operation: SearchCategories
// ====================================================
export interface SearchCategories_categories_edges_node {
export interface SearchCategories_search_edges_node {
__typename: "Category";
id: string;
name: string;
}
export interface SearchCategories_categories_edges {
export interface SearchCategories_search_edges {
__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";
edges: SearchCategories_categories_edges[];
edges: SearchCategories_search_edges[];
pageInfo: SearchCategories_search_pageInfo;
}
export interface SearchCategories {
categories: SearchCategories_categories | null;
search: SearchCategories_search | null;
}
export interface SearchCategoriesVariables {

View file

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

View file

@ -6,24 +6,33 @@
// GraphQL query operation: SearchCollections
// ====================================================
export interface SearchCollections_collections_edges_node {
export interface SearchCollections_search_edges_node {
__typename: "Collection";
id: string;
name: string;
}
export interface SearchCollections_collections_edges {
export interface SearchCollections_search_edges {
__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";
edges: SearchCollections_collections_edges[];
edges: SearchCollections_search_edges[];
pageInfo: SearchCollections_search_pageInfo;
}
export interface SearchCollections {
collections: SearchCollections_collections | null;
search: SearchCollections_search | null;
}
export interface SearchCollectionsVariables {

View file

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

View file

@ -6,24 +6,33 @@
// GraphQL query operation: SearchCustomers
// ====================================================
export interface SearchCustomers_customers_edges_node {
export interface SearchCustomers_search_edges_node {
__typename: "User";
id: string;
email: string;
}
export interface SearchCustomers_customers_edges {
export interface SearchCustomers_search_edges {
__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";
edges: SearchCustomers_customers_edges[];
edges: SearchCustomers_search_edges[];
pageInfo: SearchCustomers_search_pageInfo;
}
export interface SearchCustomers {
customers: SearchCustomers_customers | null;
search: SearchCustomers_search | null;
}
export interface SearchCustomersVariables {

View file

@ -1,19 +1,24 @@
import gql from "graphql-tag";
import BaseSearch from "../BaseSearch";
import { pageInfoFragment } from "@saleor/queries";
import TopLevelSearch from "../TopLevelSearch";
import { SearchPages, SearchPagesVariables } from "./types/SearchPages";
export const searchPages = gql`
${pageInfoFragment}
query SearchPages($after: String, $first: Int!, $query: String!) {
pages(after: $after, first: $first, query: $query) {
search: pages(after: $after, first: $first, query: $query) {
edges {
node {
id
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
// ====================================================
export interface SearchPages_pages_edges_node {
export interface SearchPages_search_edges_node {
__typename: "Page";
id: string;
title: string;
}
export interface SearchPages_pages_edges {
export interface SearchPages_search_edges {
__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";
edges: SearchPages_pages_edges[];
edges: SearchPages_search_edges[];
pageInfo: SearchPages_search_pageInfo;
}
export interface SearchPages {
pages: SearchPages_pages | null;
search: SearchPages_search | null;
}
export interface SearchPagesVariables {

View file

@ -1,14 +1,20 @@
import gql from "graphql-tag";
import BaseSearch from "../BaseSearch";
import { pageInfoFragment } from "@saleor/queries";
import TopLevelSearch from "../TopLevelSearch";
import {
SearchProductTypes,
SearchProductTypesVariables
} from "./types/SearchProductTypes";
export const searchProductTypes = gql`
${pageInfoFragment}
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 {
node {
id
@ -28,10 +34,13 @@ export const searchProductTypes = gql`
}
}
}
pageInfo {
...PageInfoFragment
}
}
}
`;
export default BaseSearch<SearchProductTypes, SearchProductTypesVariables>(
export default TopLevelSearch<SearchProductTypes, SearchProductTypesVariables>(
searchProductTypes
);

View file

@ -8,43 +8,52 @@ import { AttributeInputTypeEnum } from "./../../../types/globalTypes";
// GraphQL query operation: SearchProductTypes
// ====================================================
export interface SearchProductTypes_productTypes_edges_node_productAttributes_values {
export interface SearchProductTypes_search_edges_node_productAttributes_values {
__typename: "AttributeValue";
id: string;
name: string | null;
slug: string | null;
}
export interface SearchProductTypes_productTypes_edges_node_productAttributes {
export interface SearchProductTypes_search_edges_node_productAttributes {
__typename: "Attribute";
id: string;
inputType: AttributeInputTypeEnum | null;
slug: string | null;
name: string | null;
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";
id: string;
name: string;
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";
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";
edges: SearchProductTypes_productTypes_edges[];
edges: SearchProductTypes_search_edges[];
pageInfo: SearchProductTypes_search_pageInfo;
}
export interface SearchProductTypes {
productTypes: SearchProductTypes_productTypes | null;
search: SearchProductTypes_search | null;
}
export interface SearchProductTypesVariables {

View file

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

View file

@ -6,30 +6,39 @@
// GraphQL query operation: SearchProducts
// ====================================================
export interface SearchProducts_products_edges_node_thumbnail {
export interface SearchProducts_search_edges_node_thumbnail {
__typename: "Image";
url: string;
}
export interface SearchProducts_products_edges_node {
export interface SearchProducts_search_edges_node {
__typename: "Product";
id: 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";
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";
edges: SearchProducts_products_edges[];
edges: SearchProducts_search_edges[];
pageInfo: SearchProducts_search_pageInfo;
}
export interface SearchProducts {
products: SearchProducts_products | null;
search: SearchProducts_search | null;
}
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(() =>
searchProductsOpts.data.products.edges
searchProductsOpts.data.search.edges
.map(edge => edge.node)
.filter(
suggestedProduct => suggestedProduct.id
@ -389,7 +389,7 @@ export const SaleDetails: React.StatelessComponent<SaleDetailsProps> = ({
}) => (
<AssignCategoriesDialog
categories={maybe(() =>
searchCategoriesOpts.data.categories.edges
searchCategoriesOpts.data.search.edges
.map(edge => edge.node)
.filter(
suggestedCategory =>
@ -426,7 +426,7 @@ export const SaleDetails: React.StatelessComponent<SaleDetailsProps> = ({
}) => (
<AssignCollectionDialog
collections={maybe(() =>
searchCollectionsOpts.data.collections.edges
searchCollectionsOpts.data.search.edges
.map(edge => edge.node)
.filter(
suggestedCategory =>

View file

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

View file

@ -44,11 +44,249 @@ export const listActionsProps: ListActions = {
};
export const countries = [
{ code: "AF", label: "Afghanistan" },
{ code: "AX", label: "Åland Islands" },
{ code: "AL", label: "Albania" },
{ code: "DZ", label: "Algeria" },
{ code: "AS", label: "American Samoa" }
{ name: "Afghanistan", code: "AF" },
{ name: "Åland Islands", code: "AX" },
{ name: "Albania", code: "AL" },
{ name: "Algeria", code: "DZ" },
{ 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 = {

View file

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

View file

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

View file

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

View file

@ -20,8 +20,9 @@ import SingleAutocompleteSelectField from "@saleor/components/SingleAutocomplete
import Skeleton from "@saleor/components/Skeleton";
import useStateFromProps from "@saleor/hooks/useStateFromProps";
import { buttonMessages } from "@saleor/intl";
import { FetchMoreProps } from "@saleor/types";
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 { createHref, maybe } from "../../../misc";
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;
users?: SearchCustomers_customers_edges_node[];
users?: SearchCustomers_search_edges_node[];
loading?: boolean;
canEditAddresses: boolean;
canEditCustomer: boolean;
@ -67,14 +68,16 @@ const OrderCustomer = withStyles(styles, { name: "OrderCustomer" })(
canEditAddresses,
canEditCustomer,
fetchUsers,
hasMore: hasMoreUsers,
loading,
order,
users,
onCustomerEdit,
onBillingAddressEdit,
onFetchMore: onFetchMoreUsers,
onProfileView,
onShippingAddressEdit
}: OrderCustomerProps) => {
}: OrderCustomerProps & WithStyles<typeof styles>) => {
const intl = useIntl();
const user = maybe(() => order.user);
@ -138,11 +141,13 @@ const OrderCustomer = withStyles(styles, { name: "OrderCustomer" })(
choices={userChoices}
displayValue={userDisplayName}
fetchChoices={fetchUsers}
hasMore={hasMoreUsers}
loading={loading}
placeholder={intl.formatMessage({
defaultMessage: "Search Customers"
})}
onChange={handleUserChange}
onFetchMore={onFetchMoreUsers}
name="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 Skeleton from "@saleor/components/Skeleton";
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 { DraftOrderInput } from "../../../types/globalTypes";
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;
order: OrderDetails_order;
users: SearchCustomers_customers_edges_node[];
users: SearchCustomers_search_edges_node[];
usersLoading: boolean;
countries: Array<{
code: string;
@ -72,12 +73,14 @@ const OrderDraftPage = withStyles(styles, { name: "OrderDraftPage" })(
classes,
disabled,
fetchUsers,
hasMore,
saveButtonBarState,
onBack,
onBillingAddressEdit,
onCustomerEdit,
onDraftFinalize,
onDraftRemove,
onFetchMore,
onNoteAdd,
onOrderLineAdd,
onOrderLineChange,
@ -88,7 +91,7 @@ const OrderDraftPage = withStyles(styles, { name: "OrderDraftPage" })(
order,
users,
usersLoading
}: OrderDraftPageProps) => {
}: OrderDraftPageProps & WithStyles<typeof styles>) => {
const intl = useIntl();
return (
@ -139,14 +142,16 @@ const OrderDraftPage = withStyles(styles, { name: "OrderDraftPage" })(
<OrderCustomer
canEditAddresses={true}
canEditCustomer={true}
fetchUsers={fetchUsers}
hasMore={hasMore}
loading={usersLoading}
order={order}
users={users}
loading={usersLoading}
fetchUsers={fetchUsers}
onBillingAddressEdit={onBillingAddressEdit}
onCustomerEdit={onCustomerEdit}
onShippingAddressEdit={onShippingAddressEdit}
onFetchMore={onFetchMore}
onProfileView={onProfileView}
onShippingAddressEdit={onShippingAddressEdit}
/>
</div>
</Grid>

View file

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

View file

@ -1,5 +1,5 @@
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 {
FulfillmentStatus,
@ -11,7 +11,7 @@ import {
import { OrderDetails_order } from "./types/OrderDetails";
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",
email: "test.client1@example.com",

View file

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

View file

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

View file

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

View file

@ -38,5 +38,25 @@ export const searchAttributes = gql`
`;
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 {
SearchProductTypes_productTypes_edges_node,
SearchProductTypes_productTypes_edges_node_productAttributes
SearchProductTypes_search_edges_node,
SearchProductTypes_search_edges_node_productAttributes
} from "@saleor/containers/SearchProductTypes/types/SearchProductTypes";
import { AttributeInputTypeEnum } from "../types/globalTypes";
import { ProductTypeDetails_productType } from "./types/ProductTypeDetails";
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: {
__typename: "Attribute" as "Attribute",
@ -469,8 +469,7 @@ export const attributes: SearchProductTypes_productTypes_edges_node_productAttri
].map(edge => edge.node);
export const productTypes: Array<
SearchProductTypes_productTypes_edges_node &
ProductTypeList_productTypes_edges_node
SearchProductTypes_search_edges_node & ProductTypeList_productTypes_edges_node
> = [
{
__typename: "ProductType" as "ProductType",

View file

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

View file

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

View file

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

View file

@ -2,7 +2,7 @@ import { RawDraftContentState } from "draft-js";
import { MultiAutocompleteChoiceType } from "@saleor/components/MultiAutocompleteSelectField";
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 {
ProductDetails_product,
@ -35,7 +35,7 @@ export interface ProductType {
hasVariants: boolean;
id: string;
name: string;
productAttributes: SearchProductTypes_productTypes_edges_node_productAttributes[];
productAttributes: SearchProductTypes_search_edges_node_productAttributes[];
}
export function getAttributeInputFromProduct(

View file

@ -33,11 +33,20 @@ export const ProductUpdate: React.StatelessComponent<
return (
<SearchCategories variables={DEFAULT_INITIAL_SEARCH_DATA}>
{({ search: searchCategory, result: searchCategoryOpts }) => (
{({
loadMore: loadMoreCategories,
search: searchCategory,
result: searchCategoryOpts
}) => (
<SearchCollections variables={DEFAULT_INITIAL_SEARCH_DATA}>
{({ search: searchCollection, result: searchCollectionOpts }) => (
{({
loadMore: loadMoreCollections,
search: searchCollection,
result: searchCollectionOpts
}) => (
<SearchProductTypes variables={DEFAULT_INITIAL_SEARCH_DATA}>
{({
loadMore: loadMoreProductTypes,
search: searchProductTypes,
result: searchProductTypesOpts
}) => {
@ -121,11 +130,11 @@ export const ProductUpdate: React.StatelessComponent<
<ProductCreatePage
currency={maybe(() => shop.defaultCurrency)}
categories={maybe(
() => searchCategoryOpts.data.categories.edges,
() => searchCategoryOpts.data.search.edges,
[]
).map(edge => edge.node)}
collections={maybe(
() => searchCollectionOpts.data.collections.edges,
() => searchCollectionOpts.data.search.edges,
[]
).map(edge => edge.node)}
disabled={productCreateDataLoading}
@ -141,13 +150,40 @@ export const ProductUpdate: React.StatelessComponent<
description: "page header"
})}
productTypes={maybe(() =>
searchProductTypesOpts.data.productTypes.edges.map(
searchProductTypesOpts.data.search.edges.map(
edge => edge.node
)
)}
onBack={handleBack}
onSubmit={handleSubmit}
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 (
<SearchCategories variables={DEFAULT_INITIAL_SEARCH_DATA}>
{({ search: searchCategories, result: searchCategoriesOpts }) => (
{({
loadMore: loadMoreCategories,
search: searchCategories,
result: searchCategoriesOpts
}) => (
<SearchCollections variables={DEFAULT_INITIAL_SEARCH_DATA}>
{({ search: searchCollections, result: searchCollectionsOpts }) => (
{({
loadMore: loadMoreCollections,
search: searchCollections,
result: searchCollectionsOpts
}) => (
<TypedProductDetailsQuery
displayLoader
require={["product"]}
@ -228,11 +236,11 @@ export const ProductUpdate: React.StatelessComponent<ProductUpdateProps> = ({
);
const categories = maybe(
() => searchCategoriesOpts.data.categories.edges,
() => searchCategoriesOpts.data.search.edges,
[]
).map(edge => edge.node);
const collections = maybe(
() => searchCollectionsOpts.data.collections.edges,
() => searchCollectionsOpts.data.search.edges,
[]
).map(edge => edge.node);
const errors = maybe(
@ -295,6 +303,24 @@ export const ProductUpdate: React.StatelessComponent<ProductUpdateProps> = ({
selected={listElements.length}
toggle={toggle}
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
open={params.action === "remove"}

View file

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

View file

@ -7,6 +7,7 @@ const CardDecorator = storyFn => (
style={{
margin: "auto",
overflow: "visible",
position: "relative",
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/Money");
require("./stories/components/MoneyRange");
require("./stories/components/MultiAutocompleteSelectField");
require("./stories/components/MultiSelectField");
require("./stories/components/NotFoundPage");
require("./stories/components/PageHeader");
@ -39,7 +38,6 @@ function loadStories() {
require("./stories/components/RichTextEditor");
require("./stories/components/SaveButtonBar");
require("./stories/components/SaveFilterTabDialog");
require("./stories/components/SingleAutocompleteSelectField");
require("./stories/components/SingleSelectField");
require("./stories/components/Skeleton");
require("./stories/components/StatusLabel");
@ -125,7 +123,6 @@ function loadStories() {
require("./stories/orders/OrderBulkCancelDialog");
require("./stories/orders/OrderCancelDialog");
require("./stories/orders/OrderCustomer");
require("./stories/orders/OrderCustomerEditDialog");
require("./stories/orders/OrderDetailsPage");
require("./stories/orders/OrderDraftCancelDialog");
require("./stories/orders/OrderDraftFinalizeDialog");

View file

@ -1,38 +1,41 @@
import React from "react";
import { SingleAutocompleteChoiceType } from "@saleor/components/SingleAutocompleteSelectField/SingleAutocompleteSelectFieldContent";
interface ChoiceProviderProps {
children: ((
props: {
choices: Array<{
label: string;
value: string;
}>;
loading: boolean;
fetchChoices(value: string);
}
) => React.ReactElement<any>);
choices: Array<{
label: string;
value: string;
}>;
children: (props: {
choices: SingleAutocompleteChoiceType[];
hasMore: boolean;
loading: boolean;
fetchChoices: (value: string) => void;
fetchMore: () => void;
}) => React.ReactElement<any>;
choices: SingleAutocompleteChoiceType[];
}
interface ChoiceProviderState {
choices: Array<{
label: string;
value: string;
}>;
choices: SingleAutocompleteChoiceType[];
filteredChoices: SingleAutocompleteChoiceType[];
first: number;
loading: boolean;
timeout: any;
}
const step = 5;
export class ChoiceProvider extends React.Component<
ChoiceProviderProps,
ChoiceProviderState
> {
state = { choices: [], loading: false, timeout: null };
state = {
choices: [],
filteredChoices: [],
first: step,
loading: false,
timeout: null
};
handleChange = inputValue => {
if (this.state.loading) {
handleChange = (inputValue: string) => {
if (!!this.state.timeout) {
clearTimeout(this.state.timeout);
}
const timeout = setTimeout(() => this.fetchChoices(inputValue), 500);
@ -42,22 +45,35 @@ export class ChoiceProvider extends React.Component<
});
};
fetchChoices = inputValue => {
let count = 0;
handleFetchMore = () => {
if (!!this.state.timeout) {
clearTimeout(this.state.timeout);
}
const timeout = setTimeout(this.fetchMore, 500);
this.setState({
choices: this.props.choices.filter(suggestion => {
const keep =
(!inputValue ||
suggestion.label.toLowerCase().indexOf(inputValue.toLowerCase()) !==
-1) &&
count < 5;
loading: true,
timeout
});
};
if (keep) {
count += 1;
}
fetchMore = () =>
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,
timeout: null
});
@ -65,8 +81,10 @@ export class ChoiceProvider extends React.Component<
render() {
return this.props.children({
choices: this.state.choices,
choices: this.state.filteredChoices,
fetchChoices: this.handleChange,
fetchMore: this.handleFetchMore,
hasMore: this.state.choices.length > this.state.filteredChoices.length,
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 = {
address: customer.addresses[0],
confirmButtonState: "default",
countries,
countries: countries.map(c => ({
code: c.code,
label: c.name
})),
errors: [],
onClose: () => 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 placeholderImage from "@assets/images/placeholder60x60.png";
import { fetchMoreProps } from "@saleor/fixtures";
import OrderDraftPage, {
OrderDraftPageProps
} from "../../../orders/components/OrderDraftPage";
@ -12,6 +13,7 @@ import Decorator from "../../Decorator";
const order = draftOrder(placeholderImage);
const props: Omit<OrderDraftPageProps, "classes"> = {
...fetchMoreProps,
countries,
disabled: false,
fetchUsers: () => undefined,

View file

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

View file

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

View file

@ -10,7 +10,8 @@ function createSingleAutocompleteSelectHandler(
change(event);
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);
};
}