Merge pull request #212 from mirumee/fix/autocomplete-ux
Improve autocomplete ux
This commit is contained in:
commit
3936e68a36
67 changed files with 3226 additions and 1455 deletions
|
@ -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
|
||||
|
|
3
assets/images/ChevronDown.svg
Normal file
3
assets/images/ChevronDown.svg
Normal 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 |
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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)
|
||||
)}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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} />);
|
|
@ -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,7 +123,17 @@ export const MultiAutocompleteSelectFieldComponent = withStyles(styles, {
|
|||
toggleMenu,
|
||||
highlightedIndex,
|
||||
inputValue
|
||||
}) => (
|
||||
}) => {
|
||||
const displayCustomValue =
|
||||
inputValue &&
|
||||
inputValue.length > 0 &&
|
||||
allowCustomValues &&
|
||||
!choices.find(
|
||||
choice =>
|
||||
choice.label.toLowerCase() === inputValue.toLowerCase()
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={classes.container} {...props}>
|
||||
<TextField
|
||||
InputProps={{
|
||||
|
@ -180,11 +142,7 @@ export const MultiAutocompleteSelectFieldComponent = withStyles(styles, {
|
|||
}),
|
||||
endAdornment: (
|
||||
<div>
|
||||
{loading ? (
|
||||
<CircularProgress size={20} />
|
||||
) : (
|
||||
<ArrowDropdownIcon onClick={toggleMenu} />
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
id: undefined,
|
||||
|
@ -195,99 +153,23 @@ export const MultiAutocompleteSelectFieldComponent = withStyles(styles, {
|
|||
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} />
|
||||
<MultiAutocompleteSelectFieldContent
|
||||
choices={choices.filter(
|
||||
choice => !value.includes(choice.value)
|
||||
)}
|
||||
{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
|
||||
displayCustomValue={displayCustomValue}
|
||||
displayValues={displayValues}
|
||||
getItemProps={getItemProps}
|
||||
hasMore={hasMore}
|
||||
highlightedIndex={highlightedIndex}
|
||||
loading={loading}
|
||||
inputValue={inputValue}
|
||||
onFetchMore={onFetchMore}
|
||||
/>
|
||||
<span className={classes.menuItemLabel}>
|
||||
{suggestion.label}
|
||||
</span>
|
||||
</MenuItem>
|
||||
))}
|
||||
{allowCustomValues &&
|
||||
inputValue &&
|
||||
!choices.find(
|
||||
choice =>
|
||||
choice.label.toLowerCase() ===
|
||||
inputValue.toLowerCase()
|
||||
) && (
|
||||
<MenuItem
|
||||
className={classes.menuItem}
|
||||
key={"customValue"}
|
||||
component="div"
|
||||
{...getItemProps({
|
||||
item: inputValue
|
||||
})}
|
||||
data-tc="multiautocomplete-select-option"
|
||||
>
|
||||
<span className={classes.menuItemLabel}>
|
||||
<FormattedMessage
|
||||
defaultMessage="Add new value: {value}"
|
||||
description="add custom option to select input"
|
||||
values={{
|
||||
value: inputValue
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
</MenuItem>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
!loading && (
|
||||
<MenuItem
|
||||
disabled={true}
|
||||
component="div"
|
||||
data-tc="multiautocomplete-select-no-options"
|
||||
>
|
||||
<FormattedMessage defaultMessage="No results found" />
|
||||
</MenuItem>
|
||||
)
|
||||
)}
|
||||
</Paper>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
);
|
||||
}}
|
||||
</Downshift>
|
||||
<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}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -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;
|
|
@ -1,2 +1,3 @@
|
|||
export { default } from "./MultiAutocompleteSelectField";
|
||||
export * from "./MultiAutocompleteSelectField";
|
||||
export * from "./MultiAutocompleteSelectFieldContent";
|
||||
|
|
|
@ -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} />);
|
|
@ -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({
|
||||
const styles = 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
|
||||
}
|
||||
});
|
||||
|
||||
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} />
|
||||
)}
|
||||
</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
|
||||
}}
|
||||
<SingleAutocompleteSelectFieldContent
|
||||
choices={choices}
|
||||
displayCustomValue={displayCustomValue}
|
||||
emptyOption={emptyOption}
|
||||
getItemProps={getItemProps}
|
||||
hasMore={hasMore}
|
||||
highlightedIndex={highlightedIndex}
|
||||
loading={loading}
|
||||
inputValue={inputValue}
|
||||
isCustomValueSelected={isCustomValueSelected}
|
||||
selectedItem={selectedItem}
|
||||
onFetchMore={onFetchMore}
|
||||
/>
|
||||
</MenuItem>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<MenuItem
|
||||
disabled={true}
|
||||
component="div"
|
||||
data-tc="singleautocomplete-select-no-options"
|
||||
>
|
||||
<FormattedMessage defaultMessage="No results found" />
|
||||
</MenuItem>
|
||||
)}
|
||||
</Paper>
|
||||
)}
|
||||
</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 (
|
||||
<DebounceAutocomplete debounceFn={fetchChoices}>
|
||||
{debounceFn => (
|
||||
<SingleAutocompleteSelectFieldComponent
|
||||
{...this.props}
|
||||
choices={this.state.choices}
|
||||
fetchChoices={this.handleInputChange}
|
||||
choices={choices}
|
||||
{...props}
|
||||
fetchChoices={debounceFn}
|
||||
/>
|
||||
)}
|
||||
</DebounceAutocomplete>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<SingleAutocompleteSelectFieldComponent
|
||||
fetchChoices={q => setQuery(q || "")}
|
||||
choices={filter(choices, query, {
|
||||
key: "label"
|
||||
})}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
export default SingleAutocompleteSelectField;
|
||||
|
|
|
@ -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;
|
|
@ -1,2 +1,3 @@
|
|||
export { default } from "./SingleAutocompleteSelectField";
|
||||
export * from "./SingleAutocompleteSelectField";
|
||||
export * from "./SingleAutocompleteSelectFieldContent";
|
||||
|
|
|
@ -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: ""
|
||||
};
|
||||
|
||||
|
|
|
@ -10,26 +10,30 @@ export interface SearchQueryVariables {
|
|||
query: string;
|
||||
}
|
||||
|
||||
function BaseSearch<TQuery, TQueryVariables extends SearchQueryVariables>(
|
||||
query: DocumentNode
|
||||
) {
|
||||
const Query = TypedQuery<TQuery, TQueryVariables>(query);
|
||||
interface BaseSearchProps {
|
||||
interface BaseSearchProps<
|
||||
TQuery,
|
||||
TQueryVariables extends SearchQueryVariables
|
||||
> {
|
||||
children: (props: {
|
||||
loadMore: () => void;
|
||||
search: (query: string) => void;
|
||||
result: TypedQueryResult<TQuery, TQueryVariables>;
|
||||
}) => React.ReactElement<any>;
|
||||
variables: TQueryVariables;
|
||||
}
|
||||
interface BaseSearchState {
|
||||
query: string;
|
||||
}
|
||||
|
||||
function BaseSearch<TQuery, TQueryVariables extends SearchQueryVariables>(
|
||||
query: DocumentNode,
|
||||
loadMoreFn: (result: TypedQueryResult<TQuery, TQueryVariables>) => void
|
||||
) {
|
||||
const Query = TypedQuery<TQuery, TQueryVariables>(query);
|
||||
|
||||
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;
|
||||
|
|
|
@ -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
|
||||
);
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
);
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
);
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
);
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
);
|
||||
|
|
|
@ -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 {
|
||||
|
|
47
src/containers/TopLevelSearch.tsx
Normal file
47
src/containers/TopLevelSearch.tsx
Normal 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;
|
|
@ -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 =>
|
||||
|
|
|
@ -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
|
||||
|
|
248
src/fixtures.ts
248
src/fixtures.ts
|
@ -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 = {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
),
|
||||
[]
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
@ -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;
|
|
@ -1,2 +0,0 @@
|
|||
export { default } from "./OrderCustomerEditDialog";
|
||||
export * from "./OrderCustomerEditDialog";
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,37 +521,10 @@ 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,
|
||||
|
@ -556,17 +539,17 @@ export const OrderDetails: React.StatelessComponent<OrderDetailsProps> = ({
|
|||
open={params.action === "add-order-line"}
|
||||
hasMore={maybe(
|
||||
() =>
|
||||
variantSearchOpts.data.products
|
||||
.pageInfo.hasNextPage
|
||||
variantSearchOpts.data.search.pageInfo
|
||||
.hasNextPage
|
||||
)}
|
||||
products={maybe(() =>
|
||||
variantSearchOpts.data.products.edges.map(
|
||||
variantSearchOpts.data.search.edges.map(
|
||||
edge => edge.node
|
||||
)
|
||||
)}
|
||||
onClose={closeModal}
|
||||
onFetch={variantSearch}
|
||||
onFetchMore={fetchMore}
|
||||
onFetchMore={loadMore}
|
||||
onSubmit={variants =>
|
||||
orderLinesAdd.mutate({
|
||||
id,
|
||||
|
@ -577,8 +560,7 @@ export const OrderDetails: React.StatelessComponent<OrderDetailsProps> = ({
|
|||
})
|
||||
}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
)}
|
||||
</SearchOrderVariant>
|
||||
</>
|
||||
)}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
)
|
||||
);
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -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"}
|
||||
|
|
|
@ -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>>;
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
@ -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");
|
||||
|
|
|
@ -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;
|
||||
}>;
|
||||
children: (props: {
|
||||
choices: SingleAutocompleteChoiceType[];
|
||||
hasMore: boolean;
|
||||
loading: boolean;
|
||||
fetchChoices(value: string);
|
||||
}
|
||||
) => React.ReactElement<any>);
|
||||
choices: Array<{
|
||||
label: string;
|
||||
value: string;
|
||||
}>;
|
||||
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;
|
||||
this.setState({
|
||||
choices: this.props.choices.filter(suggestion => {
|
||||
const keep =
|
||||
(!inputValue ||
|
||||
suggestion.label.toLowerCase().indexOf(inputValue.toLowerCase()) !==
|
||||
-1) &&
|
||||
count < 5;
|
||||
|
||||
if (keep) {
|
||||
count += 1;
|
||||
handleFetchMore = () => {
|
||||
if (!!this.state.timeout) {
|
||||
clearTimeout(this.state.timeout);
|
||||
}
|
||||
const timeout = setTimeout(this.fetchMore, 500);
|
||||
this.setState({
|
||||
loading: true,
|
||||
timeout
|
||||
});
|
||||
};
|
||||
|
||||
return keep;
|
||||
}),
|
||||
fetchMore = () =>
|
||||
this.setState(prevState => ({
|
||||
filteredChoices: prevState.choices.slice(0, prevState.first + step),
|
||||
first: prevState.first + step,
|
||||
loading: false,
|
||||
timeout: null
|
||||
}));
|
||||
|
||||
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
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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} />);
|
|
@ -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} />);
|
|
@ -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,
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
));
|
|
@ -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,
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue