From 0bf7594ce0b4b251d39769551b081368586ccc0b Mon Sep 17 00:00:00 2001 From: dominik-zeglen Date: Mon, 14 Oct 2019 13:57:08 +0200 Subject: [PATCH 01/18] Refactor ui a bit --- ...SingleAutocompleteSelectField.stories.tsx} | 26 +- .../SingleAutocompleteSelectField.tsx | 142 +++------- .../SingleAutocompleteSelectFieldContent.tsx | 197 ++++++++++++++ .../SingleAutocompleteSelectField/fixtures.ts | 245 ++++++++++++++++++ src/storybook/config.js | 1 - src/storybook/mock.tsx | 42 ++- 6 files changed, 497 insertions(+), 156 deletions(-) rename src/{storybook/stories/components/SingleAutocompleteSelectField.tsx => components/SingleAutocompleteSelectField/SingleAutocompleteSelectField.stories.tsx} (80%) create mode 100644 src/components/SingleAutocompleteSelectField/SingleAutocompleteSelectFieldContent.tsx create mode 100644 src/components/SingleAutocompleteSelectField/fixtures.ts diff --git a/src/storybook/stories/components/SingleAutocompleteSelectField.tsx b/src/components/SingleAutocompleteSelectField/SingleAutocompleteSelectField.stories.tsx similarity index 80% rename from src/storybook/stories/components/SingleAutocompleteSelectField.tsx rename to src/components/SingleAutocompleteSelectField/SingleAutocompleteSelectField.stories.tsx index 0827f0b70..e332f3647 100644 --- a/src/storybook/stories/components/SingleAutocompleteSelectField.tsx +++ b/src/components/SingleAutocompleteSelectField/SingleAutocompleteSelectField.stories.tsx @@ -2,28 +2,16 @@ import { storiesOf } from "@storybook/react"; import React from "react"; import Form from "@saleor/components/Form"; +import { maybe } from "@saleor/misc"; +import CardDecorator from "@saleor/storybook/CardDecorator"; +import Decorator from "@saleor/storybook/Decorator"; +import { ChoiceProvider } from "@saleor/storybook/mock"; +import { countries } from "./fixtures"; import SingleAutocompleteSelectField, { SingleAutocompleteSelectFieldProps -} from "@saleor/components/SingleAutocompleteSelectField"; -import { maybe } from "@saleor/misc"; -import CardDecorator from "../../CardDecorator"; -import Decorator from "../../Decorator"; -import { ChoiceProvider } from "../../mock"; +} from "./SingleAutocompleteSelectField"; -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 suggestions = countries.map(c => ({ label: c.name, value: c.code })); const props: SingleAutocompleteSelectFieldProps = { choices: undefined, diff --git a/src/components/SingleAutocompleteSelectField/SingleAutocompleteSelectField.tsx b/src/components/SingleAutocompleteSelectField/SingleAutocompleteSelectField.tsx index 1f1cf92c3..742a67fb2 100644 --- a/src/components/SingleAutocompleteSelectField/SingleAutocompleteSelectField.tsx +++ b/src/components/SingleAutocompleteSelectField/SingleAutocompleteSelectField.tsx @@ -1,50 +1,26 @@ 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 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 ArrowDropdownIcon from "../../icons/ArrowDropdown"; import Debounce, { DebounceProps } from "../Debounce"; -const styles = (theme: Theme) => - createStyles({ - container: { - flexGrow: 1, - position: "relative" - }, - menuItem: { - height: "auto", - whiteSpace: "normal" - }, - paper: { - borderRadius: 4, - left: 0, - marginTop: theme.spacing.unit, - padding: 8, - position: "absolute", - right: 0, - zIndex: 22 - } - }); +const styles = createStyles({ + container: { + flexGrow: 1, + position: "relative" + } +}); -export interface SingleAutocompleteChoiceType { - label: string; - value: any; -} export interface SingleAutocompleteSelectFieldProps { error?: boolean; name: string; @@ -97,6 +73,7 @@ const SingleAutocompleteSelectFieldComponent = withStyles(styles, { ...props }: SingleAutocompleteSelectFieldProps & WithStyles) => { const [prevDisplayValue] = useStateFromProps(displayValue); + const handleChange = item => onChange({ target: { @@ -131,11 +108,20 @@ 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.length > 0 && + allowCustomValues && + !choices.find( + choice => + choice.label.toLowerCase() === inputValue.toLowerCase() + ); + return (
{isOpen && (!!inputValue || !!choices.length) && ( - - {choices.length > 0 || allowCustomValues ? ( - <> - {emptyOption && ( - - - - - - )} - {choices.map((suggestion, index) => { - const choiceIndex = index + (emptyOption ? 1 : 0); - - return ( - - {suggestion.label} - - ); - })} - {allowCustomValues && - !!inputValue && - !choices.find( - choice => - choice.label.toLowerCase() === - inputValue.toLowerCase() - ) && ( - - - - )} - - ) : ( - - - - )} - + )}
); diff --git a/src/components/SingleAutocompleteSelectField/SingleAutocompleteSelectFieldContent.tsx b/src/components/SingleAutocompleteSelectField/SingleAutocompleteSelectFieldContent.tsx new file mode 100644 index 000000000..4775e063e --- /dev/null +++ b/src/components/SingleAutocompleteSelectField/SingleAutocompleteSelectFieldContent.tsx @@ -0,0 +1,197 @@ +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 { FormattedMessage } from "react-intl"; + +import useElementScroll from "@saleor/hooks/useElementScroll"; +import Hr from "../Hr"; + +const menuItemHeight = 46; +const maxMenuItems = 5; + +export interface SingleAutocompleteChoiceType { + label: string; + value: any; +} +interface SingleAutocompleteSelectFieldContentProps { + choices: SingleAutocompleteChoiceType[]; + displayCustomValue: boolean; + emptyOption: boolean; + getItemProps: (options: GetItemPropsOptions) => void; + highlightedIndex: number; + inputValue: string; + isCustomValueSelected: boolean; + selectedItem: any; +} + +const useStyles = makeStyles( + (theme: Theme) => ({ + content: { + maxHeight: menuItemHeight * maxMenuItems + theme.spacing.unit * 2, + overflow: "scroll", + padding: 8 + }, + hr: { + margin: `${theme.spacing.unit}px 0` + }, + menuItem: { + height: "auto", + whiteSpace: "normal" + }, + root: { + borderRadius: 4, + left: 0, + marginTop: theme.spacing.unit, + position: "absolute", + right: 0, + zIndex: 22 + }, + shadow: { + boxShadow: `0px -5px 10px 0px ${theme.palette.grey[800]}` + }, + shadowLine: { + boxShadow: `0px 0px 0px 0px ${theme.palette.grey[50]}`, + height: 1, + transition: theme.transitions.duration.short + "ms" + } + }), + { + 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, + highlightedIndex, + inputValue, + isCustomValueSelected, + selectedItem + } = props; + + const classes = useStyles(props); + const anchor = React.useRef(); + const scrollPosition = useElementScroll(anchor); + + const dropShadow = anchor.current + ? scrollPosition.y + anchor.current.clientHeight < + anchor.current.scrollHeight + : false; + + return ( + +
+ {choices.length > 0 || displayCustomValue ? ( + <> + {emptyOption && ( + + + + + + )} + {displayCustomValue && ( + + + + )} + {choices.length > 0 && displayCustomValue && ( +
+ )} + {choices.map((suggestion, index) => { + const choiceIndex = getChoiceIndex( + index, + emptyOption, + displayCustomValue + ); + + return ( + + {suggestion.label} + + ); + })} + + ) : ( + + + + )} +
+
+ + ); +}; + +SingleAutocompleteSelectFieldContent.displayName = + "SingleAutocompleteSelectFieldContent"; +export default SingleAutocompleteSelectFieldContent; diff --git a/src/components/SingleAutocompleteSelectField/fixtures.ts b/src/components/SingleAutocompleteSelectField/fixtures.ts new file mode 100644 index 000000000..77eefc369 --- /dev/null +++ b/src/components/SingleAutocompleteSelectField/fixtures.ts @@ -0,0 +1,245 @@ +export const countries = [ + { 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" } +]; diff --git a/src/storybook/config.js b/src/storybook/config.js index 89b3a57cf..9b7e2c7ec 100644 --- a/src/storybook/config.js +++ b/src/storybook/config.js @@ -39,7 +39,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"); diff --git a/src/storybook/mock.tsx b/src/storybook/mock.tsx index a977fd8fe..7e40ca5da 100644 --- a/src/storybook/mock.tsx +++ b/src/storybook/mock.tsx @@ -1,16 +1,14 @@ import React from "react"; interface ChoiceProviderProps { - children: (( - props: { - choices: Array<{ - label: string; - value: string; - }>; - loading: boolean; - fetchChoices(value: string); - } - ) => React.ReactElement); + children: (props: { + choices: Array<{ + label: string; + value: string; + }>; + loading: boolean; + fetchChoices(value: string); + }) => React.ReactElement; choices: Array<{ label: string; value: string; @@ -31,7 +29,7 @@ export class ChoiceProvider extends React.Component< > { state = { choices: [], loading: false, timeout: null }; - handleChange = inputValue => { + handleChange = (inputValue: string) => { if (this.state.loading) { clearTimeout(this.state.timeout); } @@ -42,22 +40,16 @@ export class ChoiceProvider extends React.Component< }); }; - fetchChoices = inputValue => { - let count = 0; + fetchChoices = (inputValue: string) => { this.setState({ - choices: this.props.choices.filter(suggestion => { - const keep = - (!inputValue || + choices: this.props.choices + .filter( + suggestion => + !inputValue || suggestion.label.toLowerCase().indexOf(inputValue.toLowerCase()) !== - -1) && - count < 5; - - if (keep) { - count += 1; - } - - return keep; - }), + -1 + ) + .slice(0, 10), loading: false, timeout: null }); From d99321be841d957720628925164fb5620c5af096 Mon Sep 17 00:00:00 2001 From: dominik-zeglen Date: Mon, 14 Oct 2019 16:17:03 +0200 Subject: [PATCH 02/18] Add dynamic loading --- .../SingleAutocompleteSelectField.stories.tsx | 65 ++++++++++++---- .../SingleAutocompleteSelectField.tsx | 20 ++--- .../SingleAutocompleteSelectFieldContent.tsx | 48 ++++++++++-- .../SingleAutocompleteSelectField/index.ts | 1 + src/storybook/CardDecorator.tsx | 1 + src/storybook/mock.tsx | 76 +++++++++++++------ 6 files changed, 156 insertions(+), 55 deletions(-) diff --git a/src/components/SingleAutocompleteSelectField/SingleAutocompleteSelectField.stories.tsx b/src/components/SingleAutocompleteSelectField/SingleAutocompleteSelectField.stories.tsx index e332f3647..8a4435bd6 100644 --- a/src/components/SingleAutocompleteSelectField/SingleAutocompleteSelectField.stories.tsx +++ b/src/components/SingleAutocompleteSelectField/SingleAutocompleteSelectField.stories.tsx @@ -6,10 +6,14 @@ import { maybe } from "@saleor/misc"; 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 { countries } from "./fixtures"; import SingleAutocompleteSelectField, { SingleAutocompleteSelectFieldProps } from "./SingleAutocompleteSelectField"; +import SingleAutocompleteSelectFieldContent, { + SingleAutocompleteSelectFieldContentProps +} from "./SingleAutocompleteSelectFieldContent"; const suggestions = countries.map(c => ({ label: c.name, value: c.code })); @@ -20,11 +24,16 @@ const props: SingleAutocompleteSelectFieldProps = { loading: false, name: "country", onChange: () => undefined, - placeholder: "Select country" + placeholder: "Select country", + value: suggestions[0].value }; const Story: React.FC< - Partial + Partial< + SingleAutocompleteSelectFieldProps & { + enableLoadMore: boolean; + } + > > = storyProps => { const [displayValue, setDisplayValue] = React.useState(suggestions[0].label); @@ -32,14 +41,12 @@ const Story: React.FC<
{({ change, data }) => ( - {({ choices, loading, fetchChoices }) => { - const handleSelect = (event: React.ChangeEvent) => { - const value: string = event.target.value; - const match = choices.find(choice => choice.value === value); - const label = maybe(() => match.label, value); - setDisplayValue(label); - change(event); - }; + {({ choices, fetchChoices, fetchMore, hasMore, loading }) => { + const handleSelect = createSingleAutocompleteSelectHandler( + change, + setDisplayValue, + choices + ); return ( ); @@ -61,9 +70,35 @@ const Story: React.FC< ); }; -storiesOf("Generics / SingleAutocompleteSelectField", module) +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("with loaded data", () => ) - .add("with loading data", () => ) - .add("with custom option", () => ); + .add("default", () => ( + + )) + .add("can load more", () => ( + + )) + .add("no data", () => ( + + )) + .add("interactive", () => ) + .add("interactive with custom option", () => ( + + )) + .add("interactive with empty option", () => ) + .add("interactive with load more", () => ); diff --git a/src/components/SingleAutocompleteSelectField/SingleAutocompleteSelectField.tsx b/src/components/SingleAutocompleteSelectField/SingleAutocompleteSelectField.tsx index 742a67fb2..b48793371 100644 --- a/src/components/SingleAutocompleteSelectField/SingleAutocompleteSelectField.tsx +++ b/src/components/SingleAutocompleteSelectField/SingleAutocompleteSelectField.tsx @@ -1,5 +1,4 @@ import { Omit } from "@material-ui/core"; -import CircularProgress from "@material-ui/core/CircularProgress"; import { InputProps } from "@material-ui/core/Input"; import { createStyles, withStyles, WithStyles } from "@material-ui/core/styles"; import TextField from "@material-ui/core/TextField"; @@ -11,6 +10,7 @@ import SingleAutocompleteSelectFieldContent, { } from "./SingleAutocompleteSelectFieldContent"; import useStateFromProps from "@saleor/hooks/useStateFromProps"; +import { FetchMoreProps } from "@saleor/types"; import ArrowDropdownIcon from "../../icons/ArrowDropdown"; import Debounce, { DebounceProps } from "../Debounce"; @@ -21,15 +21,15 @@ const styles = createStyles({ } }); -export interface SingleAutocompleteSelectFieldProps { +export interface SingleAutocompleteSelectFieldProps + extends Partial { error?: boolean; name: string; displayValue: string; emptyOption?: boolean; choices: SingleAutocompleteChoiceType[]; - value?: string; + value: string; disabled?: boolean; - loading?: boolean; placeholder?: string; allowCustomValues?: boolean; helperText?: string; @@ -61,6 +61,7 @@ const SingleAutocompleteSelectFieldComponent = withStyles(styles, { displayValue, emptyOption, error, + hasMore, helperText, label, loading, @@ -70,6 +71,7 @@ const SingleAutocompleteSelectFieldComponent = withStyles(styles, { InputProps, fetchChoices, onChange, + onFetchMore, ...props }: SingleAutocompleteSelectFieldProps & WithStyles) => { const [prevDisplayValue] = useStateFromProps(displayValue); @@ -132,11 +134,7 @@ const SingleAutocompleteSelectFieldComponent = withStyles(styles, { }), endAdornment: (
- {loading ? ( - - ) : ( - - )} +
), error, @@ -156,10 +154,13 @@ const SingleAutocompleteSelectFieldComponent = withStyles(styles, { displayCustomValue={displayCustomValue} emptyOption={emptyOption} getItemProps={getItemProps} + hasMore={hasMore} highlightedIndex={highlightedIndex} + loading={loading} inputValue={inputValue} isCustomValueSelected={isCustomValueSelected} selectedItem={selectedItem} + onFetchMore={onFetchMore} /> )}
@@ -208,4 +209,5 @@ export class SingleAutocompleteSelectField extends React.Component< ); } } + export default SingleAutocompleteSelectField; diff --git a/src/components/SingleAutocompleteSelectField/SingleAutocompleteSelectFieldContent.tsx b/src/components/SingleAutocompleteSelectField/SingleAutocompleteSelectFieldContent.tsx index 4775e063e..517852007 100644 --- a/src/components/SingleAutocompleteSelectField/SingleAutocompleteSelectFieldContent.tsx +++ b/src/components/SingleAutocompleteSelectField/SingleAutocompleteSelectFieldContent.tsx @@ -1,3 +1,4 @@ +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"; @@ -9,16 +10,19 @@ import React from "react"; import { FormattedMessage } from "react-intl"; import useElementScroll 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; } -interface SingleAutocompleteSelectFieldContentProps { +export interface SingleAutocompleteSelectFieldContentProps + extends Partial { choices: SingleAutocompleteChoiceType[]; displayCustomValue: boolean; emptyOption: boolean; @@ -43,6 +47,11 @@ const useStyles = makeStyles( height: "auto", whiteSpace: "normal" }, + progress: {}, + progressContainer: { + display: "flex", + justifyContent: "center" + }, root: { borderRadius: 4, left: 0, @@ -52,7 +61,9 @@ const useStyles = makeStyles( zIndex: 22 }, shadow: { - boxShadow: `0px -5px 10px 0px ${theme.palette.grey[800]}` + "&$shadowLine": { + boxShadow: `0px -5px 10px 0px ${theme.palette.grey[800]}` + } }, shadowLine: { boxShadow: `0px 0px 0px 0px ${theme.palette.grey[50]}`, @@ -89,21 +100,38 @@ const SingleAutocompleteSelectFieldContent: React.FC< displayCustomValue, emptyOption, getItemProps, + hasMore, highlightedIndex, + loading, inputValue, isCustomValueSelected, - selectedItem + selectedItem, + onFetchMore } = props; const classes = useStyles(props); const anchor = React.useRef(); const scrollPosition = useElementScroll(anchor); + const [calledForMore, setCalledForMore] = React.useState(false); - const dropShadow = anchor.current - ? scrollPosition.y + anchor.current.clientHeight < + const scrolledToBottom = anchor.current + ? scrollPosition.y + anchor.current.clientHeight + offset >= anchor.current.scrollHeight : false; + React.useEffect(() => { + if (!calledForMore && onFetchMore && scrolledToBottom) { + onFetchMore(); + setCalledForMore(true); + } + }, [scrolledToBottom]); + + React.useEffect(() => { + if (calledForMore && !loading) { + setCalledForMore(false); + } + }, [loading]); + return (
@@ -172,6 +200,14 @@ const SingleAutocompleteSelectFieldContent: React.FC< ); })} + {hasMore && ( + <> +
+
+ +
+ + )} ) : (
diff --git a/src/components/SingleAutocompleteSelectField/index.ts b/src/components/SingleAutocompleteSelectField/index.ts index c1cdf064d..56328d1d5 100644 --- a/src/components/SingleAutocompleteSelectField/index.ts +++ b/src/components/SingleAutocompleteSelectField/index.ts @@ -1,2 +1,3 @@ export { default } from "./SingleAutocompleteSelectField"; export * from "./SingleAutocompleteSelectField"; +export * from "./SingleAutocompleteSelectFieldContent"; diff --git a/src/storybook/CardDecorator.tsx b/src/storybook/CardDecorator.tsx index f94b6ea87..6f113a514 100644 --- a/src/storybook/CardDecorator.tsx +++ b/src/storybook/CardDecorator.tsx @@ -7,6 +7,7 @@ const CardDecorator = storyFn => ( style={{ margin: "auto", overflow: "visible", + position: "relative", width: 400 }} > diff --git a/src/storybook/mock.tsx b/src/storybook/mock.tsx index 7e40ca5da..0e6b83f0f 100644 --- a/src/storybook/mock.tsx +++ b/src/storybook/mock.tsx @@ -1,36 +1,41 @@ import React from "react"; +import { SingleAutocompleteChoiceType } from "@saleor/components/SingleAutocompleteSelectField/SingleAutocompleteSelectFieldContent"; + interface ChoiceProviderProps { children: (props: { - choices: Array<{ - label: string; - value: string; - }>; + choices: SingleAutocompleteChoiceType[]; + hasMore: boolean; loading: boolean; - fetchChoices(value: string); + fetchChoices: (value: string) => void; + fetchMore: () => void; }) => React.ReactElement; - choices: Array<{ - label: string; - value: string; - }>; + 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: string) => { - if (this.state.loading) { + if (!!this.state.timeout) { clearTimeout(this.state.timeout); } const timeout = setTimeout(() => this.fetchChoices(inputValue), 500); @@ -40,16 +45,35 @@ export class ChoiceProvider extends React.Component< }); }; - fetchChoices = (inputValue: string) => { + handleFetchMore = () => { + if (!!this.state.timeout) { + clearTimeout(this.state.timeout); + } + const timeout = setTimeout(this.fetchMore, 500); this.setState({ - choices: this.props.choices - .filter( - suggestion => - !inputValue || - suggestion.label.toLowerCase().indexOf(inputValue.toLowerCase()) !== - -1 - ) - .slice(0, 10), + loading: true, + timeout + }); + }; + + 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 }); @@ -57,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 }); } From c33102b472acdf8e7a757bca28fe7d96130d1227 Mon Sep 17 00:00:00 2001 From: dominik-zeglen Date: Mon, 14 Oct 2019 16:32:53 +0200 Subject: [PATCH 03/18] Show shadow only when scrollable --- .../SingleAutocompleteSelectFieldContent.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/SingleAutocompleteSelectField/SingleAutocompleteSelectFieldContent.tsx b/src/components/SingleAutocompleteSelectField/SingleAutocompleteSelectFieldContent.tsx index 517852007..24507368c 100644 --- a/src/components/SingleAutocompleteSelectField/SingleAutocompleteSelectFieldContent.tsx +++ b/src/components/SingleAutocompleteSelectField/SingleAutocompleteSelectFieldContent.tsx @@ -221,7 +221,7 @@ const SingleAutocompleteSelectFieldContent: React.FC<
0 })} /> From ae14076ceb0805b382db82f7ea0915ec2a795dbc Mon Sep 17 00:00:00 2001 From: dominik-zeglen Date: Tue, 15 Oct 2019 12:33:14 +0200 Subject: [PATCH 04/18] Improve scrolling experience --- assets/images/ChevronDown.svg | 3 + .../SingleAutocompleteSelectField.stories.tsx | 1 - .../SingleAutocompleteSelectFieldContent.tsx | 59 ++++++++++++------- src/hooks/useElementScroll.ts | 22 +++++-- .../singleAutocompleteSelectChangeHandler.ts | 3 +- 5 files changed, 58 insertions(+), 30 deletions(-) create mode 100644 assets/images/ChevronDown.svg diff --git a/assets/images/ChevronDown.svg b/assets/images/ChevronDown.svg new file mode 100644 index 000000000..cc047ee72 --- /dev/null +++ b/assets/images/ChevronDown.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/SingleAutocompleteSelectField/SingleAutocompleteSelectField.stories.tsx b/src/components/SingleAutocompleteSelectField/SingleAutocompleteSelectField.stories.tsx index 8a4435bd6..b8c11b685 100644 --- a/src/components/SingleAutocompleteSelectField/SingleAutocompleteSelectField.stories.tsx +++ b/src/components/SingleAutocompleteSelectField/SingleAutocompleteSelectField.stories.tsx @@ -2,7 +2,6 @@ import { storiesOf } from "@storybook/react"; import React from "react"; import Form from "@saleor/components/Form"; -import { maybe } from "@saleor/misc"; import CardDecorator from "@saleor/storybook/CardDecorator"; import Decorator from "@saleor/storybook/Decorator"; import { ChoiceProvider } from "@saleor/storybook/mock"; diff --git a/src/components/SingleAutocompleteSelectField/SingleAutocompleteSelectFieldContent.tsx b/src/components/SingleAutocompleteSelectField/SingleAutocompleteSelectFieldContent.tsx index 24507368c..72bc9da2a 100644 --- a/src/components/SingleAutocompleteSelectField/SingleAutocompleteSelectFieldContent.tsx +++ b/src/components/SingleAutocompleteSelectField/SingleAutocompleteSelectFieldContent.tsx @@ -7,9 +7,13 @@ 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 useElementScroll from "@saleor/hooks/useElementScroll"; +import chevronDown from "@assets/images/ChevronDown.svg"; +import useElementScroll, { + isScrolledToBottom +} from "@saleor/hooks/useElementScroll"; import { FetchMoreProps } from "@saleor/types"; import Hr from "../Hr"; @@ -35,11 +39,29 @@ export interface SingleAutocompleteSelectFieldContentProps 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` }, @@ -53,22 +75,14 @@ const useStyles = makeStyles( justifyContent: "center" }, root: { - borderRadius: 4, + borderBottomLeftRadius: 8, + borderBottomRightRadius: 8, left: 0, marginTop: theme.spacing.unit, + overflow: "hidden", position: "absolute", right: 0, zIndex: 22 - }, - shadow: { - "&$shadowLine": { - boxShadow: `0px -5px 10px 0px ${theme.palette.grey[800]}` - } - }, - shadowLine: { - boxShadow: `0px 0px 0px 0px ${theme.palette.grey[50]}`, - height: 1, - transition: theme.transitions.duration.short + "ms" } }), { @@ -114,10 +128,7 @@ const SingleAutocompleteSelectFieldContent: React.FC< const scrollPosition = useElementScroll(anchor); const [calledForMore, setCalledForMore] = React.useState(false); - const scrolledToBottom = anchor.current - ? scrollPosition.y + anchor.current.clientHeight + offset >= - anchor.current.scrollHeight - : false; + const scrolledToBottom = isScrolledToBottom(anchor, scrollPosition, 50); React.useEffect(() => { if (!calledForMore && onFetchMore && scrolledToBottom) { @@ -133,7 +144,7 @@ const SingleAutocompleteSelectFieldContent: React.FC< }, [loading]); return ( - +
{choices.length > 0 || displayCustomValue ? ( <> @@ -219,11 +230,15 @@ const SingleAutocompleteSelectFieldContent: React.FC< )}
-
0 - })} - /> +
+
0 + })} + > + +
+
); }; diff --git a/src/hooks/useElementScroll.ts b/src/hooks/useElementScroll.ts index 0804d824e..83392800c 100644 --- a/src/hooks/useElementScroll.ts +++ b/src/hooks/useElementScroll.ts @@ -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) { +export function isScrolledToBottom( + anchor: MutableRefObject, + position: Position, + offset: number = 0 +) { + return !!anchor.current && position + ? position.y + anchor.current.clientHeight + offset >= + anchor.current.scrollHeight + : false; +} + +function useElementScroll(anchor: MutableRefObject): Position { const [scroll, setScroll] = useState(getPosition(anchor.current)); useEffect(() => { diff --git a/src/utils/handlers/singleAutocompleteSelectChangeHandler.ts b/src/utils/handlers/singleAutocompleteSelectChangeHandler.ts index ee2265185..1f95c8a54 100644 --- a/src/utils/handlers/singleAutocompleteSelectChangeHandler.ts +++ b/src/utils/handlers/singleAutocompleteSelectChangeHandler.ts @@ -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); }; } From 1a58d4ab8005c37721ca54465d3ec5437c13bb55 Mon Sep 17 00:00:00 2001 From: dominik-zeglen Date: Tue, 15 Oct 2019 14:12:38 +0200 Subject: [PATCH 05/18] Set offset --- .../SingleAutocompleteSelectFieldContent.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/SingleAutocompleteSelectField/SingleAutocompleteSelectFieldContent.tsx b/src/components/SingleAutocompleteSelectField/SingleAutocompleteSelectFieldContent.tsx index 72bc9da2a..01580a40a 100644 --- a/src/components/SingleAutocompleteSelectField/SingleAutocompleteSelectFieldContent.tsx +++ b/src/components/SingleAutocompleteSelectField/SingleAutocompleteSelectFieldContent.tsx @@ -128,7 +128,7 @@ const SingleAutocompleteSelectFieldContent: React.FC< const scrollPosition = useElementScroll(anchor); const [calledForMore, setCalledForMore] = React.useState(false); - const scrolledToBottom = isScrolledToBottom(anchor, scrollPosition, 50); + const scrolledToBottom = isScrolledToBottom(anchor, scrollPosition, offset); React.useEffect(() => { if (!calledForMore && onFetchMore && scrolledToBottom) { From d295804778c0df6e8c3ddfc0d933ebb398a7c16e Mon Sep 17 00:00:00 2001 From: dominik-zeglen Date: Tue, 15 Oct 2019 14:17:35 +0200 Subject: [PATCH 06/18] Refactor searches --- src/collections/views/CollectionDetails.tsx | 2 +- .../AssignCategoryDialog.tsx | 18 ++- .../AssignCollectionDialog.tsx | 18 ++- .../AssignProductDialog.tsx | 16 +-- src/containers/BaseSearch.tsx | 41 ++++--- src/containers/SearchCategories/index.tsx | 11 +- .../types/SearchCategories.ts | 21 +++- src/containers/SearchCollections/index.tsx | 11 +- .../types/SearchCollections.ts | 21 +++- src/containers/SearchCustomers/index.ts | 11 +- .../SearchCustomers/types/SearchCustomers.ts | 21 +++- src/containers/SearchPages/index.tsx | 11 +- .../SearchPages/types/SearchPages.ts | 21 +++- src/containers/SearchProductTypes/index.tsx | 15 ++- .../types/SearchProductTypes.ts | 29 +++-- src/containers/SearchProducts/index.tsx | 11 +- .../SearchProducts/types/SearchProducts.ts | 25 ++-- src/containers/TopLevelSearch.tsx | 47 +++++++ src/discounts/views/SaleDetails.tsx | 6 +- src/discounts/views/VoucherDetails.tsx | 6 +- .../MenuItemDialog/MenuItemDialog.tsx | 12 +- src/navigation/views/MenuDetails/index.tsx | 6 +- .../OrderCustomer/OrderCustomer.tsx | 13 +- .../OrderDraftPage/OrderDraftPage.tsx | 19 +-- .../OrderProductAddDialog.tsx | 28 ++--- src/orders/fixtures.ts | 4 +- src/orders/queries.ts | 6 +- src/orders/types/SearchOrderVariant.ts | 28 ++--- src/orders/views/OrderDetails/index.tsx | 116 ++++++++---------- .../containers/SearchAttributes/index.tsx | 22 +++- src/productTypes/fixtures.ts | 9 +- .../ProductCreatePage/ProductCreatePage.tsx | 12 +- .../ProductUpdatePage/ProductUpdatePage.tsx | 8 +- src/products/utils/data.ts | 4 +- src/products/views/ProductCreate.tsx | 6 +- .../views/ProductUpdate/ProductUpdate.tsx | 4 +- src/queries.tsx | 2 +- .../stories/orders/OrderDraftPage.tsx | 2 + 38 files changed, 409 insertions(+), 254 deletions(-) create mode 100644 src/containers/TopLevelSearch.tsx diff --git a/src/collections/views/CollectionDetails.tsx b/src/collections/views/CollectionDetails.tsx index 9d1668512..085bb051c 100644 --- a/src/collections/views/CollectionDetails.tsx +++ b/src/collections/views/CollectionDetails.tsx @@ -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) )} diff --git a/src/components/AssignCategoryDialog/AssignCategoryDialog.tsx b/src/components/AssignCategoryDialog/AssignCategoryDialog.tsx index a6e76df8a..257b7a941 100644 --- a/src/components/AssignCategoryDialog/AssignCategoryDialog.tsx +++ b/src/components/AssignCategoryDialog/AssignCategoryDialog.tsx @@ -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 { - 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); diff --git a/src/components/AssignCollectionDialog/AssignCollectionDialog.tsx b/src/components/AssignCollectionDialog/AssignCollectionDialog.tsx index 388ae5a7b..16c4e52e7 100644 --- a/src/components/AssignCollectionDialog/AssignCollectionDialog.tsx +++ b/src/components/AssignCollectionDialog/AssignCollectionDialog.tsx @@ -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 { - 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); diff --git a/src/components/AssignProductDialog/AssignProductDialog.tsx b/src/components/AssignProductDialog/AssignProductDialog.tsx index fe8ee4613..d52de901d 100644 --- a/src/components/AssignProductDialog/AssignProductDialog.tsx +++ b/src/components/AssignProductDialog/AssignProductDialog.tsx @@ -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); diff --git a/src/containers/BaseSearch.tsx b/src/containers/BaseSearch.tsx index 0742edbc1..4562db65b 100644 --- a/src/containers/BaseSearch.tsx +++ b/src/containers/BaseSearch.tsx @@ -10,26 +10,30 @@ export interface SearchQueryVariables { query: string; } +interface BaseSearchProps< + TQuery, + TQueryVariables extends SearchQueryVariables +> { + children: (props: { + loadMore: () => void; + search: (query: string) => void; + result: TypedQueryResult; + }) => React.ReactElement; + variables: TQueryVariables; +} + function BaseSearch( - query: DocumentNode + query: DocumentNode, + loadMoreFn: (result: TypedQueryResult) => void ) { const Query = TypedQuery(query); - interface BaseSearchProps { - children: (props: { - search: (query: string) => void; - result: TypedQueryResult; - }) => React.ReactElement; - variables: TQueryVariables; - } - interface BaseSearchState { - query: string; - } class BaseSearchComponent extends React.Component< - BaseSearchProps, - BaseSearchState + BaseSearchProps, + SearchQueryVariables > { - state: BaseSearchState = { + state: SearchQueryVariables = { + first: this.props.variables.first, query: this.props.variables.query }; @@ -54,7 +58,13 @@ function BaseSearch( query: this.state.query }} > - {result => children({ search, result })} + {result => + children({ + loadMore: () => loadMoreFn(result), + result, + search + }) + } )} @@ -63,4 +73,5 @@ function BaseSearch( } return BaseSearchComponent; } + export default BaseSearch; diff --git a/src/containers/SearchCategories/index.tsx b/src/containers/SearchCategories/index.tsx index 0179c7e58..cdaebb8b1 100644 --- a/src/containers/SearchCategories/index.tsx +++ b/src/containers/SearchCategories/index.tsx @@ -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( +export default TopLevelSearch( searchCategories ); diff --git a/src/containers/SearchCategories/types/SearchCategories.ts b/src/containers/SearchCategories/types/SearchCategories.ts index 0bd976f19..8b05ba28a 100644 --- a/src/containers/SearchCategories/types/SearchCategories.ts +++ b/src/containers/SearchCategories/types/SearchCategories.ts @@ -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 { diff --git a/src/containers/SearchCollections/index.tsx b/src/containers/SearchCollections/index.tsx index 54f2177f9..2ceafcb46 100644 --- a/src/containers/SearchCollections/index.tsx +++ b/src/containers/SearchCollections/index.tsx @@ -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( +export default TopLevelSearch( searchCollections ); diff --git a/src/containers/SearchCollections/types/SearchCollections.ts b/src/containers/SearchCollections/types/SearchCollections.ts index ce9c70cca..18718886b 100644 --- a/src/containers/SearchCollections/types/SearchCollections.ts +++ b/src/containers/SearchCollections/types/SearchCollections.ts @@ -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 { diff --git a/src/containers/SearchCustomers/index.ts b/src/containers/SearchCustomers/index.ts index 2ed50f4a2..0164f7826 100644 --- a/src/containers/SearchCustomers/index.ts +++ b/src/containers/SearchCustomers/index.ts @@ -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( +export default TopLevelSearch( searchCustomers ); diff --git a/src/containers/SearchCustomers/types/SearchCustomers.ts b/src/containers/SearchCustomers/types/SearchCustomers.ts index ad51c10ab..c63a1a4b6 100644 --- a/src/containers/SearchCustomers/types/SearchCustomers.ts +++ b/src/containers/SearchCustomers/types/SearchCustomers.ts @@ -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 { diff --git a/src/containers/SearchPages/index.tsx b/src/containers/SearchPages/index.tsx index 70bf13053..bd6ce287e 100644 --- a/src/containers/SearchPages/index.tsx +++ b/src/containers/SearchPages/index.tsx @@ -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); +export default TopLevelSearch(searchPages); diff --git a/src/containers/SearchPages/types/SearchPages.ts b/src/containers/SearchPages/types/SearchPages.ts index 7357bb965..7ecfed264 100644 --- a/src/containers/SearchPages/types/SearchPages.ts +++ b/src/containers/SearchPages/types/SearchPages.ts @@ -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 { diff --git a/src/containers/SearchProductTypes/index.tsx b/src/containers/SearchProductTypes/index.tsx index 6cc389137..6adf1ef5a 100644 --- a/src/containers/SearchProductTypes/index.tsx +++ b/src/containers/SearchProductTypes/index.tsx @@ -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( +export default TopLevelSearch( searchProductTypes ); diff --git a/src/containers/SearchProductTypes/types/SearchProductTypes.ts b/src/containers/SearchProductTypes/types/SearchProductTypes.ts index f242be391..2aaf2fe5e 100644 --- a/src/containers/SearchProductTypes/types/SearchProductTypes.ts +++ b/src/containers/SearchProductTypes/types/SearchProductTypes.ts @@ -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 { diff --git a/src/containers/SearchProducts/index.tsx b/src/containers/SearchProducts/index.tsx index c5029eff3..ce86cb09b 100644 --- a/src/containers/SearchProducts/index.tsx +++ b/src/containers/SearchProducts/index.tsx @@ -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( +export default TopLevelSearch( searchProducts ); diff --git a/src/containers/SearchProducts/types/SearchProducts.ts b/src/containers/SearchProducts/types/SearchProducts.ts index fdafa6e10..3a944d21c 100644 --- a/src/containers/SearchProducts/types/SearchProducts.ts +++ b/src/containers/SearchProducts/types/SearchProducts.ts @@ -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 { diff --git a/src/containers/TopLevelSearch.tsx b/src/containers/TopLevelSearch.tsx new file mode 100644 index 000000000..85a90a301 --- /dev/null +++ b/src/containers/TopLevelSearch.tsx @@ -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(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; diff --git a/src/discounts/views/SaleDetails.tsx b/src/discounts/views/SaleDetails.tsx index 83f17b7a6..5f5c4b353 100644 --- a/src/discounts/views/SaleDetails.tsx +++ b/src/discounts/views/SaleDetails.tsx @@ -371,7 +371,7 @@ export const SaleDetails: React.StatelessComponent = ({ }) } 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 = ({ }) => ( - searchCategoriesOpts.data.categories.edges + searchCategoriesOpts.data.search.edges .map(edge => edge.node) .filter( suggestedCategory => @@ -426,7 +426,7 @@ export const SaleDetails: React.StatelessComponent = ({ }) => ( - searchCollectionsOpts.data.collections.edges + searchCollectionsOpts.data.search.edges .map(edge => edge.node) .filter( suggestedCategory => diff --git a/src/discounts/views/VoucherDetails.tsx b/src/discounts/views/VoucherDetails.tsx index 2b438c614..b72fe84be 100644 --- a/src/discounts/views/VoucherDetails.tsx +++ b/src/discounts/views/VoucherDetails.tsx @@ -429,7 +429,7 @@ export const VoucherDetails: React.StatelessComponent = ({ }) => ( - searchCategoriesOpts.data.categories.edges + searchCategoriesOpts.data.search.edges .map(edge => edge.node) .filter( suggestedCategory => @@ -466,7 +466,7 @@ export const VoucherDetails: React.StatelessComponent = ({ }) => ( - searchCollectionsOpts.data.collections.edges + searchCollectionsOpts.data.search.edges .map(edge => edge.node) .filter( suggestedCategory => @@ -544,7 +544,7 @@ export const VoucherDetails: React.StatelessComponent = ({ }) } products={maybe(() => - searchProductsOpts.data.products.edges + searchProductsOpts.data.search.edges .map(edge => edge.node) .filter( suggestedProduct => suggestedProduct.id diff --git a/src/navigation/components/MenuItemDialog/MenuItemDialog.tsx b/src/navigation/components/MenuItemDialog/MenuItemDialog.tsx index 545d0c642..e4e5aeb4f 100644 --- a/src/navigation/components/MenuItemDialog/MenuItemDialog.tsx +++ b/src/navigation/components/MenuItemDialog/MenuItemDialog.tsx @@ -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; diff --git a/src/navigation/views/MenuDetails/index.tsx b/src/navigation/views/MenuDetails/index.tsx index a24a782cf..eb4f5b801 100644 --- a/src/navigation/views/MenuDetails/index.tsx +++ b/src/navigation/views/MenuDetails/index.tsx @@ -111,7 +111,7 @@ const MenuDetails: React.FC = ({ 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 = ({ 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 = ({ id, params }) => { const pages = maybe( () => - pageSearch.result.data.pages.edges.map( + pageSearch.result.data.search.edges.map( edge => edge.node ), [] diff --git a/src/orders/components/OrderCustomer/OrderCustomer.tsx b/src/orders/components/OrderCustomer/OrderCustomer.tsx index 55fadd593..ab07c3220 100644 --- a/src/orders/components/OrderCustomer/OrderCustomer.tsx +++ b/src/orders/components/OrderCustomer/OrderCustomer.tsx @@ -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 { +export interface OrderCustomerProps extends Partial { 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) => { 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} /> diff --git a/src/orders/components/OrderDraftPage/OrderDraftPage.tsx b/src/orders/components/OrderDraftPage/OrderDraftPage.tsx index 4afab620c..92518ea54 100644 --- a/src/orders/components/OrderDraftPage/OrderDraftPage.tsx +++ b/src/orders/components/OrderDraftPage/OrderDraftPage.tsx @@ -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 { +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) => { const intl = useIntl(); return ( @@ -139,14 +142,16 @@ const OrderDraftPage = withStyles(styles, { name: "OrderDraftPage" })(
diff --git a/src/orders/components/OrderProductAddDialog/OrderProductAddDialog.tsx b/src/orders/components/OrderProductAddDialog/OrderProductAddDialog.tsx index 767e01810..f96fed5b5 100644 --- a/src/orders/components/OrderProductAddDialog/OrderProductAddDialog.tsx +++ b/src/orders/components/OrderProductAddDialog/OrderProductAddDialog.tsx @@ -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 diff --git a/src/orders/fixtures.ts b/src/orders/fixtures.ts index 4403c6939..af8a13c45 100644 --- a/src/orders/fixtures.ts +++ b/src/orders/fixtures.ts @@ -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", diff --git a/src/orders/queries.ts b/src/orders/queries.ts index 2eab79cd8..47a6add2f 100644 --- a/src/orders/queries.ts +++ b/src/orders/queries.ts @@ -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); diff --git a/src/orders/types/SearchOrderVariant.ts b/src/orders/types/SearchOrderVariant.ts index f2f0949eb..156de4604 100644 --- a/src/orders/types/SearchOrderVariant.ts +++ b/src/orders/types/SearchOrderVariant.ts @@ -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 { diff --git a/src/orders/views/OrderDetails/index.tsx b/src/orders/views/OrderDetails/index.tsx index 8a4052c42..36ee9afd5 100644 --- a/src/orders/views/OrderDetails/index.tsx +++ b/src/orders/views/OrderDetails/index.tsx @@ -93,7 +93,11 @@ export const OrderDetails: React.StatelessComponent = ({ ); return ( - {({ search: searchUsers, result: users }) => ( + {({ + loadMore: loadMoreCustomers, + search: searchUsers, + result: users + }) => ( {orderMessages => ( = ({ } users={maybe( () => - users.data.customers.edges.map( + users.data.search.edges.map( edge => edge.node ), [] )} + hasMore={maybe( + () => users.data.search.pageInfo.hasNextPage, + false + )} + onFetchMore={loadMoreCustomers} fetchUsers={searchUsers} + loading={users.loading} usersLoading={users.loading} onCustomerEdit={data => orderDraftUpdate.mutate({ @@ -511,74 +521,46 @@ export const OrderDetails: React.StatelessComponent = ({ 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 ( - - orderLinesAdd.opts.data - .draftOrderLinesCreate.errors - ) - )} - loading={variantSearchOpts.loading} - open={params.action === "add-order-line"} - hasMore={maybe( + }) => ( + - variantSearchOpts.data.products - .pageInfo.hasNextPage - )} - products={maybe(() => - variantSearchOpts.data.products.edges.map( - edge => edge.node - ) - )} - onClose={closeModal} - onFetch={variantSearch} - onFetchMore={fetchMore} - onSubmit={variants => - orderLinesAdd.mutate({ - id, - input: variants.map(variant => ({ - quantity: 1, - variantId: variant.id - })) - }) - } - /> - ); - }} + orderLinesAdd.opts.data + .draftOrderLinesCreate.errors + ) + )} + loading={variantSearchOpts.loading} + open={params.action === "add-order-line"} + hasMore={maybe( + () => + variantSearchOpts.data.search.pageInfo + .hasNextPage + )} + products={maybe(() => + variantSearchOpts.data.search.edges.map( + edge => edge.node + ) + )} + onClose={closeModal} + onFetch={variantSearch} + onFetchMore={loadMore} + onSubmit={variants => + orderLinesAdd.mutate({ + id, + input: variants.map(variant => ({ + quantity: 1, + variantId: variant.id + })) + }) + } + /> + )} )} diff --git a/src/productTypes/containers/SearchAttributes/index.tsx b/src/productTypes/containers/SearchAttributes/index.tsx index 6af8faa86..8f00cbf5b 100644 --- a/src/productTypes/containers/SearchAttributes/index.tsx +++ b/src/productTypes/containers/SearchAttributes/index.tsx @@ -38,5 +38,25 @@ export const searchAttributes = gql` `; export default BaseSearch( - 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 + } + ) ); diff --git a/src/productTypes/fixtures.ts b/src/productTypes/fixtures.ts index 375f1b751..17ef73a82 100644 --- a/src/productTypes/fixtures.ts +++ b/src/productTypes/fixtures.ts @@ -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", diff --git a/src/products/components/ProductCreatePage/ProductCreatePage.tsx b/src/products/components/ProductCreatePage/ProductCreatePage.tsx index 3874ae966..d1f6da571 100644 --- a/src/products/components/ProductCreatePage/ProductCreatePage.tsx +++ b/src/products/components/ProductCreatePage/ProductCreatePage.tsx @@ -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"; @@ -63,15 +63,15 @@ 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; productTypes?: Array<{ id: string; name: string; hasVariants: boolean; - productAttributes: SearchProductTypes_productTypes_edges_node_productAttributes[]; + productAttributes: SearchProductTypes_search_edges_node_productAttributes[]; }>; header: string; saveButtonBarState: ConfirmButtonTransitionState; diff --git a/src/products/components/ProductUpdatePage/ProductUpdatePage.tsx b/src/products/components/ProductUpdatePage/ProductUpdatePage.tsx index f341f5875..4604a0116 100644 --- a/src/products/components/ProductUpdatePage/ProductUpdatePage.tsx +++ b/src/products/components/ProductUpdatePage/ProductUpdatePage.tsx @@ -12,8 +12,8 @@ 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"; @@ -50,8 +50,8 @@ 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; variants: ProductDetails_product_variants[]; images: ProductDetails_product_images[]; diff --git a/src/products/utils/data.ts b/src/products/utils/data.ts index 56d48658b..b1fc302fb 100644 --- a/src/products/utils/data.ts +++ b/src/products/utils/data.ts @@ -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( diff --git a/src/products/views/ProductCreate.tsx b/src/products/views/ProductCreate.tsx index aac14ea19..2a2e97fdb 100644 --- a/src/products/views/ProductCreate.tsx +++ b/src/products/views/ProductCreate.tsx @@ -121,11 +121,11 @@ export const ProductUpdate: React.StatelessComponent< 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,7 +141,7 @@ export const ProductUpdate: React.StatelessComponent< description: "page header" })} productTypes={maybe(() => - searchProductTypesOpts.data.productTypes.edges.map( + searchProductTypesOpts.data.search.edges.map( edge => edge.node ) )} diff --git a/src/products/views/ProductUpdate/ProductUpdate.tsx b/src/products/views/ProductUpdate/ProductUpdate.tsx index 8f5d3fffb..6889dc023 100644 --- a/src/products/views/ProductUpdate/ProductUpdate.tsx +++ b/src/products/views/ProductUpdate/ProductUpdate.tsx @@ -228,11 +228,11 @@ export const ProductUpdate: React.StatelessComponent = ({ ); 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( diff --git a/src/queries.tsx b/src/queries.tsx index e3bdda8cd..56a979ce8 100644 --- a/src/queries.tsx +++ b/src/queries.tsx @@ -15,7 +15,7 @@ import { RequireAtLeastOne } from "./misc"; export interface LoadMore { loadMore: ( mergeFunc: (prev: TData, next: TData) => TData, - extraVariables: RequireAtLeastOne + extraVariables: Partial ) => Promise>; } diff --git a/src/storybook/stories/orders/OrderDraftPage.tsx b/src/storybook/stories/orders/OrderDraftPage.tsx index db70be0a4..382a3b94e 100644 --- a/src/storybook/stories/orders/OrderDraftPage.tsx +++ b/src/storybook/stories/orders/OrderDraftPage.tsx @@ -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 = { + ...fetchMoreProps, countries, disabled: false, fetchUsers: () => undefined, From 1de5f7435d3f55334fbc79e21c5a028ee48e726c Mon Sep 17 00:00:00 2001 From: dominik-zeglen Date: Tue, 15 Oct 2019 14:33:35 +0200 Subject: [PATCH 07/18] Show arrow before scrolling --- .../SingleAutocompleteSelectFieldContent.tsx | 4 +++- src/hooks/useElementScroll.ts | 6 +++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/components/SingleAutocompleteSelectField/SingleAutocompleteSelectFieldContent.tsx b/src/components/SingleAutocompleteSelectField/SingleAutocompleteSelectFieldContent.tsx index 01580a40a..93a012993 100644 --- a/src/components/SingleAutocompleteSelectField/SingleAutocompleteSelectFieldContent.tsx +++ b/src/components/SingleAutocompleteSelectField/SingleAutocompleteSelectFieldContent.tsx @@ -233,7 +233,9 @@ const SingleAutocompleteSelectFieldContent: React.FC<
0 + // Needs to be explicitely compared to false because + // scrolledToBottom can be either true, false or undefined + [classes.hide]: scrolledToBottom !== false })} > diff --git a/src/hooks/useElementScroll.ts b/src/hooks/useElementScroll.ts index 83392800c..925a8bba4 100644 --- a/src/hooks/useElementScroll.ts +++ b/src/hooks/useElementScroll.ts @@ -21,7 +21,7 @@ export function isScrolledToBottom( return !!anchor.current && position ? position.y + anchor.current.clientHeight + offset >= anchor.current.scrollHeight - : false; + : undefined; } function useElementScroll(anchor: MutableRefObject): Position { @@ -39,6 +39,10 @@ function useElementScroll(anchor: MutableRefObject): Position { } }, [anchor.current]); + useEffect(() => { + setTimeout(() => setScroll(getPosition(anchor.current)), 100); + }, []); + return scroll; } export default useElementScroll; From 7ebf8fc254aace982a43ac0348a16ccc84df33eb Mon Sep 17 00:00:00 2001 From: dominik-zeglen Date: Tue, 15 Oct 2019 14:33:57 +0200 Subject: [PATCH 08/18] Remove unuxed component --- .../OrderCustomerEditDialog.tsx | 116 ------------------ .../OrderCustomerEditDialog/index.ts | 2 - src/storybook/config.js | 1 - .../orders/OrderCustomerEditDialog.tsx | 24 ---- 4 files changed, 143 deletions(-) delete mode 100644 src/orders/components/OrderCustomerEditDialog/OrderCustomerEditDialog.tsx delete mode 100644 src/orders/components/OrderCustomerEditDialog/index.ts delete mode 100644 src/storybook/stories/orders/OrderCustomerEditDialog.tsx diff --git a/src/orders/components/OrderCustomerEditDialog/OrderCustomerEditDialog.tsx b/src/orders/components/OrderCustomerEditDialog/OrderCustomerEditDialog.tsx deleted file mode 100644 index 891e901c7..000000000 --- a/src/orders/components/OrderCustomerEditDialog/OrderCustomerEditDialog.tsx +++ /dev/null @@ -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 { - confirmButtonState: ConfirmButtonTransitionState; - open: boolean; - user: string; - userDisplayValue: string; - users?: Array<{ - id: string; - email: string; - }>; - loading?: boolean; - fetchUsers(value: string); - onChange(event: React.ChangeEvent); - onClose?(); - onConfirm?(event: React.FormEvent); -} - -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 ( - - - - - - - - - - - - - - - ); - } -); -OrderCustomerEditDialog.displayName = "OrderCustomerEditDialog"; -export default OrderCustomerEditDialog; diff --git a/src/orders/components/OrderCustomerEditDialog/index.ts b/src/orders/components/OrderCustomerEditDialog/index.ts deleted file mode 100644 index cb32cf9f6..000000000 --- a/src/orders/components/OrderCustomerEditDialog/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from "./OrderCustomerEditDialog"; -export * from "./OrderCustomerEditDialog"; diff --git a/src/storybook/config.js b/src/storybook/config.js index 9b7e2c7ec..83005935a 100644 --- a/src/storybook/config.js +++ b/src/storybook/config.js @@ -124,7 +124,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"); diff --git a/src/storybook/stories/orders/OrderCustomerEditDialog.tsx b/src/storybook/stories/orders/OrderCustomerEditDialog.tsx deleted file mode 100644 index 698292ebe..000000000 --- a/src/storybook/stories/orders/OrderCustomerEditDialog.tsx +++ /dev/null @@ -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", () => ( - - )); From 035271adf07054f716de6cb21ef29d943678d2f4 Mon Sep 17 00:00:00 2001 From: dominik-zeglen Date: Tue, 15 Oct 2019 15:11:38 +0200 Subject: [PATCH 09/18] Use selects with loadMore option --- .../SingleAutocompleteSelectField.tsx | 1 + .../ProductCreatePage/ProductCreatePage.tsx | 11 ++++- .../ProductOrganization.tsx | 10 ++++- src/products/views/ProductCreate.tsx | 40 ++++++++++++++++++- 4 files changed, 58 insertions(+), 4 deletions(-) diff --git a/src/components/SingleAutocompleteSelectField/SingleAutocompleteSelectField.tsx b/src/components/SingleAutocompleteSelectField/SingleAutocompleteSelectField.tsx index b48793371..395e9905f 100644 --- a/src/components/SingleAutocompleteSelectField/SingleAutocompleteSelectField.tsx +++ b/src/components/SingleAutocompleteSelectField/SingleAutocompleteSelectField.tsx @@ -117,6 +117,7 @@ const SingleAutocompleteSelectFieldComponent = withStyles(styles, { } const displayCustomValue = + inputValue && inputValue.length > 0 && allowCustomValues && !choices.find( diff --git a/src/products/components/ProductCreatePage/ProductCreatePage.tsx b/src/products/components/ProductCreatePage/ProductCreatePage.tsx index d1f6da571..399839ca2 100644 --- a/src/products/components/ProductCreatePage/ProductCreatePage.tsx +++ b/src/products/components/ProductCreatePage/ProductCreatePage.tsx @@ -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, @@ -67,6 +67,9 @@ interface ProductCreatePageProps { categories: SearchCategories_search_edges_node[]; currency: string; disabled: boolean; + fetchMoreCategories: FetchMoreProps; + fetchMoreCollections: FetchMoreProps; + fetchMoreProductTypes: FetchMoreProps; productTypes?: Array<{ id: string; name: string; @@ -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} diff --git a/src/products/components/ProductOrganization/ProductOrganization.tsx b/src/products/components/ProductOrganization/ProductOrganization.tsx index 21b3932b9..705ec8af7 100644 --- a/src/products/components/ProductOrganization/ProductOrganization.tsx +++ b/src/products/components/ProductOrganization/ProductOrganization.tsx @@ -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 { 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} />
diff --git a/src/products/views/ProductCreate.tsx b/src/products/views/ProductCreate.tsx index 2a2e97fdb..8023ac60a 100644 --- a/src/products/views/ProductCreate.tsx +++ b/src/products/views/ProductCreate.tsx @@ -33,11 +33,20 @@ export const ProductUpdate: React.StatelessComponent< return ( - {({ search: searchCategory, result: searchCategoryOpts }) => ( + {({ + loadMore: loadMoreCategories, + search: searchCategory, + result: searchCategoryOpts + }) => ( - {({ search: searchCollection, result: searchCollectionOpts }) => ( + {({ + loadMore: loadMoreCollections, + search: searchCollection, + result: searchCollectionOpts + }) => ( {({ + loadMore: loadMoreProductTypes, search: searchProductTypes, result: searchProductTypesOpts }) => { @@ -148,6 +157,33 @@ export const ProductUpdate: React.StatelessComponent< 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 + }} /> ); From 1f3d3be072630515cd79b9f56985bcd07ad93121 Mon Sep 17 00:00:00 2001 From: dominik-zeglen Date: Tue, 15 Oct 2019 15:20:36 +0200 Subject: [PATCH 10/18] Fix types --- .../ProductOrganization.tsx | 1 + .../ProductUpdatePage/ProductUpdatePage.tsx | 8 ++++- .../views/ProductUpdate/ProductUpdate.tsx | 30 +++++++++++++++++-- .../stories/products/ProductCreatePage.tsx | 14 +++++++-- .../stories/products/ProductUpdatePage.tsx | 4 ++- 5 files changed, 50 insertions(+), 7 deletions(-) diff --git a/src/products/components/ProductOrganization/ProductOrganization.tsx b/src/products/components/ProductOrganization/ProductOrganization.tsx index 705ec8af7..c52a85bf1 100644 --- a/src/products/components/ProductOrganization/ProductOrganization.tsx +++ b/src/products/components/ProductOrganization/ProductOrganization.tsx @@ -188,6 +188,7 @@ const ProductOrganization = withStyles(styles, { name: "ProductOrganization" })( onChange={onCollectionChange} fetchChoices={fetchCollections} data-tc="collections" + {...fetchMoreCollections} /> diff --git a/src/products/components/ProductUpdatePage/ProductUpdatePage.tsx b/src/products/components/ProductUpdatePage/ProductUpdatePage.tsx index 4604a0116..0859c6d4a 100644 --- a/src/products/components/ProductUpdatePage/ProductUpdatePage.tsx +++ b/src/products/components/ProductUpdatePage/ProductUpdatePage.tsx @@ -19,7 +19,7 @@ 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 { @@ -53,6 +53,8 @@ export interface ProductUpdatePageProps extends ListActions { 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 = ({ errors: userErrors, fetchCategories, fetchCollections, + fetchMoreCategories, + fetchMoreCollections, images, header, placeholderImage, @@ -285,6 +289,8 @@ export const ProductUpdatePage: React.FC = ({ errors={errors} fetchCategories={fetchCategories} fetchCollections={fetchCollections} + fetchMoreCategories={fetchMoreCategories} + fetchMoreCollections={fetchMoreCollections} productType={maybe(() => product.productType)} onCategoryChange={handleCategorySelect} onCollectionChange={handleCollectionSelect} diff --git a/src/products/views/ProductUpdate/ProductUpdate.tsx b/src/products/views/ProductUpdate/ProductUpdate.tsx index 6889dc023..360acbd12 100644 --- a/src/products/views/ProductUpdate/ProductUpdate.tsx +++ b/src/products/views/ProductUpdate/ProductUpdate.tsx @@ -68,9 +68,17 @@ export const ProductUpdate: React.StatelessComponent = ({ return ( - {({ search: searchCategories, result: searchCategoriesOpts }) => ( + {({ + loadMore: loadMoreCategories, + search: searchCategories, + result: searchCategoriesOpts + }) => ( - {({ search: searchCollections, result: searchCollectionsOpts }) => ( + {({ + loadMore: loadMoreCollections, + search: searchCollections, + result: searchCollectionsOpts + }) => ( = ({ 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 + }} /> 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} diff --git a/src/storybook/stories/products/ProductUpdatePage.tsx b/src/storybook/stories/products/ProductUpdatePage.tsx index a0d34b83c..84c5b0952 100644 --- a/src/storybook/stories/products/ProductUpdatePage.tsx +++ b/src/storybook/stories/products/ProductUpdatePage.tsx @@ -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, From 6c44769629cfee35e6d10a19555dc03fe3cdc7ab Mon Sep 17 00:00:00 2001 From: dominik-zeglen Date: Tue, 15 Oct 2019 17:23:33 +0200 Subject: [PATCH 11/18] Refactor multi autocomplete ux --- .../MultiAutocompleteSelectField.stories.tsx | 89 ++++++ .../MultiAutocompleteSelectField.tsx | 226 ++++---------- .../MultiAutocompleteSelectFieldContent.tsx | 291 ++++++++++++++++++ .../MultiAutocompleteSelectField/index.ts | 1 + .../SingleAutocompleteSelectField.stories.tsx | 2 +- .../SingleAutocompleteSelectField/fixtures.ts | 245 --------------- src/fixtures.ts | 248 ++++++++++++++- src/storybook/config.js | 1 - .../MultiAutocompleteSelectField.tsx | 69 ----- .../customers/CustomerAddressDialog.tsx | 5 +- 10 files changed, 683 insertions(+), 494 deletions(-) create mode 100644 src/components/MultiAutocompleteSelectField/MultiAutocompleteSelectField.stories.tsx create mode 100644 src/components/MultiAutocompleteSelectField/MultiAutocompleteSelectFieldContent.tsx delete mode 100644 src/components/SingleAutocompleteSelectField/fixtures.ts delete mode 100644 src/storybook/stories/components/MultiAutocompleteSelectField.tsx diff --git a/src/components/MultiAutocompleteSelectField/MultiAutocompleteSelectField.stories.tsx b/src/components/MultiAutocompleteSelectField/MultiAutocompleteSelectField.stories.tsx new file mode 100644 index 000000000..0beab562e --- /dev/null +++ b/src/components/MultiAutocompleteSelectField/MultiAutocompleteSelectField.stories.tsx @@ -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; + } + > +> = storyProps => { + const { change, data: countries } = useMultiAutocomplete([suggestions[0]]); + + return ( + + {({ choices, fetchChoices, fetchMore, hasMore, loading }) => ( + country.value) + .join(", ")}`} + onChange={event => change(event, choices)} + value={countries.map(country => country.value)} + loading={loading} + hasMore={storyProps.enableLoadMore ? hasMore : false} + onFetchMore={storyProps.enableLoadMore ? fetchMore : undefined} + {...storyProps} + /> + )} + + ); +}; + +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", () => ( + + )) + .add("can load more", () => ( + + )) + .add("no data", () => ( + + )) + .add("interactive", () => ) + .add("interactive with custom option", () => ( + + )) + .add("interactive with load more", () => ); diff --git a/src/components/MultiAutocompleteSelectField/MultiAutocompleteSelectField.tsx b/src/components/MultiAutocompleteSelectField/MultiAutocompleteSelectField.tsx index 46ebf6cdc..18b6607cb 100644 --- a/src/components/MultiAutocompleteSelectField/MultiAutocompleteSelectField.tsx +++ b/src/components/MultiAutocompleteSelectField/MultiAutocompleteSelectField.tsx @@ -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, @@ -13,26 +10,18 @@ import Typography from "@material-ui/core/Typography"; import CloseIcon from "@material-ui/icons/Close"; import Downshift, { ControllerStateAndHelpers } from "downshift"; 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 { 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) => { const handleSelect = ( @@ -155,7 +108,6 @@ export const MultiAutocompleteSelectFieldComponent = withStyles(styles, { target: { name, value: item } } as any); }; - const suggestions = choices.filter(choice => !value.includes(choice.value)); return ( <> @@ -171,123 +123,53 @@ export const MultiAutocompleteSelectFieldComponent = withStyles(styles, { toggleMenu, highlightedIndex, inputValue - }) => ( -
- - {loading ? ( - - ) : ( + }) => { + const displayCustomValue = + inputValue && + inputValue.length > 0 && + allowCustomValues && + !choices.find( + choice => + choice.label.toLowerCase() === inputValue.toLowerCase() + ); + + return ( +
+ - )} -
- ), - id: undefined, - onClick: toggleMenu - }} - helperText={helperText} - label={label} - fullWidth={true} - /> - {isOpen && (!!inputValue || !!choices.length) && ( - - {choices.length > 0 || - displayValues.length > 0 || - allowCustomValues ? ( - <> - {displayValues.map(value => ( - - - - {value.label} - - - ))} - {displayValues.length > 0 && suggestions.length > 0 && ( -
- )} - {suggestions.map((suggestion, index) => ( - - - - {suggestion.label} - - - ))} - {allowCustomValues && - inputValue && - !choices.find( - choice => - choice.label.toLowerCase() === - inputValue.toLowerCase() - ) && ( - - - - - - )} - - ) : ( - !loading && ( - - - - ) - )} -
- )} -
- )} +
+ ), + id: undefined, + onClick: toggleMenu + }} + helperText={helperText} + label={label} + fullWidth={true} + /> + {isOpen && (!!inputValue || !!choices.length) && ( + !value.includes(choice.value) + )} + displayCustomValue={displayCustomValue} + displayValues={displayValues} + getItemProps={getItemProps} + hasMore={hasMore} + highlightedIndex={highlightedIndex} + loading={loading} + inputValue={inputValue} + onFetchMore={onFetchMore} + /> + )} +
+ ); + }}
{displayValues.map(value => ( diff --git a/src/components/MultiAutocompleteSelectField/MultiAutocompleteSelectFieldContent.tsx b/src/components/MultiAutocompleteSelectField/MultiAutocompleteSelectFieldContent.tsx new file mode 100644 index 000000000..2b3f1445c --- /dev/null +++ b/src/components/MultiAutocompleteSelectField/MultiAutocompleteSelectFieldContent.tsx @@ -0,0 +1,291 @@ +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 { + 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(); + 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 ( + +
+ {choices.length > 0 || displayCustomValue ? ( + <> + {displayCustomValue && ( + + + + + )} + {(choices.length > 0 || displayValues.length > 0) && + displayCustomValue &&
} + {displayValues.map(value => ( + + + {value.label} + + ))} + {displayValues.length > 0 && choices.length > 0 && ( +
+ )} + {choices.map((suggestion, index) => { + const choiceIndex = getChoiceIndex( + index, + displayValues, + displayCustomValue + ); + + return ( + + + + {suggestion.label} + + + ); + })} + {hasMore && ( + <> +
+
+ +
+ + )} + + ) : ( + + + + )} +
+
+
+ +
+
+
+ ); +}; + +MultiAutocompleteSelectFieldContent.displayName = + "MultiAutocompleteSelectFieldContent"; +export default MultiAutocompleteSelectFieldContent; diff --git a/src/components/MultiAutocompleteSelectField/index.ts b/src/components/MultiAutocompleteSelectField/index.ts index e7e4cfd9b..8a33bf4b4 100644 --- a/src/components/MultiAutocompleteSelectField/index.ts +++ b/src/components/MultiAutocompleteSelectField/index.ts @@ -1,2 +1,3 @@ export { default } from "./MultiAutocompleteSelectField"; export * from "./MultiAutocompleteSelectField"; +export * from "./MultiAutocompleteSelectFieldContent"; diff --git a/src/components/SingleAutocompleteSelectField/SingleAutocompleteSelectField.stories.tsx b/src/components/SingleAutocompleteSelectField/SingleAutocompleteSelectField.stories.tsx index b8c11b685..4ce6428e8 100644 --- a/src/components/SingleAutocompleteSelectField/SingleAutocompleteSelectField.stories.tsx +++ b/src/components/SingleAutocompleteSelectField/SingleAutocompleteSelectField.stories.tsx @@ -2,11 +2,11 @@ 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 { countries } from "./fixtures"; import SingleAutocompleteSelectField, { SingleAutocompleteSelectFieldProps } from "./SingleAutocompleteSelectField"; diff --git a/src/components/SingleAutocompleteSelectField/fixtures.ts b/src/components/SingleAutocompleteSelectField/fixtures.ts deleted file mode 100644 index 77eefc369..000000000 --- a/src/components/SingleAutocompleteSelectField/fixtures.ts +++ /dev/null @@ -1,245 +0,0 @@ -export const countries = [ - { 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" } -]; diff --git a/src/fixtures.ts b/src/fixtures.ts index 2bc0090ca..9c3394411 100644 --- a/src/fixtures.ts +++ b/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 = { diff --git a/src/storybook/config.js b/src/storybook/config.js index 83005935a..c1d03a182 100644 --- a/src/storybook/config.js +++ b/src/storybook/config.js @@ -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"); diff --git a/src/storybook/stories/components/MultiAutocompleteSelectField.tsx b/src/storybook/stories/components/MultiAutocompleteSelectField.tsx deleted file mode 100644 index 0582992cc..000000000 --- a/src/storybook/stories/components/MultiAutocompleteSelectField.tsx +++ /dev/null @@ -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 -> = storyProps => { - const { change, data: countries } = useMultiAutocomplete([suggestions[0]]); - - return ( - - {({ choices, loading, fetchChoices }) => ( - country.label) - .join(", ")}`} - onChange={event => change(event, choices)} - value={countries.map(country => country.value)} - loading={loading} - {...storyProps} - /> - )} - - ); -}; - -storiesOf("Generics / MultiAutocompleteSelectField", module) - .addDecorator(CardDecorator) - .addDecorator(Decorator) - .add("with loaded data", () => ) - .add("with loading data", () => ) - .add("with custom option", () => ); diff --git a/src/storybook/stories/customers/CustomerAddressDialog.tsx b/src/storybook/stories/customers/CustomerAddressDialog.tsx index 73ce77a43..369448178 100644 --- a/src/storybook/stories/customers/CustomerAddressDialog.tsx +++ b/src/storybook/stories/customers/CustomerAddressDialog.tsx @@ -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, From f0d343dec6e24dee874c539e9fb6b28d5e5e4568 Mon Sep 17 00:00:00 2001 From: dominik-zeglen Date: Tue, 15 Oct 2019 17:34:13 +0200 Subject: [PATCH 12/18] Fix no results state --- .../MultiAutocompleteSelectFieldContent.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/MultiAutocompleteSelectField/MultiAutocompleteSelectFieldContent.tsx b/src/components/MultiAutocompleteSelectField/MultiAutocompleteSelectFieldContent.tsx index 2b3f1445c..276b7a5b7 100644 --- a/src/components/MultiAutocompleteSelectField/MultiAutocompleteSelectFieldContent.tsx +++ b/src/components/MultiAutocompleteSelectField/MultiAutocompleteSelectFieldContent.tsx @@ -176,7 +176,9 @@ const MultiAutocompleteSelectFieldContent: React.FC< return (
- {choices.length > 0 || displayCustomValue ? ( + {choices.length > 0 || + displayValues.length > 0 || + displayCustomValue ? ( <> {displayCustomValue && ( Date: Tue, 15 Oct 2019 17:46:19 +0200 Subject: [PATCH 13/18] Fix stories --- .../MultiAutocompleteSelectField.stories.tsx | 8 +- .../SingleAutocompleteSelectField.stories.tsx | 9 +- .../__snapshots__/Stories.test.ts.snap | 1988 +++++++++++++---- 3 files changed, 1520 insertions(+), 485 deletions(-) diff --git a/src/components/MultiAutocompleteSelectField/MultiAutocompleteSelectField.stories.tsx b/src/components/MultiAutocompleteSelectField/MultiAutocompleteSelectField.stories.tsx index 0beab562e..533326c51 100644 --- a/src/components/MultiAutocompleteSelectField/MultiAutocompleteSelectField.stories.tsx +++ b/src/components/MultiAutocompleteSelectField/MultiAutocompleteSelectField.stories.tsx @@ -32,7 +32,7 @@ const Story: React.FC< enableLoadMore: boolean; } > -> = storyProps => { +> = ({ allowCustomValues, enableLoadMore }) => { const { change, data: countries } = useMultiAutocomplete([suggestions[0]]); return ( @@ -49,9 +49,9 @@ const Story: React.FC< onChange={event => change(event, choices)} value={countries.map(country => country.value)} loading={loading} - hasMore={storyProps.enableLoadMore ? hasMore : false} - onFetchMore={storyProps.enableLoadMore ? fetchMore : undefined} - {...storyProps} + hasMore={enableLoadMore ? hasMore : false} + onFetchMore={enableLoadMore ? fetchMore : undefined} + allowCustomValues={allowCustomValues} /> )} diff --git a/src/components/SingleAutocompleteSelectField/SingleAutocompleteSelectField.stories.tsx b/src/components/SingleAutocompleteSelectField/SingleAutocompleteSelectField.stories.tsx index 4ce6428e8..1d8468f30 100644 --- a/src/components/SingleAutocompleteSelectField/SingleAutocompleteSelectField.stories.tsx +++ b/src/components/SingleAutocompleteSelectField/SingleAutocompleteSelectField.stories.tsx @@ -33,7 +33,7 @@ const Story: React.FC< enableLoadMore: boolean; } > -> = storyProps => { +> = ({ allowCustomValues, emptyOption, enableLoadMore }) => { const [displayValue, setDisplayValue] = React.useState(suggestions[0].label); return ( @@ -57,9 +57,10 @@ const Story: React.FC< loading={loading} onChange={handleSelect} value={data.country} - hasMore={storyProps.enableLoadMore ? hasMore : false} - onFetchMore={storyProps.enableLoadMore ? fetchMore : undefined} - {...storyProps} + hasMore={enableLoadMore ? hasMore : false} + onFetchMore={enableLoadMore ? fetchMore : undefined} + allowCustomValues={allowCustomValues} + emptyOption={emptyOption} /> ); }} diff --git a/src/storybook/__snapshots__/Stories.test.ts.snap b/src/storybook/__snapshots__/Stories.test.ts.snap index afe0e6323..7f8731f37 100644 --- a/src/storybook/__snapshots__/Stories.test.ts.snap +++ b/src/storybook/__snapshots__/Stories.test.ts.snap @@ -497,7 +497,7 @@ exports[`Storyshots Generics / AddressFormatter default 1`] = ` >
`; -exports[`Storyshots Generics / MultiAutocompleteSelectField with custom option 1`] = ` -
-
-
-
-
- - -

- Value: Afghanistan -

-
-
-
-
-
-

- Afghanistan -

- -
-
-
-
-
-
-`; - -exports[`Storyshots Generics / MultiAutocompleteSelectField with loaded data 1`] = ` -
-
-
-
-
- - -

- Value: Afghanistan -

-
-
-
-
-
-

- Afghanistan -

- -
-
-
-
-
-
-`; - -exports[`Storyshots Generics / MultiAutocompleteSelectField with loading data 1`] = ` -
-
-
-
-
- - -

- Value: Afghanistan -

-
-
-
-
-
-

- Afghanistan -

- -
-
-
-
-
-
-`; - exports[`Storyshots Generics / MultiSelectField interactive 1`] = `
`; +exports[`Storyshots Generics / Multiple select with autocomplete can load more 1`] = ` +
+
+
+
+
+ +
+ + + + + + + + + + +
+
+
+ + + +
+
+
+
+
+ +
+
+
+
+
+
+`; + +exports[`Storyshots Generics / Multiple select with autocomplete default 1`] = ` +
+
+
+
+
+ +
+ + + + + + + + + + +
+
+
+ +
+
+
+
+
+
+`; + +exports[`Storyshots Generics / Multiple select with autocomplete interactive 1`] = ` +
+
+
+
+
+ + +

+ Value: AF +

+
+
+
+
+
+

+ Afghanistan +

+ +
+
+
+
+
+
+`; + +exports[`Storyshots Generics / Multiple select with autocomplete interactive with custom option 1`] = ` +
+
+
+
+
+ + +

+ Value: AF +

+
+
+
+
+
+

+ Afghanistan +

+ +
+
+
+
+
+
+`; + +exports[`Storyshots Generics / Multiple select with autocomplete interactive with load more 1`] = ` +
+
+
+
+
+ + +

+ Value: AF +

+
+
+
+
+
+

+ Afghanistan +

+ +
+
+
+
+
+
+`; + +exports[`Storyshots Generics / Multiple select with autocomplete no data 1`] = ` +
+
+
+
+
+ +
+
+
+ +
+
+
+
+
+
+`; + exports[`Storyshots Generics / PageHeader with title 1`] = `
`; -exports[`Storyshots Generics / SingleAutocompleteSelectField with custom option 1`] = ` +exports[`Storyshots Generics / Select with autocomplete can load more 1`] = `
+
+
+
+ + + + + + + + + + +
+
+
+ + + +
+
+
+
+
+ +
+
+
+
+
+
+`; + +exports[`Storyshots Generics / Select with autocomplete default 1`] = ` +
+
+
+
+
+ + + + + + + + + + +
+
+
+ +
+
+
+
+
+
+`; + +exports[`Storyshots Generics / Select with autocomplete interactive 1`] = ` +
+
- Value: afghanistan + Value: AF

@@ -3811,13 +4729,13 @@ exports[`Storyshots Generics / SingleAutocompleteSelectField with custom option
`; -exports[`Storyshots Generics / SingleAutocompleteSelectField with loaded data 1`] = ` +exports[`Storyshots Generics / Select with autocomplete interactive with custom option 1`] = `
- Value: afghanistan + Value: AF

@@ -3894,13 +4812,13 @@ exports[`Storyshots Generics / SingleAutocompleteSelectField with loaded data 1`
`; -exports[`Storyshots Generics / SingleAutocompleteSelectField with loading data 1`] = ` +exports[`Storyshots Generics / Select with autocomplete interactive with empty option 1`] = `
- + +

- Value: afghanistan + Value: AF

@@ -3981,13 +4895,139 @@ exports[`Storyshots Generics / SingleAutocompleteSelectField with loading data 1
`; +exports[`Storyshots Generics / Select with autocomplete interactive with load more 1`] = ` +
+
+
+ +
+
+ + +

+ Value: AF +

+
+
+ +
+
+
+`; + +exports[`Storyshots Generics / Select with autocomplete no data 1`] = ` +
+
+
+
+
+ +
+
+
+ +
+
+
+
+
+
+`; + exports[`Storyshots Generics / SingleSelectField with error hint 1`] = `
`; -exports[`Storyshots Orders / OrderCustomerEditDialog default 1`] = ` -
-`; - exports[`Storyshots Orders / OrderDraftCancelDialog default 1`] = `
Date: Tue, 15 Oct 2019 17:47:52 +0200 Subject: [PATCH 14/18] Update messages --- locale/messages.pot | 26 +++++++------------------- 1 file changed, 7 insertions(+), 19 deletions(-) diff --git a/locale/messages.pot b/locale/messages.pot index a5f2fceae..0737d71ba 100644 --- a/locale/messages.pot +++ b/locale/messages.pot @@ -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 From 44633033c7ab0184c023dd7ddb43bbf2d5989a53 Mon Sep 17 00:00:00 2001 From: dominik-zeglen Date: Tue, 15 Oct 2019 17:48:20 +0200 Subject: [PATCH 15/18] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bf77d62bd..340fdb719 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 From 6384363ca663d9168270ed295e591896abef5e3b Mon Sep 17 00:00:00 2001 From: dominik-zeglen Date: Wed, 16 Oct 2019 11:27:42 +0200 Subject: [PATCH 16/18] Increase fetched package size --- src/config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config.ts b/src/config.ts index 7737bfe0d..8789313ed 100644 --- a/src/config.ts +++ b/src/config.ts @@ -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: "" }; From 16263d67a8c70c10fc545aa3773a3a0306f91042 Mon Sep 17 00:00:00 2001 From: dominik-zeglen Date: Wed, 16 Oct 2019 17:51:53 +0200 Subject: [PATCH 17/18] Improve in-memory filtering --- package.json | 2 - .../MultiAutocompleteSelectField.tsx | 18 ++---- .../SingleAutocompleteSelectField.tsx | 59 ++++++++----------- 3 files changed, 29 insertions(+), 50 deletions(-) diff --git a/package.json b/package.json index 9ba48a59b..de5286a57 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/components/MultiAutocompleteSelectField/MultiAutocompleteSelectField.tsx b/src/components/MultiAutocompleteSelectField/MultiAutocompleteSelectField.tsx index 18b6607cb..9181d7b1f 100644 --- a/src/components/MultiAutocompleteSelectField/MultiAutocompleteSelectField.tsx +++ b/src/components/MultiAutocompleteSelectField/MultiAutocompleteSelectField.tsx @@ -9,8 +9,8 @@ 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 { compareTwoStrings } from "string-similarity"; import { fade } from "@material-ui/core/styles/colorManipulator"; import Debounce, { DebounceProps } from "@saleor/components/Debounce"; @@ -210,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 ( setQuery(q || "")} - choices={sortedChoices} + choices={filter(choices, query, { + key: "label" + })} {...props} /> ); diff --git a/src/components/SingleAutocompleteSelectField/SingleAutocompleteSelectField.tsx b/src/components/SingleAutocompleteSelectField/SingleAutocompleteSelectField.tsx index 395e9905f..7e6711022 100644 --- a/src/components/SingleAutocompleteSelectField/SingleAutocompleteSelectField.tsx +++ b/src/components/SingleAutocompleteSelectField/SingleAutocompleteSelectField.tsx @@ -3,8 +3,8 @@ import { InputProps } from "@material-ui/core/Input"; import { createStyles, withStyles, WithStyles } from "@material-ui/core/styles"; import TextField from "@material-ui/core/TextField"; import Downshift from "downshift"; +import { filter } from "fuzzaldrin"; import React from "react"; -import { compareTwoStrings } from "string-similarity"; import SingleAutocompleteSelectFieldContent, { SingleAutocompleteChoiceType } from "./SingleAutocompleteSelectFieldContent"; @@ -174,41 +174,32 @@ const SingleAutocompleteSelectFieldComponent = withStyles(styles, { } ); -export class SingleAutocompleteSelectField extends React.Component< - Omit, - 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 ; - } +const SingleAutocompleteSelectField: React.FC< + SingleAutocompleteSelectFieldProps +> = ({ choices, fetchChoices, ...props }) => { + const [query, setQuery] = React.useState(""); + if (fetchChoices) { return ( - + + {debounceFn => ( + + )} + ); } -} + return ( + setQuery(q || "")} + choices={filter(choices, query, { + key: "label" + })} + {...props} + /> + ); +}; export default SingleAutocompleteSelectField; From 4295a6152a893dba33bc544a1be1d5bfb0442990 Mon Sep 17 00:00:00 2001 From: dominik-zeglen Date: Wed, 16 Oct 2019 17:56:46 +0200 Subject: [PATCH 18/18] Fix typesr --- .../SingleAutocompleteSelectField.tsx | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/components/SingleAutocompleteSelectField/SingleAutocompleteSelectField.tsx b/src/components/SingleAutocompleteSelectField/SingleAutocompleteSelectField.tsx index 7e6711022..ada7a0e46 100644 --- a/src/components/SingleAutocompleteSelectField/SingleAutocompleteSelectField.tsx +++ b/src/components/SingleAutocompleteSelectField/SingleAutocompleteSelectField.tsx @@ -1,4 +1,3 @@ -import { Omit } from "@material-ui/core"; import { InputProps } from "@material-ui/core/Input"; import { createStyles, withStyles, WithStyles } from "@material-ui/core/styles"; import TextField from "@material-ui/core/TextField"; @@ -39,13 +38,6 @@ export interface SingleAutocompleteSelectFieldProps onChange: (event: React.ChangeEvent) => void; } -interface SingleAutocompleteSelectFieldState { - choices: Array<{ - label: string; - value: string; - }>; -} - const DebounceAutocomplete: React.ComponentType< DebounceProps > = Debounce;