diff --git a/.gitignore b/.gitignore index 14f66e4e6..f5c4f1375 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,7 @@ !.nvmrc !.npmrc !.plop +!.prettierignore !.pylintrc !.travis* !.testcafe diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 000000000..bcbfbbf22 --- /dev/null +++ b/.prettierignore @@ -0,0 +1 @@ +types/ \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index bad2ea38f..5bb57de0f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ All notable, unreleased changes to this project will be documented in this file. - Fix disappearing products description - #259 by @dominik-zeglen - Improve mobile appearance - #240 by @benekex2 and @dominik-zeglen - Use searches as hooks instead of components - #262 by @dominik-zeglen +- Add navigator - #267 by @dominik-zeglen - Fix voucher limit - #271 by @dominik-zeglen ## 2.0.0 diff --git a/locale/messages.pot b/locale/messages.pot index 3ad29f137..28902c762 100644 --- a/locale/messages.pot +++ b/locale/messages.pot @@ -1,6 +1,6 @@ msgid "" msgstr "" -"POT-Creation-Date: 2019-11-13T13:13:30.128Z\n" +"POT-Creation-Date: 2019-11-26T14:34:48.426Z\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "MIME-Version: 1.0\n" @@ -167,14 +167,6 @@ msgctxt "page header" msgid "Add Collection" msgstr "" -#: build/locale/src/customers/components/CustomerCreatePage/CustomerCreatePage.json -#. [src.customers.components.CustomerCreatePage.2622255457] - page header -#. defaultMessage is: -#. Add Customer -msgctxt "page header" -msgid "Add Customer" -msgstr "" - #: build/locale/src/components/Filter/Filter.json #. [src.components.Filter.2852521946] - button #. defaultMessage is: @@ -263,14 +255,6 @@ msgctxt "button" msgid "Add authentication" msgstr "" -#: build/locale/src/customers/components/CustomerListPage/CustomerListPage.json -#. [src.customers.components.CustomerListPage.1934221653] - button -#. defaultMessage is: -#. Add customer -msgctxt "button" -msgid "Add customer" -msgstr "" - #: build/locale/src/components/Filter/FilterContent.json #. [src.components.Filter.2851720415] - button #. defaultMessage is: @@ -856,21 +840,9 @@ msgid "Are you sure you want to delete {counter,plural,one{this attribute} other msgstr "" #: build/locale/src/categories/views/CategoryDetails.json -#. [src.categories.views.982216972] +#. [src.categories.views.1592907702] #. defaultMessage is: -#. Are you sure you want to delete {counter,plural,one{this attribute} other{{displayQuantity} categories}}? -msgctxt "description" -msgid "Are you sure you want to delete {counter,plural,one{this attribute} other{{displayQuantity} categories}}?" -msgstr "" - -#: build/locale/src/categories/views/CategoryDetails.json -#. [src.categories.views.3920301974] -#. defaultMessage is: -#. Are you sure you want to delete {counter,plural,one{this attribute} other{{displayQuantity} products}}? -msgctxt "description" -msgid "Are you sure you want to delete {counter,plural,one{this attribute} other{{displayQuantity} products}}?" -msgstr "" - +#. Are you sure you want to delete {counter,plural,one{this category} other{{displayQuantity} categories}}? #: build/locale/src/categories/views/CategoryList/CategoryList.json #. [src.categories.views.CategoryList.1592907702] #. defaultMessage is: @@ -927,6 +899,14 @@ msgctxt "dialog content" msgid "Are you sure you want to delete {counter,plural,one{this product type} other{{displayQuantity} product types}}?" msgstr "" +#: build/locale/src/categories/views/CategoryDetails.json +#. [src.categories.views.785143617] +#. defaultMessage is: +#. Are you sure you want to delete {counter,plural,one{this product} other{{displayQuantity} products}}? +msgctxt "description" +msgid "Are you sure you want to delete {counter,plural,one{this product} other{{displayQuantity} products}}?" +msgstr "" + #: build/locale/src/products/views/ProductList/ProductList.json #. [src.products.views.ProductList.785143617] - dialog content #. defaultMessage is: @@ -1803,6 +1783,10 @@ msgctxt "number of categories" msgid "Categories ({quantity})" msgstr "" +#: build/locale/src/components/Navigator/modes/messages.json +#. [src.components.Navigator.modes.category] +#. defaultMessage is: +#. Category #: build/locale/src/products/components/ProductCategoryAndCollectionsForm/ProductCategoryAndCollectionsForm.json #. [src.products.components.ProductCategoryAndCollectionsForm.1755013298] #. defaultMessage is: @@ -1967,6 +1951,14 @@ msgctxt "voucher code" msgid "Code" msgstr "" +#: build/locale/src/components/Navigator/modes/messages.json +#. [src.components.Navigator.modes.collection] +#. defaultMessage is: +#. Collection +msgctxt "description" +msgid "Collection" +msgstr "" + #: build/locale/src/translations/components/TranslationsCollectionsPage/TranslationsCollectionsPage.json #. [src.translations.components.TranslationsCollectionsPage.2759199473] #. defaultMessage is: @@ -2347,6 +2339,38 @@ msgctxt "create service token, button" msgid "Create" msgstr "" +#: build/locale/src/components/Navigator/modes/messages.json +#. [src.components.Navigator.modes.createCategory] - button +#. defaultMessage is: +#. Create Category +msgctxt "button" +msgid "Create Category" +msgstr "" + +#: build/locale/src/components/Navigator/modes/messages.json +#. [src.components.Navigator.modes.createCollection] - button +#. defaultMessage is: +#. Create Collection +msgctxt "button" +msgid "Create Collection" +msgstr "" + +#: build/locale/src/components/Navigator/modes/messages.json +#. [src.components.Navigator.modes.createCustomer] - button +#. defaultMessage is: +#. Create Customer +msgctxt "button" +msgid "Create Customer" +msgstr "" + +#: build/locale/src/customers/components/CustomerCreatePage/CustomerCreatePage.json +#. [src.customers.components.CustomerCreatePage.4025686004] - page header +#. defaultMessage is: +#. Create Customer +msgctxt "page header" +msgid "Create Customer" +msgstr "" + #: build/locale/src/navigation/components/MenuCreateDialog/MenuCreateDialog.json #. [menuCreateDialogHeader] - dialog header #. defaultMessage is: @@ -2395,6 +2419,14 @@ msgctxt "header" msgid "Create New Shipping Zone" msgstr "" +#: build/locale/src/components/Navigator/modes/messages.json +#. [src.components.Navigator.modes.createOrder] - button +#. defaultMessage is: +#. Create Order +msgctxt "button" +msgid "Create Order" +msgstr "" + #: build/locale/src/pages/components/PageDetailsPage/PageDetailsPage.json #. [src.pages.components.PageDetailsPage.1068617485] - page header #. defaultMessage is: @@ -2411,14 +2443,10 @@ msgctxt "header" msgid "Create Page" msgstr "" -#: build/locale/src/orders/components/OrderProductAddDialog/OrderProductAddDialog.json -#. [src.orders.components.OrderProductAddDialog.1542417144] - dialog header +#: build/locale/src/components/Navigator/modes/messages.json +#. [src.components.Navigator.modes.createProduct] - button #. defaultMessage is: #. Create Product -msgctxt "dialog header" -msgid "Create Product" -msgstr "" - #: build/locale/src/products/components/ProductListPage/ProductListPage.json #. [src.products.components.ProductListPage.1542417144] - button #. defaultMessage is: @@ -2427,6 +2455,14 @@ msgctxt "button" msgid "Create Product" msgstr "" +#: build/locale/src/orders/components/OrderProductAddDialog/OrderProductAddDialog.json +#. [src.orders.components.OrderProductAddDialog.1542417144] - dialog header +#. defaultMessage is: +#. Create Product +msgctxt "dialog header" +msgid "Create Product" +msgstr "" + #: build/locale/src/products/views/ProductCreate.json #. [src.products.views.1542417144] - window title #. defaultMessage is: @@ -2499,6 +2535,14 @@ msgctxt "header" msgid "Create Variant" msgstr "" +#: build/locale/src/components/Navigator/modes/messages.json +#. [src.components.Navigator.modes.createVoucher] - button +#. defaultMessage is: +#. Create Voucher +msgctxt "button" +msgid "Create Voucher" +msgstr "" + #: build/locale/src/discounts/components/VoucherCreatePage/VoucherCreatePage.json #. [src.discounts.components.VoucherCreatePage.1357216572] - page header #. defaultMessage is: @@ -2571,6 +2615,14 @@ msgctxt "window title" msgid "Create collection" msgstr "" +#: build/locale/src/customers/components/CustomerListPage/CustomerListPage.json +#. [src.customers.components.CustomerListPage.2859116187] - button +#. defaultMessage is: +#. Create customer +msgctxt "button" +msgid "Create customer" +msgstr "" + #: build/locale/src/customers/views/CustomerCreate.json #. [src.customers.views.2859116187] - window title #. defaultMessage is: @@ -3451,6 +3503,14 @@ msgctxt "description" msgid "Discounts" msgstr "" +#: build/locale/src/components/Navigator/modes/messages.json +#. [src.components.Navigator.modes.helpMode] - navigator help mode description +#. defaultMessage is: +#. Display Help +msgctxt "navigator help mode description" +msgid "Display Help" +msgstr "" + #: build/locale/src/taxes/components/CountryTaxesPage/CountryTaxesPage.json #. [src.taxes.components.CountryTaxesPage.3500730003] - tax rate #. defaultMessage is: @@ -4047,6 +4107,14 @@ msgctxt "button" msgid "Go back to dashboard" msgstr "" +#: build/locale/src/components/Navigator/modes/messages.json +#. [src.components.Navigator.modes.goToOrder] - navigator action +#. defaultMessage is: +#. Go to order #{orderNumber} +msgctxt "navigator action" +msgid "Go to order #{orderNumber}" +msgstr "" + #: build/locale/src/home/components/HomeHeader/HomeHeader.json #. [homeHeaderText] - header #. defaultMessage is: @@ -4811,6 +4879,14 @@ msgctxt "description" msgid "Name of your store is shown on tab in web browser" msgstr "" +#: build/locale/src/components/Navigator/Navigator.json +#. [src.components.Navigator.3060198201] - navigator section header +#. defaultMessage is: +#. Navigate to +msgctxt "navigator section header" +msgid "Navigate to" +msgstr "" + #: build/locale/src/intl.json #. [src.navigation] - navigation section name #. defaultMessage is: @@ -4819,6 +4895,14 @@ msgctxt "navigation section name" msgid "Navigation" msgstr "" +#: build/locale/src/components/Navigator/Navigator.json +#. [src.components.Navigator.4290208300] - navigator notification title +#. defaultMessage is: +#. Navigator is here to help +msgctxt "navigator notification title" +msgid "Navigator is here to help" +msgstr "" + #: build/locale/src/auth/components/NewPasswordPage/NewPasswordPage.json #. [src.auth.components.NewPasswordPage.1254879564] #. defaultMessage is: @@ -4875,6 +4959,14 @@ msgctxt "description" msgid "No Products added to Order" msgstr "" +#: build/locale/src/components/Navigator/modes/messages.json +#. [src.components.Navigator.modes.noResults] +#. defaultMessage is: +#. No Results +msgctxt "description" +msgid "No Results" +msgstr "" + #: build/locale/src/home/components/HomeActivityCard/HomeActivityCard.json #. [homeActivityCardNoActivities] #. defaultMessage is: @@ -5315,6 +5407,14 @@ msgctxt "voucher has no requirements" msgid "None" msgstr "" +#: build/locale/src/components/Navigator/modes/messages.json +#. [src.components.Navigator.modes.collectionUnpublished] - collection +#. defaultMessage is: +#. Not Published +msgctxt "collection" +msgid "Not Published" +msgstr "" + #: build/locale/src/pages/components/PageList/PageList.json #. [src.pages.components.PageList.3767550649] - page status #. defaultMessage is: @@ -5511,6 +5611,14 @@ msgctxt "description" msgid "Order History" msgstr "" +#: build/locale/src/components/Navigator/NavigatorInput.json +#. [src.components.Navigator.1116468870] - navigator placeholder +#. defaultMessage is: +#. Order Number +msgctxt "navigator placeholder" +msgid "Order Number" +msgstr "" + #: build/locale/src/orders/components/OrderListFilter/OrderListFilter.json #. [src.orders.components.OrderListFilter.2222765704] #. defaultMessage is: @@ -5715,6 +5823,14 @@ msgctxt "description" msgid "Original String" msgstr "" +#: build/locale/src/components/Navigator/Navigator.json +#. [src.components.Navigator.3384551821] - navigator notification +#. defaultMessage is: +#. Our new feature to help you with your daily tasks. Run Navigator using {keyboardShortcut} shortcut. +msgctxt "navigator notification" +msgid "Our new feature to help you with your daily tasks. Run Navigator using {keyboardShortcut} shortcut." +msgstr "" + #: build/locale/src/products/components/ProductListFilter/ProductListFilter.json #. [src.products.components.ProductListFilter.1640493122] - product status #. defaultMessage is: @@ -6251,6 +6367,10 @@ msgctxt "page header" msgid "Primary Address" msgstr "" +#: build/locale/src/components/Navigator/modes/messages.json +#. [src.components.Navigator.modes.product] +#. defaultMessage is: +#. Product #: build/locale/src/orders/components/OrderDraftDetailsProducts/OrderDraftDetailsProducts.json #. [src.orders.components.OrderDraftDetailsProducts.1895667608] #. defaultMessage is: @@ -6599,6 +6719,14 @@ msgctxt "product is published" msgid "Published" msgstr "" +#: build/locale/src/components/Navigator/modes/messages.json +#. [src.components.Navigator.modes.collectionPublished] - collection +#. defaultMessage is: +#. Published +msgctxt "collection" +msgid "Published" +msgstr "" + #: build/locale/src/pages/components/PageList/PageList.json #. [src.pages.components.PageList.3640454975] - page status #. defaultMessage is: @@ -6655,6 +6783,14 @@ msgctxt "ordered products" msgid "Quantity" msgstr "" +#: build/locale/src/components/Navigator/Navigator.json +#. [src.components.Navigator.3636839115] - navigator section header +#. defaultMessage is: +#. Quick Actions +msgctxt "navigator section header" +msgid "Quick Actions" +msgstr "" + #: build/locale/src/shipping/components/ShippingZoneCountriesAssignDialog/ShippingZoneCountriesAssignDialog.json #. [src.shipping.components.ShippingZoneCountriesAssignDialog.1440682557] #. defaultMessage is: @@ -7043,6 +7179,14 @@ msgctxt "description" msgid "Search Collection" msgstr "" +#: build/locale/src/components/Navigator/modes/messages.json +#. [src.components.Navigator.modes.helpCommandsMode] - navigator command mode description +#. defaultMessage is: +#. Search Command +msgctxt "navigator command mode description" +msgid "Search Command" +msgstr "" + #: build/locale/src/shipping/components/ShippingZoneCountriesAssignDialog/ShippingZoneCountriesAssignDialog.json #. [src.shipping.components.ShippingZoneCountriesAssignDialog.3510295703] #. defaultMessage is: @@ -7051,6 +7195,14 @@ msgctxt "description" msgid "Search Countries" msgstr "" +#: build/locale/src/components/Navigator/NavigatorInput.json +#. [src.components.Navigator.1643417013] - navigator placeholder +#. defaultMessage is: +#. Search Customer +msgctxt "navigator placeholder" +msgid "Search Customer" +msgstr "" + #: build/locale/src/customers/components/CustomerListPage/CustomerListPage.json #. [src.customers.components.CustomerListPage.1643417013] #. defaultMessage is: @@ -7059,6 +7211,14 @@ msgctxt "description" msgid "Search Customer" msgstr "" +#: build/locale/src/components/Navigator/modes/messages.json +#. [src.components.Navigator.modes.helpCustomersMode] - navigator customer mode description +#. defaultMessage is: +#. Search Customers +msgctxt "navigator customer mode description" +msgid "Search Customers" +msgstr "" + #: build/locale/src/orders/components/OrderCustomer/OrderCustomer.json #. [src.orders.components.OrderCustomer.2433460203] #. defaultMessage is: @@ -7147,6 +7307,14 @@ msgctxt "save search tab" msgid "Search Name" msgstr "" +#: build/locale/src/components/Navigator/modes/messages.json +#. [src.components.Navigator.modes.helpOrdersMode] - navigator order mode description +#. defaultMessage is: +#. Search Orders +msgctxt "navigator order mode description" +msgid "Search Orders" +msgstr "" + #: build/locale/src/orders/components/OrderListPage/OrderListPage.json #. [src.orders.components.OrderListPage.355376157] #. defaultMessage is: @@ -7231,6 +7399,14 @@ msgctxt "description" msgid "Search Staff Member" msgstr "" +#: build/locale/src/components/Navigator/modes/messages.json +#. [src.components.Navigator.modes.helpDefaultMode] - navigator default mode description +#. defaultMessage is: +#. Search Views and Actions +msgctxt "navigator default mode description" +msgid "Search Views and Actions" +msgstr "" + #: build/locale/src/discounts/components/VoucherListPage/VoucherListPage.json #. [src.discounts.components.VoucherListPage.1930485532] #. defaultMessage is: @@ -7319,6 +7495,38 @@ msgctxt "description" msgid "Search engine title" msgstr "" +#: build/locale/src/components/Navigator/modes/messages.json +#. [src.components.Navigator.modes.helpCatalogMode] - navigator catalog mode description +#. defaultMessage is: +#. Search in Catalog +msgctxt "navigator catalog mode description" +msgid "Search in Catalog" +msgstr "" + +#: build/locale/src/components/Navigator/Navigator.json +#. [src.components.Navigator.2935523260] - navigator section header +#. defaultMessage is: +#. Search in Catalog +msgctxt "navigator section header" +msgid "Search in Catalog" +msgstr "" + +#: build/locale/src/components/Navigator/NavigatorInput.json +#. [src.components.Navigator.2935523260] - navigator placeholder +#. defaultMessage is: +#. Search in Catalog +msgctxt "navigator placeholder" +msgid "Search in Catalog" +msgstr "" + +#: build/locale/src/components/Navigator/Navigator.json +#. [src.components.Navigator.1809988825] - navigator section header +#. defaultMessage is: +#. Search in Customers +msgctxt "navigator section header" +msgid "Search in Customers" +msgstr "" + #: build/locale/src/attributes/components/AttributeList/AttributeList.json #. [src.attributes.components.AttributeList.2235596452] - attribute can be searched in dashboard #. defaultMessage is: @@ -8603,6 +8811,14 @@ msgctxt "product type is either simple or configurable" msgid "Type" msgstr "" +#: build/locale/src/components/Navigator/NavigatorInput.json +#. [src.components.Navigator.1167695965] - navigator placeholder +#. defaultMessage is: +#. Type Command +msgctxt "navigator placeholder" +msgid "Type Command" +msgstr "" + #: build/locale/src/productTypes/components/ProductTypeList/ProductTypeList.json #. [src.productTypes.components.ProductTypeList.2253986440] - product type name #. defaultMessage is: @@ -8611,6 +8827,14 @@ msgctxt "product type name" msgid "Type Name" msgstr "" +#: build/locale/src/components/Navigator/NavigatorInput.json +#. [src.components.Navigator.2874620973] - navigator placeholder +#. defaultMessage is: +#. Type {key} to see available actions +msgctxt "navigator placeholder" +msgid "Type {key} to see available actions" +msgstr "" + #: build/locale/src/pages/components/PageSlug/PageSlug.json #. [src.pages.components.PageSlug.1324178587] #. defaultMessage is: @@ -9715,6 +9939,14 @@ msgctxt "translation progress" msgid "{current} of {max}" msgstr "" +#: build/locale/src/components/Navigator/modes/messages.json +#. [src.components.Navigator.modes.customerWithName] +#. defaultMessage is: +#. {firstName} {lastName} +msgctxt "description" +msgid "{firstName} {lastName}" +msgstr "" + #: build/locale/src/components/MoneyRange/MoneyRange.json #. [src.components.MoneyRange.1316359951] - money #. defaultMessage is: diff --git a/package-lock.json b/package-lock.json index 37d527f1a..41c04c99a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3286,6 +3286,11 @@ "integrity": "sha512-YesPanU1+WCigC/Aj1Mga8UCOjHIfMNHZ3zzDsUY7lI8GlKnh/Kv2QwJOQ+jNQ36Ru7IfzSedlG14hppYaN13A==", "dev": true }, + "@types/semver-compare": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@types/semver-compare/-/semver-compare-1.0.1.tgz", + "integrity": "sha512-wx2LQVvKlEkhXp/HoKIZ/aSL+TvfJdKco8i0xJS3aR877mg4qBHzNT6+B5a61vewZHo79EdZavskGnRXEC2H6A==" + }, "@types/shallowequal": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@types/shallowequal/-/shallowequal-1.1.1.tgz", @@ -11333,6 +11338,11 @@ "integrity": "sha512-kssjab8CvdXfcXMXVcvsXum4Hwdq9XGtRD3TteMEvEbq0LXyiNQr6AprqKqfeaDXze7SxWvRxdpwE6ku7ikLkg==", "dev": true }, + "hotkeys-js": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/hotkeys-js/-/hotkeys-js-3.7.2.tgz", + "integrity": "sha512-LJIPBgejlklphThig2edlYUiPq3iFDHdsXftvZ0VWrpi330dRwD2YxPlzOYv0UQEatvZdgwG3ZLCfJ020ix8vQ==" + }, "hpack.js": { "version": "2.1.6", "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", @@ -18050,8 +18060,7 @@ "semver-compare": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", - "integrity": "sha1-De4hahyUGrN+nvsXiPavxf9VN/w=", - "dev": true + "integrity": "sha1-De4hahyUGrN+nvsXiPavxf9VN/w=" }, "send": { "version": "0.17.1", diff --git a/package.json b/package.json index 31d82bf87..74788cf4c 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "fuzzaldrin": "^2.1.0", "graphql": "^14.4.2", "graphql-tag": "^2.10.1", + "hotkeys-js": "^3.7.2", "is-url": "^1.2.4", "jss": "^9.8.7", "keycode": "^2.2.0", @@ -62,6 +63,7 @@ "react-sortable-hoc": "^0.6.8", "react-sortable-tree": "^2.6.2", "react-svg": "^2.2.11", + "semver-compare": "^1.0.0", "slugify": "^1.3.4", "typescript": "^3.6.4", "url-join": "^4.0.1", @@ -97,6 +99,7 @@ "@types/react-sortable-hoc": "^0.6.5", "@types/react-sortable-tree": "^0.3.6", "@types/react-test-renderer": "^16.8.2", + "@types/semver-compare": "^1.0.1", "@types/storybook__addon-storyshots": "^3.4.9", "@types/storybook__react": "^4.0.2", "@types/url-join": "^0.8.3", diff --git a/src/components/Navigator/Navigator.tsx b/src/components/Navigator/Navigator.tsx new file mode 100644 index 000000000..3a2752f60 --- /dev/null +++ b/src/components/Navigator/Navigator.tsx @@ -0,0 +1,206 @@ +import Fade from "@material-ui/core/Fade"; +import Modal from "@material-ui/core/Modal"; +import Paper from "@material-ui/core/Paper"; +import makeStyles from "@material-ui/core/styles/makeStyles"; +import useTheme from "@material-ui/core/styles/useTheme"; +import Downshift from "downshift"; +import hotkeys from "hotkeys-js"; +import React from "react"; +import { useIntl } from "react-intl"; +import cmp from "semver-compare"; + +import { APP_VERSION } from "@saleor/config"; +import useLocalStorage from "@saleor/hooks/useLocalStorage"; +import useNotifier from "@saleor/hooks/useNotifier"; +import { + getActions, + getCatalog, + getCustomers, + getViews, + hasActions, + hasCatalog, + hasCustomers, + hasViews +} from "./modes/utils"; +import NavigatorInput from "./NavigatorInput"; +import NavigatorSection from "./NavigatorSection"; +import { QuickSearchAction } from "./types"; +import useQuickSearch from "./useQuickSearch"; + +const navigatorHotkey = "ctrl+k, command+k"; +const navigatorNotificationStorageKey = "notifiedAboutNavigator"; + +function getItemOffset( + actions: QuickSearchAction[], + cbs: Array +): number { + return cbs.reduce((acc, cb) => cb(actions).length + acc, 0); +} + +const useStyles = makeStyles( + theme => ({ + modal: { + alignItems: "center", + display: "flex", + justifyContent: "center", + padding: theme.spacing(3) + }, + paper: { + overflow: "hidden" + }, + root: { + height: 500, + maxWidth: 600, + outline: 0, + width: "100%" + } + }), + { + name: "Navigator" + } +); + +const Navigator: React.FC = () => { + const [visible, setVisible] = React.useState(false); + const input = React.useRef(null); + const [query, mode, change, actions] = useQuickSearch(visible, input); + const intl = useIntl(); + const notify = useNotifier(); + const [notifiedAboutNavigator, setNotifiedAboutNavigator] = useLocalStorage( + navigatorNotificationStorageKey, + false + ); + const classes = useStyles({}); + const theme = useTheme(); + + React.useEffect(() => { + hotkeys(navigatorHotkey, event => { + event.preventDefault(); + setVisible(!visible); + }); + + if (cmp(APP_VERSION, "2.1.0") !== 1 && !notifiedAboutNavigator) { + notify({ + autohide: null, + text: intl.formatMessage( + { + defaultMessage: + "Our new feature to help you with your daily tasks. Run Navigator using {keyboardShortcut} shortcut.", + description: "navigator notification" + }, + { + keyboardShortcut: + navigator.platform.toLowerCase().indexOf("mac") >= 0 + ? "⌘+K" + : "Ctrl+K" + } + ), + title: intl.formatMessage({ + defaultMessage: "Navigator is here to help", + description: "navigator notification title" + }) + }); + setNotifiedAboutNavigator(true); + } + + return () => hotkeys.unbind(navigatorHotkey); + }, []); + + return ( + setVisible(false)} + > + +
+ + + item ? item.label : "" + } + onSelect={(item: QuickSearchAction) => { + const shouldRemainVisible = item.onClick(); + if (!shouldRemainVisible) { + setVisible(false); + } + }} + onInputValueChange={value => + change({ + target: { + name: "query", + value + } + }) + } + defaultHighlightedIndex={0} + > + {({ getInputProps, getItemProps, highlightedIndex }) => ( +
+ + {hasViews(actions) && ( + + )} + {hasActions(actions) && ( + + )} + {hasCustomers(actions) && ( + + )} + {hasCatalog(actions) && ( + + )} +
+ )} +
+
+
+
+
+ ); +}; + +export default Navigator; diff --git a/src/components/Navigator/NavigatorInput.tsx b/src/components/Navigator/NavigatorInput.tsx new file mode 100644 index 000000000..b82235162 --- /dev/null +++ b/src/components/Navigator/NavigatorInput.tsx @@ -0,0 +1,112 @@ +import makeStyles from "@material-ui/core/styles/makeStyles"; +import React from "react"; +import { useIntl } from "react-intl"; +import { QuickSearchMode } from "./types"; + +const useStyles = makeStyles( + theme => { + const typography = { + color: theme.palette.text.primary, + fontSize: 24, + lineHeight: 1.33 + }; + + return { + adornment: { + ...typography, + color: theme.palette.text.secondary, + paddingRight: theme.spacing(1) + }, + input: { + ...typography, + background: "transparent", + border: "none", + outline: 0, + padding: 0, + width: "100%" + }, + root: { + background: theme.palette.background.default, + display: "flex", + padding: theme.spacing(2, 3) + } + }; + }, + { + name: "NavigatorInput" + } +); + +interface NavigatorInputProps + extends React.InputHTMLAttributes { + mode: QuickSearchMode; +} + +const NavigatorInput = React.forwardRef( + (props, ref) => { + const { mode, ...rest } = props; + const classes = useStyles(props); + const intl = useIntl(); + + return ( +
+ {mode !== "default" && ( + + {mode === "orders" + ? "#" + : mode === "customers" + ? "@" + : mode === "catalog" + ? "$" + : mode === "help" + ? "?" + : ">"} + + )} + +
+ ); + } +); + +NavigatorInput.displayName = "NavigatorInput"; +export default NavigatorInput; diff --git a/src/components/Navigator/NavigatorSection.tsx b/src/components/Navigator/NavigatorSection.tsx new file mode 100644 index 000000000..40cb73a67 --- /dev/null +++ b/src/components/Navigator/NavigatorSection.tsx @@ -0,0 +1,104 @@ +import MenuItem from "@material-ui/core/MenuItem"; +import makeStyles from "@material-ui/core/styles/makeStyles"; +import Typography from "@material-ui/core/Typography"; +import { GetItemPropsOptions } from "downshift"; +import React from "react"; + +import Hr from "../Hr"; +import { QuickSearchAction } from "./types"; + +interface NavigatorSectionProps { + getItemProps: (options: GetItemPropsOptions) => any; + highlightedIndex: number; + label: string; + items: QuickSearchAction[]; + offset: number; +} + +const useStyles = makeStyles( + theme => ({ + item: { + "&&&&": { + color: theme.palette.text.secondary, + fontWeight: 400 + }, + display: "flex", + margin: theme.spacing(1, 0) + }, + itemLabel: { + display: "inline-block" + }, + label: { + paddingLeft: theme.spacing(2), + textTransform: "uppercase" + }, + root: { + "&:last-child": { + marginBottom: 0 + }, + margin: theme.spacing(2, 0), + padding: theme.spacing(0, 1) + }, + spacer: { + flex: 1 + }, + symbol: { + display: "inline-block", + fontWeight: 600, + width: theme.spacing(4) + } + }), + { + name: "NavigatorSection" + } +); + +const NavigatorSection: React.FC = props => { + const { getItemProps, highlightedIndex, label, items, offset } = props; + + const classes = useStyles(props); + + return ( +
+ + {label} + +
+ {items.map((item, itemIndex) => { + const index = offset + itemIndex; + const itemProps = getItemProps({ + index, + item + }); + + return ( + + + {item.symbol && ( + {item.symbol} + )} + {item.label} + {item.caption && ( + {item.caption} + )} + + + {item.extraInfo} + + ); + })} +
+ ); +}; + +NavigatorSection.displayName = "NavigatorSection"; +export default NavigatorSection; diff --git a/src/components/Navigator/index.ts b/src/components/Navigator/index.ts new file mode 100644 index 000000000..8816cc013 --- /dev/null +++ b/src/components/Navigator/index.ts @@ -0,0 +1,2 @@ +export { default } from "./Navigator"; +export * from "./Navigator"; diff --git a/src/components/Navigator/modes/catalog.ts b/src/components/Navigator/modes/catalog.ts new file mode 100644 index 000000000..08be62dee --- /dev/null +++ b/src/components/Navigator/modes/catalog.ts @@ -0,0 +1,102 @@ +import { score } from "fuzzaldrin"; +import { IntlShape } from "react-intl"; + +import { categoryUrl } from "@saleor/categories/urls"; +import { collectionUrl } from "@saleor/collections/urls"; +import { UseNavigatorResult } from "@saleor/hooks/useNavigator"; +import { maybe } from "@saleor/misc"; +import { productUrl } from "@saleor/products/urls"; +import { SearchCatalog } from "../queries/types/SearchCatalog"; +import { QuickSearchAction, QuickSearchActionInput } from "../types"; +import messages from "./messages"; +import { sortScores } from "./utils"; + +const maxActions = 5; + +export function searchInCatalog( + search: string, + intl: IntlShape, + navigate: UseNavigatorResult, + catalog: SearchCatalog +): QuickSearchAction[] { + const categories: QuickSearchActionInput[] = maybe( + () => catalog.categories.edges.map(edge => edge.node), + [] + ) + .map(category => ({ + caption: intl.formatMessage(messages.category), + label: category.name, + onClick: () => { + navigate(categoryUrl(category.id)); + return false; + }, + score: score(category.name, search), + text: category.name, + type: "catalog" + })) + .sort(sortScores); + + const collections: QuickSearchActionInput[] = maybe( + () => catalog.collections.edges.map(edge => edge.node), + [] + ) + .map(collection => ({ + caption: intl.formatMessage(messages.collection), + extraInfo: intl.formatMessage( + collection.isPublished + ? messages.collectionPublished + : messages.collectionUnpublished + ), + label: collection.name, + onClick: () => { + navigate(collectionUrl(collection.id)); + return false; + }, + score: score(collection.name, search), + text: collection.name, + type: "catalog" + })) + .sort(sortScores); + + const products: QuickSearchActionInput[] = maybe( + () => catalog.products.edges.map(edge => edge.node), + [] + ) + .map(product => ({ + caption: intl.formatMessage(messages.product), + extraInfo: product.category.name, + label: product.name, + onClick: () => { + navigate(productUrl(product.id)); + return false; + }, + score: score(product.name, search), + text: product.name, + type: "catalog" + })) + .sort(sortScores); + + const baseActions = [ + ...categories.slice(0, 1), + ...collections.slice(0, 1), + ...products.slice(0, 1) + ]; + + return [ + ...baseActions, + ...[...categories.slice(1), ...collections.slice(1), ...products.slice(1)] + .sort(sortScores) + .slice(0, maxActions - baseActions.length) + ].sort(sortScores); +} + +function getCatalogModeActions( + query: string, + intl: IntlShape, + navigate: UseNavigatorResult, + catalog: SearchCatalog +): QuickSearchAction[] { + return searchInCatalog(query, intl, navigate, catalog); +} + +export default getCatalogModeActions; diff --git a/src/components/Navigator/modes/commands/actions.ts b/src/components/Navigator/modes/commands/actions.ts new file mode 100644 index 000000000..07fad303f --- /dev/null +++ b/src/components/Navigator/modes/commands/actions.ts @@ -0,0 +1,104 @@ +import { score } from "fuzzaldrin"; +import { IntlShape } from "react-intl"; + +import { categoryAddUrl } from "@saleor/categories/urls"; +import { collectionAddUrl } from "@saleor/collections/urls"; +import { customerAddUrl } from "@saleor/customers/urls"; +import { voucherAddUrl } from "@saleor/discounts/urls"; +import { UseNavigatorResult } from "@saleor/hooks/useNavigator"; +import { OrderDraftCreate } from "@saleor/orders/types/OrderDraftCreate"; +import { productAddUrl } from "@saleor/products/urls"; +import { MutationFunction } from "react-apollo"; +import { QuickSearchActionInput, QuickSearchMode } from "../../types"; +import messages from "../messages"; +import { sortScores } from "../utils"; + +const threshold = 0.05; +const maxActions = 5; + +interface Command { + label: string; + onClick: () => boolean; +} +export function searchInCommands( + search: string, + intl: IntlShape, + navigate: UseNavigatorResult, + createOrder: MutationFunction, + setMode: (mode: QuickSearchMode) => void +): QuickSearchActionInput[] { + const actions: Command[] = [ + { + label: intl.formatMessage(messages.createCategory), + onClick: () => { + navigate(categoryAddUrl()); + return false; + } + }, + { + label: intl.formatMessage(messages.createCollection), + onClick: () => { + navigate(collectionAddUrl); + return false; + } + }, + { + label: intl.formatMessage(messages.createProduct), + onClick: () => { + navigate(productAddUrl); + return false; + } + }, + { + label: intl.formatMessage(messages.createCustomer), + onClick: () => { + navigate(customerAddUrl); + return false; + } + }, + { + label: intl.formatMessage(messages.createVoucher), + onClick: () => { + navigate(voucherAddUrl); + return false; + } + }, + { + label: intl.formatMessage(messages.createOrder), + onClick: () => { + createOrder(); + return false; + } + }, + { + label: intl.formatMessage(messages.helpMode), + onClick: () => { + setMode("help"); + return true; + } + } + ]; + + return actions.map(action => ({ + label: action.label, + onClick: action.onClick, + score: score(action.label, search), + text: action.label, + type: "action" + })); +} + +function getCommandModeActions( + query: string, + intl: IntlShape, + navigate: UseNavigatorResult, + createOrder: MutationFunction, + setMode: (mode: QuickSearchMode) => void +): QuickSearchActionInput[] { + return [...searchInCommands(query, intl, navigate, createOrder, setMode)] + .filter(action => action.score >= threshold) + .sort(sortScores) + .slice(0, maxActions); +} + +export default getCommandModeActions; diff --git a/src/components/Navigator/modes/commands/index.ts b/src/components/Navigator/modes/commands/index.ts new file mode 100644 index 000000000..9600b6455 --- /dev/null +++ b/src/components/Navigator/modes/commands/index.ts @@ -0,0 +1,2 @@ +export * from "./actions"; +export { default } from "./actions"; diff --git a/src/components/Navigator/modes/customers.ts b/src/components/Navigator/modes/customers.ts new file mode 100644 index 000000000..32c1fe187 --- /dev/null +++ b/src/components/Navigator/modes/customers.ts @@ -0,0 +1,40 @@ +import { IntlShape } from "react-intl"; + +import { customerUrl } from "@saleor/customers/urls"; +import { UseNavigatorResult } from "@saleor/hooks/useNavigator"; +import { SearchCustomers_search_edges_node } from "@saleor/searches/types/SearchCustomers"; +import { QuickSearchAction } from "../types"; +import messages from "./messages"; + +export function searchInCustomers( + intl: IntlShape, + navigate: UseNavigatorResult, + customers: SearchCustomers_search_edges_node[] +): QuickSearchAction[] { + return customers.map(customer => ({ + caption: customer.email, + label: + customer.firstName && customer.lastName + ? intl.formatMessage(messages.customerWithName, { + firstName: customer.firstName, + lastName: customer.lastName + }) + : customer.email, + onClick: () => { + navigate(customerUrl(customer.id)); + return false; + }, + score: 1, + type: "customer" + })); +} + +function getCustomersModeActions( + intl: IntlShape, + navigate: UseNavigatorResult, + customers: SearchCustomers_search_edges_node[] +): QuickSearchAction[] { + return searchInCustomers(intl, navigate, customers); +} + +export default getCustomersModeActions; diff --git a/src/components/Navigator/modes/default/default.ts b/src/components/Navigator/modes/default/default.ts new file mode 100644 index 000000000..d25691fff --- /dev/null +++ b/src/components/Navigator/modes/default/default.ts @@ -0,0 +1,30 @@ +import { MutationFunction } from "react-apollo"; +import { IntlShape } from "react-intl"; + +import { UseNavigatorResult } from "@saleor/hooks/useNavigator"; +import { OrderDraftCreate } from "@saleor/orders/types/OrderDraftCreate"; +import { QuickSearchAction, QuickSearchMode } from "../../types"; +import { searchInCommands } from "../commands"; +import { sortScores } from "../utils"; +import searchInViews from "./views"; + +const threshold = 0.05; +const maxActions = 5; + +function getDefaultModeActions( + query: string, + intl: IntlShape, + navigate: UseNavigatorResult, + createOrder: MutationFunction, + setMode: (mode: QuickSearchMode) => void +): QuickSearchAction[] { + return [ + ...searchInViews(query, intl, navigate), + ...searchInCommands(query, intl, navigate, createOrder, setMode) + ] + .filter(action => action.score >= threshold) + .sort(sortScores) + .slice(0, maxActions); +} + +export default getDefaultModeActions; diff --git a/src/components/Navigator/modes/default/index.ts b/src/components/Navigator/modes/default/index.ts new file mode 100644 index 000000000..86fa704d4 --- /dev/null +++ b/src/components/Navigator/modes/default/index.ts @@ -0,0 +1,3 @@ +export * from "./default"; +export { default } from "./default"; +export * from "./views"; diff --git a/src/components/Navigator/modes/default/views.ts b/src/components/Navigator/modes/default/views.ts new file mode 100644 index 000000000..639f1852e --- /dev/null +++ b/src/components/Navigator/modes/default/views.ts @@ -0,0 +1,134 @@ +import { score } from "fuzzaldrin"; +import { IntlShape } from "react-intl"; + +import { attributeListUrl } from "@saleor/attributes/urls"; +import { categoryListUrl } from "@saleor/categories/urls"; +import { collectionListUrl } from "@saleor/collections/urls"; +import { customerListUrl } from "@saleor/customers/urls"; +import { saleListUrl, voucherListUrl } from "@saleor/discounts/urls"; +import { UseNavigatorResult } from "@saleor/hooks/useNavigator"; +import { sectionNames } from "@saleor/intl"; +import { menuListUrl } from "@saleor/navigation/urls"; +import { orderDraftListUrl, orderListUrl } from "@saleor/orders/urls"; +import { pageListUrl } from "@saleor/pages/urls"; +import { pluginsListUrl } from "@saleor/plugins/urls"; +import { productListUrl } from "@saleor/products/urls"; +import { productTypeListUrl } from "@saleor/productTypes/urls"; +import { serviceListUrl } from "@saleor/services/urls"; +import { shippingZonesListUrl } from "@saleor/shipping/urls"; +import { siteSettingsUrl } from "@saleor/siteSettings/urls"; +import { staffListUrl } from "@saleor/staff/urls"; +import { countryListUrl } from "@saleor/taxes/urls"; +import { languageListUrl } from "@saleor/translations/urls"; +import { webhooksListUrl } from "@saleor/webhooks/urls"; +import { QuickSearchActionInput } from "../../types"; + +interface View { + label: string; + url: string; +} +function searchInViews( + search: string, + intl: IntlShape, + navigate: UseNavigatorResult +): QuickSearchActionInput[] { + const views: View[] = [ + { + label: intl.formatMessage(sectionNames.attributes), + url: attributeListUrl() + }, + { + label: intl.formatMessage(sectionNames.categories), + url: categoryListUrl() + }, + { + label: intl.formatMessage(sectionNames.collections), + url: collectionListUrl() + }, + { + label: intl.formatMessage(sectionNames.customers), + url: customerListUrl() + }, + { + label: intl.formatMessage(sectionNames.draftOrders), + url: orderDraftListUrl() + }, + { + label: intl.formatMessage(sectionNames.home), + url: "/" + }, + { + label: intl.formatMessage(sectionNames.navigation), + url: menuListUrl() + }, + { + label: intl.formatMessage(sectionNames.orders), + url: orderListUrl() + }, + { + label: intl.formatMessage(sectionNames.pages), + url: pageListUrl() + }, + { + label: intl.formatMessage(sectionNames.plugins), + url: pluginsListUrl() + }, + { + label: intl.formatMessage(sectionNames.productTypes), + url: productTypeListUrl() + }, + { + label: intl.formatMessage(sectionNames.products), + url: productListUrl() + }, + { + label: intl.formatMessage(sectionNames.sales), + url: saleListUrl() + }, + { + label: intl.formatMessage(sectionNames.serviceAccounts), + url: serviceListUrl() + }, + { + label: intl.formatMessage(sectionNames.shipping), + url: shippingZonesListUrl() + }, + { + label: intl.formatMessage(sectionNames.siteSettings), + url: siteSettingsUrl() + }, + { + label: intl.formatMessage(sectionNames.staff), + url: staffListUrl() + }, + { + label: intl.formatMessage(sectionNames.taxes), + url: countryListUrl + }, + { + label: intl.formatMessage(sectionNames.translations), + url: languageListUrl + }, + { + label: intl.formatMessage(sectionNames.vouchers), + url: voucherListUrl() + }, + { + label: intl.formatMessage(sectionNames.webhooks), + url: webhooksListUrl() + } + ]; + + return views.map(view => ({ + label: view.label, + onClick: () => { + navigate(view.url); + return false; + }, + score: score(view.label, search), + text: view.label, + type: "view" + })); +} + +export default searchInViews; diff --git a/src/components/Navigator/modes/help.ts b/src/components/Navigator/modes/help.ts new file mode 100644 index 000000000..92cd110c2 --- /dev/null +++ b/src/components/Navigator/modes/help.ts @@ -0,0 +1,79 @@ +import { IntlShape } from "react-intl"; + +import { QuickSearchAction, QuickSearchMode } from "../types"; +import messages from "./messages"; + +function getHelpModeActions( + query: string, + intl: IntlShape, + setMode: (mode: QuickSearchMode) => void +): QuickSearchAction[] { + if (query !== "") { + return [ + { + label: intl.formatMessage(messages.noResults), + onClick: () => true, + type: "action" + } + ]; + } + + return [ + { + label: intl.formatMessage(messages.helpDefaultMode), + onClick: () => { + setMode("default"); + return true; + }, + symbol: "...", + type: "action" + }, + { + label: intl.formatMessage(messages.helpCommandsMode), + onClick: () => { + setMode("commands"); + return true; + }, + symbol: ">", + type: "action" + }, + { + label: intl.formatMessage(messages.helpOrdersMode), + onClick: () => { + setMode("orders"); + return true; + }, + symbol: "#", + type: "action" + }, + { + label: intl.formatMessage(messages.helpCustomersMode), + onClick: () => { + setMode("customers"); + return true; + }, + symbol: "@", + type: "action" + }, + { + label: intl.formatMessage(messages.helpCatalogMode), + onClick: () => { + setMode("catalog"); + return true; + }, + symbol: "$", + type: "action" + }, + { + label: intl.formatMessage(messages.helpMode), + onClick: () => { + setMode("help"); + return true; + }, + symbol: "?", + type: "action" + } + ]; +} + +export default getHelpModeActions; diff --git a/src/components/Navigator/modes/index.ts b/src/components/Navigator/modes/index.ts new file mode 100644 index 000000000..4dd2d487f --- /dev/null +++ b/src/components/Navigator/modes/index.ts @@ -0,0 +1,54 @@ +import { IntlShape } from "react-intl"; + +import { UseNavigatorResult } from "@saleor/hooks/useNavigator"; +import { OrderDraftCreate } from "@saleor/orders/types/OrderDraftCreate"; +import { MutationFunction } from "react-apollo"; +import { QuickSearchAction, QuickSearchMode } from "../types"; +import getCatalogModeActions from "./catalog"; +import getCommandModeActions from "./commands"; +import getCustomersModeActions from "./customers"; +import getDefaultModeActions from "./default"; +import getHelpModeActions from "./help"; +import getOrdersModeActions from "./orders"; +import { ActionQueries } from "./types"; + +function getModeActions( + mode: QuickSearchMode, + query: string, + intl: IntlShape, + queries: ActionQueries, + cbs: { + createOrder: MutationFunction; + navigate: UseNavigatorResult; + setMode: (mode: QuickSearchMode) => void; + } +): QuickSearchAction[] { + switch (mode) { + case "catalog": + return getCatalogModeActions(query, intl, cbs.navigate, queries.catalog); + case "commands": + return getCommandModeActions( + query, + intl, + cbs.navigate, + cbs.createOrder, + cbs.setMode + ); + case "customers": + return getCustomersModeActions(intl, cbs.navigate, queries.customers); + case "help": + return getHelpModeActions(query, intl, cbs.setMode); + case "orders": + return getOrdersModeActions(query, intl, cbs.navigate, queries.order); + default: + return getDefaultModeActions( + query, + intl, + cbs.navigate, + cbs.createOrder, + cbs.setMode + ); + } +} + +export default getModeActions; diff --git a/src/components/Navigator/modes/messages.ts b/src/components/Navigator/modes/messages.ts new file mode 100644 index 000000000..6d3fef9d3 --- /dev/null +++ b/src/components/Navigator/modes/messages.ts @@ -0,0 +1,81 @@ +import { defineMessages } from "react-intl"; + +const messages = defineMessages({ + category: { + defaultMessage: "Category" + }, + collection: { + defaultMessage: "Collection" + }, + collectionPublished: { + defaultMessage: "Published", + description: "collection" + }, + collectionUnpublished: { + defaultMessage: "Not Published", + description: "collection" + }, + createCategory: { + defaultMessage: "Create Category", + description: "button" + }, + createCollection: { + defaultMessage: "Create Collection", + description: "button" + }, + createCustomer: { + defaultMessage: "Create Customer", + description: "button" + }, + createOrder: { + defaultMessage: "Create Order", + description: "button" + }, + createProduct: { + defaultMessage: "Create Product", + description: "button" + }, + createVoucher: { + defaultMessage: "Create Voucher", + description: "button" + }, + customerWithName: { + defaultMessage: "{firstName} {lastName}" + }, + goToOrder: { + defaultMessage: "Go to order #{orderNumber}", + description: "navigator action" + }, + helpCatalogMode: { + defaultMessage: "Search in Catalog", + description: "navigator catalog mode description" + }, + helpCommandsMode: { + defaultMessage: "Search Command", + description: "navigator command mode description" + }, + helpCustomersMode: { + defaultMessage: "Search Customers", + description: "navigator customer mode description" + }, + helpDefaultMode: { + defaultMessage: "Search Views and Actions", + description: "navigator default mode description" + }, + helpMode: { + defaultMessage: "Display Help", + description: "navigator help mode description" + }, + helpOrdersMode: { + defaultMessage: "Search Orders", + description: "navigator order mode description" + }, + noResults: { + defaultMessage: "No Results" + }, + product: { + defaultMessage: "Product" + } +}); + +export default messages; diff --git a/src/components/Navigator/modes/orders.ts b/src/components/Navigator/modes/orders.ts new file mode 100644 index 000000000..d5d44b244 --- /dev/null +++ b/src/components/Navigator/modes/orders.ts @@ -0,0 +1,45 @@ +import { IntlShape } from "react-intl"; + +import { UseNavigatorResult } from "@saleor/hooks/useNavigator"; +import { maybe, transformOrderStatus } from "@saleor/misc"; +import { orderUrl } from "@saleor/orders/urls"; +import { CheckIfOrderExists_order } from "../queries/types/CheckIfOrderExists"; +import { QuickSearchAction } from "../types"; +import messages from "./messages"; + +export function isQueryValidOrderNumber(query: string): boolean { + return query === parseInt(query, 0).toString(); +} + +export function getGqlOrderId(orderNumber: string): string { + return btoa(`Order:${orderNumber}`); +} + +function getOrdersModeActions( + query: string, + intl: IntlShape, + navigate: UseNavigatorResult, + order: CheckIfOrderExists_order +): QuickSearchAction[] { + const gqlId = getGqlOrderId(query); + + if (isQueryValidOrderNumber(query) && maybe(() => order.id === gqlId)) { + return [ + { + extraInfo: transformOrderStatus(order.status, intl).localized, + label: intl.formatMessage(messages.goToOrder, { + orderNumber: query + }), + onClick: () => { + navigate(orderUrl(gqlId)); + return false; + }, + type: "action" + } + ]; + } + + return []; +} + +export default getOrdersModeActions; diff --git a/src/components/Navigator/modes/types.ts b/src/components/Navigator/modes/types.ts new file mode 100644 index 000000000..13bab413f --- /dev/null +++ b/src/components/Navigator/modes/types.ts @@ -0,0 +1,9 @@ +import { SearchCustomers_search_edges_node } from "@saleor/searches/types/SearchCustomers"; +import { CheckIfOrderExists_order } from "../queries/types/CheckIfOrderExists"; +import { SearchCatalog } from "../queries/types/SearchCatalog"; + +export interface ActionQueries { + catalog: SearchCatalog; + customers: SearchCustomers_search_edges_node[]; + order: CheckIfOrderExists_order; +} diff --git a/src/components/Navigator/modes/utils.ts b/src/components/Navigator/modes/utils.ts new file mode 100644 index 000000000..2c0f599e1 --- /dev/null +++ b/src/components/Navigator/modes/utils.ts @@ -0,0 +1,60 @@ +import { + QuickSearchAction, + QuickSearchActionInput, + QuickSearchMode +} from "../types"; + +export function getActions(actions: QuickSearchAction[]): QuickSearchAction[] { + return actions.filter(action => action.type === "action"); +} +export function hasActions(actions: QuickSearchAction[]): boolean { + return getActions(actions).length > 0; +} + +export function getViews(actions: QuickSearchAction[]): QuickSearchAction[] { + return actions.filter(action => action.type === "view"); +} +export function hasViews(actions: QuickSearchAction[]): boolean { + return getViews(actions).length > 0; +} + +export function getCustomers( + actions: QuickSearchAction[] +): QuickSearchAction[] { + return actions.filter(action => action.type === "customer"); +} +export function hasCustomers(actions: QuickSearchAction[]): boolean { + return getCustomers(actions).length > 0; +} + +export function getCatalog(actions: QuickSearchAction[]): QuickSearchAction[] { + return actions.filter(action => action.type === "catalog"); +} +export function hasCatalog(actions: QuickSearchAction[]): boolean { + return getCatalog(actions).length > 0; +} + +export function sortScores( + a: QuickSearchActionInput, + b: QuickSearchActionInput +) { + return a.score <= b.score ? 1 : -1; +} + +export function getMode(command: string): QuickSearchMode { + switch (command) { + case ">": + return "commands"; + case "@": + return "customers"; + case "#": + return "orders"; + case "$": + return "catalog"; + case "?": + return "help"; + + default: + return null; + } +} diff --git a/src/components/Navigator/queries/types/CheckIfOrderExists.ts b/src/components/Navigator/queries/types/CheckIfOrderExists.ts new file mode 100644 index 000000000..ef91ee35e --- /dev/null +++ b/src/components/Navigator/queries/types/CheckIfOrderExists.ts @@ -0,0 +1,23 @@ +/* tslint:disable */ +/* eslint-disable */ +// This file was automatically generated and should not be edited. + +import { OrderStatus } from "./../../../../types/globalTypes"; + +// ==================================================== +// GraphQL query operation: CheckIfOrderExists +// ==================================================== + +export interface CheckIfOrderExists_order { + __typename: "Order"; + id: string; + status: OrderStatus; +} + +export interface CheckIfOrderExists { + order: CheckIfOrderExists_order | null; +} + +export interface CheckIfOrderExistsVariables { + id: string; +} diff --git a/src/components/Navigator/queries/types/SearchCatalog.ts b/src/components/Navigator/queries/types/SearchCatalog.ts new file mode 100644 index 000000000..0fd38ea68 --- /dev/null +++ b/src/components/Navigator/queries/types/SearchCatalog.ts @@ -0,0 +1,75 @@ +/* tslint:disable */ +/* eslint-disable */ +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL query operation: SearchCatalog +// ==================================================== + +export interface SearchCatalog_categories_edges_node { + __typename: "Category"; + id: string; + name: string; +} + +export interface SearchCatalog_categories_edges { + __typename: "CategoryCountableEdge"; + node: SearchCatalog_categories_edges_node; +} + +export interface SearchCatalog_categories { + __typename: "CategoryCountableConnection"; + edges: SearchCatalog_categories_edges[]; +} + +export interface SearchCatalog_collections_edges_node { + __typename: "Collection"; + id: string; + name: string; + isPublished: boolean; + publicationDate: any | null; +} + +export interface SearchCatalog_collections_edges { + __typename: "CollectionCountableEdge"; + node: SearchCatalog_collections_edges_node; +} + +export interface SearchCatalog_collections { + __typename: "CollectionCountableConnection"; + edges: SearchCatalog_collections_edges[]; +} + +export interface SearchCatalog_products_edges_node_category { + __typename: "Category"; + id: string; + name: string; +} + +export interface SearchCatalog_products_edges_node { + __typename: "Product"; + id: string; + category: SearchCatalog_products_edges_node_category; + name: string; +} + +export interface SearchCatalog_products_edges { + __typename: "ProductCountableEdge"; + node: SearchCatalog_products_edges_node; +} + +export interface SearchCatalog_products { + __typename: "ProductCountableConnection"; + edges: SearchCatalog_products_edges[]; +} + +export interface SearchCatalog { + categories: SearchCatalog_categories | null; + collections: SearchCatalog_collections | null; + products: SearchCatalog_products | null; +} + +export interface SearchCatalogVariables { + first: number; + query: string; +} diff --git a/src/components/Navigator/queries/useCatalogSearch.ts b/src/components/Navigator/queries/useCatalogSearch.ts new file mode 100644 index 000000000..acf80c47d --- /dev/null +++ b/src/components/Navigator/queries/useCatalogSearch.ts @@ -0,0 +1,66 @@ +import gql from "graphql-tag"; +import { useState } from "react"; + +import makeQuery, { UseQueryResult } from "@saleor/hooks/makeQuery"; +import useDebounce from "@saleor/hooks/useDebounce"; +import { SearchCatalog, SearchCatalogVariables } from "./types/SearchCatalog"; + +const searchCatalog = gql` + query SearchCatalog($first: Int!, $query: String!) { + categories(first: $first, filter: { search: $query }) { + edges { + node { + id + name + } + } + } + + collections(first: $first, filter: { search: $query }) { + edges { + node { + id + name + isPublished + publicationDate + } + } + } + + products(first: $first, filter: { search: $query }) { + edges { + node { + id + category { + id + name + } + name + } + } + } + } +`; + +const useSearchCatalogQuery = makeQuery( + searchCatalog +); + +type UseSearchCatalog = [ + UseQueryResult, + (query: string) => void +]; +function useSearchCatalog(first: number): UseSearchCatalog { + const [query, setQuery] = useState(""); + const setQueryDebounced = useDebounce(setQuery); + const result = useSearchCatalogQuery({ + skip: query === "", + variables: { + first, + query + } + }); + + return [result, setQueryDebounced]; +} +export default useSearchCatalog; diff --git a/src/components/Navigator/queries/useCheckIfOrderExists.ts b/src/components/Navigator/queries/useCheckIfOrderExists.ts new file mode 100644 index 000000000..d08b65a1a --- /dev/null +++ b/src/components/Navigator/queries/useCheckIfOrderExists.ts @@ -0,0 +1,41 @@ +import gql from "graphql-tag"; +import { useState } from "react"; + +import makeQuery, { UseQueryResult } from "@saleor/hooks/makeQuery"; +import useDebounce from "@saleor/hooks/useDebounce"; +import { + CheckIfOrderExists, + CheckIfOrderExistsVariables +} from "./types/CheckIfOrderExists"; + +const checkIfOrderExists = gql` + query CheckIfOrderExists($id: ID!) { + order(id: $id) { + id + status + } + } +`; + +const useCheckIfOrderExistsQuery = makeQuery< + CheckIfOrderExists, + CheckIfOrderExistsVariables +>(checkIfOrderExists); + +type UseCheckIfOrderExists = [ + UseQueryResult, + (query: string) => void +]; +function useCheckIfOrderExists(): UseCheckIfOrderExists { + const [id, setId] = useState(""); + const setIdDebounced = useDebounce(setId); + const result = useCheckIfOrderExistsQuery({ + skip: id === "", + variables: { + id + } + }); + + return [result, setIdDebounced]; +} +export default useCheckIfOrderExists; diff --git a/src/components/Navigator/types.ts b/src/components/Navigator/types.ts new file mode 100644 index 000000000..a333e29a3 --- /dev/null +++ b/src/components/Navigator/types.ts @@ -0,0 +1,24 @@ +export type QuickSearchActionType = "action" | "catalog" | "customer" | "view"; + +export interface QuickSearchAction { + caption?: string; + extraInfo?: string; + label: string; + price?: number; + symbol?: string; + type: QuickSearchActionType; + onClick: () => boolean; +} + +export interface QuickSearchActionInput extends QuickSearchAction { + score: number; + text: string; +} + +export type QuickSearchMode = + | "default" + | "catalog" + | "commands" + | "customers" + | "help" + | "orders"; diff --git a/src/components/Navigator/useQuickSearch.ts b/src/components/Navigator/useQuickSearch.ts new file mode 100644 index 000000000..edf3ddde8 --- /dev/null +++ b/src/components/Navigator/useQuickSearch.ts @@ -0,0 +1,125 @@ +import { RefObject, useEffect, useState } from "react"; +import { useIntl } from "react-intl"; + +import { DEFAULT_INITIAL_SEARCH_DATA } from "@saleor/config"; +import { ChangeEvent, FormChange } from "@saleor/hooks/useForm"; +import useModalDialogOpen from "@saleor/hooks/useModalDialogOpen"; +import useNavigator from "@saleor/hooks/useNavigator"; +import { maybe } from "@saleor/misc"; +import { useOrderDraftCreateMutation } from "@saleor/orders/mutations"; +import { orderUrl } from "@saleor/orders/urls"; +import useCustomerSearch from "@saleor/searches/useCustomerSearch"; +import getModeActions from "./modes"; +import { getGqlOrderId, isQueryValidOrderNumber } from "./modes/orders"; +import { getMode } from "./modes/utils"; +import useSearchCatalog from "./queries/useCatalogSearch"; +import useCheckIfOrderExists from "./queries/useCheckIfOrderExists"; +import { QuickSearchAction, QuickSearchMode } from "./types"; + +type UseQuickSearch = [ + string, + QuickSearchMode, + FormChange, + QuickSearchAction[] +]; +function useQuickSearch( + open: boolean, + input: RefObject +): UseQuickSearch { + const [query, setQuery] = useState(""); + const [mode, setMode] = useState("default"); + const intl = useIntl(); + const navigate = useNavigator(); + const [{ data: orderData }, getOrderData] = useCheckIfOrderExists(); + const { result: customers, search: searchCustomers } = useCustomerSearch({ + variables: { + ...DEFAULT_INITIAL_SEARCH_DATA, + first: 5 + } + }); + const [{ data: catalog }, searchCatalog] = useSearchCatalog(5); + const [createOrder] = useOrderDraftCreateMutation({ + onCompleted: result => { + if (result.draftOrderCreate.errors.length === 0) { + navigate(orderUrl(result.draftOrderCreate.order.id)); + } + } + }); + + useModalDialogOpen(open, { + onClose: () => { + setMode("default"); + setQuery(""); + } + }); + + const handleBack = (event: KeyboardEvent) => { + // `any` type because of poorly typed `KeyboardEvent.EventTarget` which + // has no `value` key. Which it would have if `KeyboardEvent` and + // `EventTarget` would be generic types accepting HTMLDOM element types. + if ((event.target as any).value === "" && event.keyCode === 8) { + setMode("default"); + } + }; + + useEffect(() => { + setQuery(""); + if (mode !== "default" && input.current) { + input.current.addEventListener("keyup", handleBack); + + return () => { + if (input.current) { + input.current.removeEventListener("keyup", handleBack); + } + }; + } + }, [mode, open]); + + const change = (event: ChangeEvent) => { + const value = event.target.value; + + if (mode === "default" || mode === "help") { + const newMode = getMode(value); + if (newMode) { + setMode(newMode); + } + } + if (mode === "orders" && isQueryValidOrderNumber(value)) { + getOrderData(getGqlOrderId(value)); + } + if (mode === "catalog") { + searchCatalog(value); + } + if (mode === "customers") { + searchCustomers(value); + } + + setQuery(value); + }; + + return [ + query, + mode, + change, + getModeActions( + mode, + query, + intl, + { + catalog, + customers: maybe( + () => customers.data.search.edges.map(edge => edge.node), + [] + ), + order: maybe(() => orderData.order) + }, + { + createOrder, + navigate, + setMode + } + ) + ]; +} + +export default useQuickSearch; diff --git a/src/components/messages/MessageManager.tsx b/src/components/messages/MessageManager.tsx index 11f4f659f..e580ce721 100644 --- a/src/components/messages/MessageManager.tsx +++ b/src/components/messages/MessageManager.tsx @@ -1,6 +1,7 @@ import Button from "@material-ui/core/Button"; import IconButton from "@material-ui/core/IconButton"; import Snackbar from "@material-ui/core/Snackbar"; +import Typography from "@material-ui/core/Typography"; import CloseIcon from "@material-ui/icons/Close"; import React from "react"; @@ -15,7 +16,7 @@ interface MessageManagerState { } export class MessageManager extends React.Component<{}, MessageManagerState> { - state = { + state: MessageManagerState = { message: { text: "", key: "0", onUndo: undefined }, opened: false }; @@ -55,7 +56,7 @@ export class MessageManager extends React.Component<{}, MessageManagerState> { }; render() { - const { text, key, onUndo } = this.state.message; + const { autohide = 3000, title, text, key, onUndo } = this.state.message; return ( <> { vertical: "top" }} open={this.state.opened} - autoHideDuration={3000} + autoHideDuration={autohide} onClose={this.handleClose} onExited={this.handleExited} ContentProps={{ @@ -73,9 +74,15 @@ export class MessageManager extends React.Component<{}, MessageManagerState> { }} message={ + {title && ( + + {title} + + )} {text} } + title={title} action={[ !!onUndo ? ( diff --git a/src/hooks/makeMutation.ts b/src/hooks/makeMutation.ts index b6cfd4a91..0ec08736c 100644 --- a/src/hooks/makeMutation.ts +++ b/src/hooks/makeMutation.ts @@ -11,15 +11,15 @@ import { commonMessages } from "@saleor/intl"; import { maybe } from "@saleor/misc"; import useNotifier from "./useNotifier"; -type UseMutation = [ +export type UseMutation = [ MutationFunction, MutationResult ]; -type UseMutationCbs = Partial<{ +export type UseMutationCbs = Partial<{ onCompleted: (data: TData) => void; onError: (error: ApolloError) => void; }>; -type UseMutationHook = ( +export type UseMutationHook = ( cbs: UseMutationCbs ) => UseMutation; diff --git a/src/hooks/useDebounce.ts b/src/hooks/useDebounce.ts index 69be1e40e..b228152f4 100644 --- a/src/hooks/useDebounce.ts +++ b/src/hooks/useDebounce.ts @@ -6,12 +6,13 @@ function useDebounce( time = 200 ): UseDebounceFn { const timer = useRef(null); - useEffect(() => () => clearTimeout(timer.current)); + useEffect(() => () => clearTimeout(timer.current), []); return (...args: T[]) => { if (timer.current) { clearTimeout(timer.current); } + timer.current = setTimeout(() => debounceFn(...args), time); }; } diff --git a/src/index.tsx b/src/index.tsx index b229ec5d1..915593649 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -12,6 +12,7 @@ import ErrorBoundary from "react-error-boundary"; import { useIntl } from "react-intl"; import { BrowserRouter, Route, Switch } from "react-router-dom"; +import Navigator from "@saleor/components/Navigator"; import useAppState from "@saleor/hooks/useAppState"; import AttributeSection from "./attributes"; import { attributeSection } from "./attributes/urls"; @@ -153,6 +154,7 @@ const Routes: React.FC = () => { }) => isAuthenticated && !tokenAuthLoading && !tokenVerifyLoading ? ( + dispatchAppState({ diff --git a/src/orders/components/OrderDetailsPage/OrderDetailsPage.tsx b/src/orders/components/OrderDetailsPage/OrderDetailsPage.tsx index 886d46d9c..18bc44472 100644 --- a/src/orders/components/OrderDetailsPage/OrderDetailsPage.tsx +++ b/src/orders/components/OrderDetailsPage/OrderDetailsPage.tsx @@ -26,8 +26,7 @@ import OrderUnfulfilledItems from "../OrderUnfulfilledItems/OrderUnfulfilledItem const useStyles = makeStyles( theme => ({ date: { - marginBottom: theme.spacing(3), - marginTop: -theme.spacing(2) + marginBottom: theme.spacing(3) }, header: { display: "flex", diff --git a/src/orders/fixtures.ts b/src/orders/fixtures.ts index 4463ec60c..754d8628d 100644 --- a/src/orders/fixtures.ts +++ b/src/orders/fixtures.ts @@ -15,22 +15,30 @@ export const clients: SearchCustomers_search_edges_node[] = [ { __typename: "User" as "User", email: "test.client1@example.com", - id: "c1" + firstName: "John", + id: "c1", + lastName: "Doe" }, { __typename: "User" as "User", email: "test.client2@example.com", - id: "c2" + firstName: "Dough", + id: "c2", + lastName: "Jones" }, { __typename: "User" as "User", email: "test.client3@example.com", - id: "c3" + firstName: "Jonas", + id: "c3", + lastName: "Dough" }, { __typename: "User" as "User", email: "test.client4@example.com", - id: "c4" + firstName: "Bill", + id: "c4", + lastName: "Jonas" } ]; export const orders: OrderList_orders_edges_node[] = [ diff --git a/src/orders/mutations.ts b/src/orders/mutations.ts index 8ab8f659b..4a68ab6ba 100644 --- a/src/orders/mutations.ts +++ b/src/orders/mutations.ts @@ -1,5 +1,6 @@ import gql from "graphql-tag"; +import makeMutation from "@saleor/hooks/makeMutation"; import { TypedMutation } from "../mutations"; import { fragmentAddress, @@ -409,10 +410,9 @@ const orderDraftCreateMutation = gql` } } `; -export const TypedOrderDraftCreateMutation = TypedMutation< - OrderDraftCreate, - {} ->(orderDraftCreateMutation); +export const useOrderDraftCreateMutation = makeMutation( + orderDraftCreateMutation +); const orderLineDeleteMutation = gql` ${fragmentOrderDetails} diff --git a/src/orders/views/OrderDraftList/OrderDraftList.tsx b/src/orders/views/OrderDraftList/OrderDraftList.tsx index bbda0756e..f9b1ad9a7 100644 --- a/src/orders/views/OrderDraftList/OrderDraftList.tsx +++ b/src/orders/views/OrderDraftList/OrderDraftList.tsx @@ -21,7 +21,7 @@ import { ListViews } from "@saleor/types"; import OrderDraftListPage from "../../components/OrderDraftListPage"; import { TypedOrderDraftBulkCancelMutation, - TypedOrderDraftCreateMutation + useOrderDraftCreateMutation } from "../../mutations"; import { TypedOrderDraftListQuery } from "../../queries"; import { OrderDraftBulkCancel } from "../../types/OrderDraftBulkCancel"; @@ -58,6 +58,19 @@ export const OrderDraftList: React.FC = ({ params }) => { ); const intl = useIntl(); + const handleCreateOrderCreateSuccess = (data: OrderDraftCreate) => { + notify({ + text: intl.formatMessage({ + defaultMessage: "Order draft succesfully created" + }) + }); + navigate(orderUrl(data.draftOrderCreate.order.id)); + }; + + const [createOrder] = useOrderDraftCreateMutation({ + onCompleted: handleCreateOrderCreateSuccess + }); + const tabs = getFilterTabs(); const currentTab = @@ -88,15 +101,6 @@ export const OrderDraftList: React.FC = ({ params }) => { true ); - const handleCreateOrderCreateSuccess = (data: OrderDraftCreate) => { - notify({ - text: intl.formatMessage({ - defaultMessage: "Order draft succesfully created" - }) - }); - navigate(orderUrl(data.draftOrderCreate.order.id)); - }; - const openModal = (action: OrderDraftListUrlDialog, ids?: string[]) => navigate( orderDraftListUrl({ @@ -137,140 +141,133 @@ export const OrderDraftList: React.FC = ({ params }) => { ); return ( - - {createOrder => ( - - {({ data, loading, refetch }) => { - const { loadNextPage, loadPreviousPage, pageInfo } = paginate( - maybe(() => data.draftOrders.pageInfo), - paginationState, - params - ); + + {({ data, loading, refetch }) => { + const { loadNextPage, loadPreviousPage, pageInfo } = paginate( + maybe(() => data.draftOrders.pageInfo), + paginationState, + params + ); - const handleOrderDraftBulkCancel = (data: OrderDraftBulkCancel) => { - if (data.draftOrderBulkDelete.errors.length === 0) { - notify({ - text: intl.formatMessage({ - defaultMessage: "Deleted draft orders" - }) + const handleOrderDraftBulkCancel = (data: OrderDraftBulkCancel) => { + if (data.draftOrderBulkDelete.errors.length === 0) { + notify({ + text: intl.formatMessage({ + defaultMessage: "Deleted draft orders" + }) + }); + refetch(); + reset(); + closeModal(); + } + }; + + return ( + + {(orderDraftBulkDelete, orderDraftBulkDeleteOpts) => { + const bulkRemoveTransitionState = getMutationState( + orderDraftBulkDeleteOpts.called, + orderDraftBulkDeleteOpts.loading, + maybe( + () => + orderDraftBulkDeleteOpts.data.draftOrderBulkDelete.errors + ) + ); + const onOrderDraftBulkDelete = () => + orderDraftBulkDelete({ + variables: { + ids: params.ids + } }); - refetch(); - reset(); - closeModal(); - } - }; - return ( - - {(orderDraftBulkDelete, orderDraftBulkDeleteOpts) => { - const bulkRemoveTransitionState = getMutationState( - orderDraftBulkDeleteOpts.called, - orderDraftBulkDeleteOpts.loading, - maybe( - () => - orderDraftBulkDeleteOpts.data.draftOrderBulkDelete - .errors - ) - ); - const onOrderDraftBulkDelete = () => - orderDraftBulkDelete({ - variables: { - ids: params.ids - } - }); - - return ( - <> - changeFilterField({ query })} - onAll={() => navigate(orderDraftListUrl())} - onTabChange={handleTabChange} - onTabDelete={() => openModal("delete-search")} - onTabSave={() => openModal("save-search")} - tabs={tabs.map(tab => tab.name)} - disabled={loading} - settings={settings} - orders={maybe(() => - data.draftOrders.edges.map(edge => edge.node) - )} - pageInfo={pageInfo} - onAdd={createOrder} - onNextPage={loadNextPage} - onPreviousPage={loadPreviousPage} - onUpdateListSettings={updateListSettings} - onRowClick={id => () => navigate(orderUrl(id))} - isChecked={isSelected} - selected={listElements.length} - toggle={toggle} - toggleAll={toggleAll} - toolbar={ - - navigate( - orderDraftListUrl({ - action: "remove", - ids: listElements - }) - ) - } - > - - + return ( + <> + changeFilterField({ query })} + onAll={() => navigate(orderDraftListUrl())} + onTabChange={handleTabChange} + onTabDelete={() => openModal("delete-search")} + onTabSave={() => openModal("save-search")} + tabs={tabs.map(tab => tab.name)} + disabled={loading} + settings={settings} + orders={maybe(() => + data.draftOrders.edges.map(edge => edge.node) + )} + pageInfo={pageInfo} + onAdd={createOrder} + onNextPage={loadNextPage} + onPreviousPage={loadPreviousPage} + onUpdateListSettings={updateListSettings} + onRowClick={id => () => navigate(orderUrl(id))} + isChecked={isSelected} + selected={listElements.length} + toggle={toggle} + toggleAll={toggleAll} + toolbar={ + + navigate( + orderDraftListUrl({ + action: "remove", + ids: listElements + }) + ) } - /> - - - params.ids.length), - displayQuantity: ( - - {maybe(() => params.ids.length)} - - ) - }} - /> - - - + + } + /> + + + params.ids.length), + displayQuantity: ( + {maybe(() => params.ids.length)} + ) + }} /> - tabs[currentTab - 1].name, "...")} - /> - - ); - }} - - ); - }} - - )} - + + + + tabs[currentTab - 1].name, "...")} + /> + + ); + }} + + ); + }} + ); }; diff --git a/src/orders/views/OrderList/OrderList.tsx b/src/orders/views/OrderList/OrderList.tsx index 26e49fd22..e5b19e27f 100644 --- a/src/orders/views/OrderList/OrderList.tsx +++ b/src/orders/views/OrderList/OrderList.tsx @@ -21,7 +21,7 @@ import OrderBulkCancelDialog from "../../components/OrderBulkCancelDialog"; import OrderListPage from "../../components/OrderListPage/OrderListPage"; import { TypedOrderBulkCancelMutation, - TypedOrderDraftCreateMutation + useOrderDraftCreateMutation } from "../../mutations"; import { TypedOrderListQuery } from "../../queries"; import { OrderBulkCancel } from "../../types/OrderBulkCancel"; @@ -62,6 +62,19 @@ export const OrderList: React.FC = ({ params }) => { ); const intl = useIntl(); + const handleCreateOrderCreateSuccess = (data: OrderDraftCreate) => { + notify({ + text: intl.formatMessage({ + defaultMessage: "Order draft succesfully created" + }) + }); + navigate(orderUrl(data.draftOrderCreate.order.id)); + }; + + const [createOrder] = useOrderDraftCreateMutation({ + onCompleted: handleCreateOrderCreateSuccess + }); + const tabs = getFilterTabs(); const currentTab = @@ -130,15 +143,6 @@ export const OrderList: React.FC = ({ params }) => { const paginationState = createPaginationState(settings.rowNumber, params); const currencySymbol = maybe(() => shop.defaultCurrency, "USD"); - const handleCreateOrderCreateSuccess = (data: OrderDraftCreate) => { - notify({ - text: intl.formatMessage({ - defaultMessage: "Order draft succesfully created" - }) - }); - navigate(orderUrl(data.draftOrderCreate.order.id)); - }; - const queryVariables = React.useMemo( () => ({ ...paginationState, @@ -148,131 +152,127 @@ export const OrderList: React.FC = ({ params }) => { ); return ( - - {createOrder => ( - - {({ data, loading, refetch }) => { - const { loadNextPage, loadPreviousPage, pageInfo } = paginate( - maybe(() => data.orders.pageInfo), - paginationState, - params - ); + + {({ data, loading, refetch }) => { + const { loadNextPage, loadPreviousPage, pageInfo } = paginate( + maybe(() => data.orders.pageInfo), + paginationState, + params + ); - const handleOrderBulkCancel = (data: OrderBulkCancel) => { - if (data.orderBulkCancel.errors.length === 0) { - notify({ - text: intl.formatMessage({ - defaultMessage: "Orders cancelled" - }) + const handleOrderBulkCancel = (data: OrderBulkCancel) => { + if (data.orderBulkCancel.errors.length === 0) { + notify({ + text: intl.formatMessage({ + defaultMessage: "Orders cancelled" + }) + }); + reset(); + refetch(); + closeModal(); + } + }; + + return ( + + {(orderBulkCancel, orderBulkCancelOpts) => { + const orderBulkCancelTransitionState = getMutationState( + orderBulkCancelOpts.called, + orderBulkCancelOpts.loading, + maybe(() => orderBulkCancelOpts.data.orderBulkCancel.errors) + ); + const onOrderBulkCancel = (restock: boolean) => + orderBulkCancel({ + variables: { + ids: params.ids, + restock + } }); - reset(); - refetch(); - closeModal(); - } - }; - return ( - - {(orderBulkCancel, orderBulkCancelOpts) => { - const orderBulkCancelTransitionState = getMutationState( - orderBulkCancelOpts.called, - orderBulkCancelOpts.loading, - maybe(() => orderBulkCancelOpts.data.orderBulkCancel.errors) - ); - const onOrderBulkCancel = (restock: boolean) => - orderBulkCancel({ - variables: { - ids: params.ids, - restock - } - }); - - return ( - <> - - data.orders.edges.map(edge => edge.node) - )} - pageInfo={pageInfo} - onAdd={createOrder} - onNextPage={loadNextPage} - onPreviousPage={loadPreviousPage} - onUpdateListSettings={updateListSettings} - onRowClick={id => () => navigate(orderUrl(id))} - isChecked={isSelected} - selected={listElements.length} - toggle={toggle} - toggleAll={toggleAll} - toolbar={ - - } - onSearchChange={query => changeFilterField({ query })} - onFilterAdd={data => - changeFilterField(createFilter(params, data)) - } - onTabSave={() => openModal("save-search")} - onTabDelete={() => openModal("delete-search")} - onTabChange={handleTabChange} - initialSearch={params.query || ""} - tabs={getFilterTabs().map(tab => tab.name)} - onAll={() => - changeFilters({ - status: undefined - }) - } - /> - params.ids.length.toString(), - "..." - )} - onClose={closeModal} - onConfirm={onOrderBulkCancel} - open={params.action === "cancel"} - /> - - tabs[currentTab - 1].name, "...")} - /> - - ); - }} - - ); - }} - - )} - + return ( + <> + + data.orders.edges.map(edge => edge.node) + )} + pageInfo={pageInfo} + onAdd={createOrder} + onNextPage={loadNextPage} + onPreviousPage={loadPreviousPage} + onUpdateListSettings={updateListSettings} + onRowClick={id => () => navigate(orderUrl(id))} + isChecked={isSelected} + selected={listElements.length} + toggle={toggle} + toggleAll={toggleAll} + toolbar={ + + } + onSearchChange={query => changeFilterField({ query })} + onFilterAdd={data => + changeFilterField(createFilter(params, data)) + } + onTabSave={() => openModal("save-search")} + onTabDelete={() => openModal("delete-search")} + onTabChange={handleTabChange} + initialSearch={params.query || ""} + tabs={getFilterTabs().map(tab => tab.name)} + onAll={() => + changeFilters({ + status: undefined + }) + } + /> + params.ids.length.toString(), + "..." + )} + onClose={closeModal} + onConfirm={onOrderBulkCancel} + open={params.action === "cancel"} + /> + + tabs[currentTab - 1].name, "...")} + /> + + ); + }} + + ); + }} + ); }; diff --git a/src/plugins/types/PluginUpdate.ts b/src/plugins/types/PluginUpdate.ts index 1a5bb77a2..49a2d0b9e 100644 --- a/src/plugins/types/PluginUpdate.ts +++ b/src/plugins/types/PluginUpdate.ts @@ -2,10 +2,7 @@ /* eslint-disable */ // This file was automatically generated and should not be edited. -import { - PluginUpdateInput, - ConfigurationTypeFieldEnum -} from "./../../types/globalTypes"; +import { PluginUpdateInput, ConfigurationTypeFieldEnum } from "./../../types/globalTypes"; // ==================================================== // GraphQL mutation operation: PluginUpdate @@ -32,9 +29,7 @@ export interface PluginUpdate_pluginUpdate_plugin { name: string; description: string; active: boolean; - configuration: - | (PluginUpdate_pluginUpdate_plugin_configuration | null)[] - | null; + configuration: (PluginUpdate_pluginUpdate_plugin_configuration | null)[] | null; } export interface PluginUpdate_pluginUpdate { diff --git a/src/searches/types/SearchCustomers.ts b/src/searches/types/SearchCustomers.ts index c63a1a4b6..37c864c0b 100644 --- a/src/searches/types/SearchCustomers.ts +++ b/src/searches/types/SearchCustomers.ts @@ -10,6 +10,8 @@ export interface SearchCustomers_search_edges_node { __typename: "User"; id: string; email: string; + firstName: string; + lastName: string; } export interface SearchCustomers_search_edges { diff --git a/src/searches/types/SearchProductTypes.ts b/src/searches/types/SearchProductTypes.ts index 300ebc329..a35124ca1 100644 --- a/src/searches/types/SearchProductTypes.ts +++ b/src/searches/types/SearchProductTypes.ts @@ -22,9 +22,7 @@ export interface SearchProductTypes_search_edges_node_productAttributes { slug: string | null; name: string | null; valueRequired: boolean; - values: - | (SearchProductTypes_search_edges_node_productAttributes_values | null)[] - | null; + values: (SearchProductTypes_search_edges_node_productAttributes_values | null)[] | null; } export interface SearchProductTypes_search_edges_node { @@ -32,9 +30,7 @@ export interface SearchProductTypes_search_edges_node { id: string; name: string; hasVariants: boolean; - productAttributes: - | (SearchProductTypes_search_edges_node_productAttributes | null)[] - | null; + productAttributes: (SearchProductTypes_search_edges_node_productAttributes | null)[] | null; } export interface SearchProductTypes_search_edges { diff --git a/src/searches/useCustomerSearch.ts b/src/searches/useCustomerSearch.ts index bd046f0ab..92f4165d9 100644 --- a/src/searches/useCustomerSearch.ts +++ b/src/searches/useCustomerSearch.ts @@ -15,6 +15,8 @@ export const searchCustomers = gql` node { id email + firstName + lastName } } pageInfo { diff --git a/src/siteSettings/types/ShopSettingsUpdate.ts b/src/siteSettings/types/ShopSettingsUpdate.ts index 0a9b046a4..2b8419636 100644 --- a/src/siteSettings/types/ShopSettingsUpdate.ts +++ b/src/siteSettings/types/ShopSettingsUpdate.ts @@ -2,12 +2,7 @@ /* eslint-disable */ // This file was automatically generated and should not be edited. -import { - SiteDomainInput, - ShopSettingsInput, - AddressInput, - AuthorizationKeyType -} from "./../../types/globalTypes"; +import { SiteDomainInput, ShopSettingsInput, AddressInput, AuthorizationKeyType } from "./../../types/globalTypes"; // ==================================================== // GraphQL mutation operation: ShopSettingsUpdate diff --git a/src/storybook/__snapshots__/Stories.test.ts.snap b/src/storybook/__snapshots__/Stories.test.ts.snap index 9c231db02..ec9718967 100644 --- a/src/storybook/__snapshots__/Stories.test.ts.snap +++ b/src/storybook/__snapshots__/Stories.test.ts.snap @@ -29700,7 +29700,7 @@ exports[`Storyshots Views / Customers / Create customer default 1`] = `
- Add Customer + Create Customer
- Add Customer + Create Customer
- Add Customer + Create Customer
- Add customer + Create customer
@@ -37802,7 +37802,7 @@ exports[`Storyshots Views / Customers / Customer list loading 1`] = ` - Add customer + Create customer @@ -38131,7 +38131,7 @@ exports[`Storyshots Views / Customers / Customer list no data 1`] = ` - Add customer + Create customer diff --git a/src/theme.ts b/src/theme.ts index d57ce19d0..24530be60 100644 --- a/src/theme.ts +++ b/src/theme.ts @@ -340,7 +340,8 @@ export default (colors: IThemeColors): Theme => "& svg": { color: colors.font.default } - } + }, + alignSelf: "baseline" }, message: { fontSize: 16 @@ -349,7 +350,10 @@ export default (colors: IThemeColors): Theme => backgroundColor: colors.background.paper, boxShadow: "0 6px 10px 0px rgba(0, 0, 0, 0.15), 0 1px 18px 0px rgba(0, 0, 0, 0.12), 0 3px 5px -1px rgba(0, 0, 0, 0.10)", - color: colors.font.default + color: colors.font.default, + display: "grid", + gridTemplateColumns: "1fr 56px", + maxWidth: 480 } }, MuiSwitch: { diff --git a/src/types/globalTypes.ts b/src/types/globalTypes.ts index 10b35084d..44ec52260 100644 --- a/src/types/globalTypes.ts +++ b/src/types/globalTypes.ts @@ -8,41 +8,41 @@ export enum AddressTypeEnum { BILLING = "BILLING", - SHIPPING = "SHIPPING" + SHIPPING = "SHIPPING", } export enum AttributeInputTypeEnum { DROPDOWN = "DROPDOWN", - MULTISELECT = "MULTISELECT" + MULTISELECT = "MULTISELECT", } export enum AttributeTypeEnum { PRODUCT = "PRODUCT", - VARIANT = "VARIANT" + VARIANT = "VARIANT", } export enum AttributeValueType { COLOR = "COLOR", GRADIENT = "GRADIENT", STRING = "STRING", - URL = "URL" + URL = "URL", } export enum AuthorizationKeyType { FACEBOOK = "FACEBOOK", - GOOGLE_OAUTH2 = "GOOGLE_OAUTH2" + GOOGLE_OAUTH2 = "GOOGLE_OAUTH2", } export enum CollectionPublished { HIDDEN = "HIDDEN", - PUBLISHED = "PUBLISHED" + PUBLISHED = "PUBLISHED", } export enum ConfigurationTypeFieldEnum { BOOLEAN = "BOOLEAN", PASSWORD = "PASSWORD", SECRET = "SECRET", - STRING = "STRING" + STRING = "STRING", } export enum CountryCode { @@ -295,23 +295,23 @@ export enum CountryCode { YT = "YT", ZA = "ZA", ZM = "ZM", - ZW = "ZW" + ZW = "ZW", } export enum DiscountStatusEnum { ACTIVE = "ACTIVE", EXPIRED = "EXPIRED", - SCHEDULED = "SCHEDULED" + SCHEDULED = "SCHEDULED", } export enum DiscountValueTypeEnum { FIXED = "FIXED", - PERCENTAGE = "PERCENTAGE" + PERCENTAGE = "PERCENTAGE", } export enum FulfillmentStatus { CANCELED = "CANCELED", - FULFILLED = "FULFILLED" + FULFILLED = "FULFILLED", } export enum LanguageCodeEnum { @@ -357,19 +357,19 @@ export enum LanguageCodeEnum { UK = "UK", VI = "VI", ZH_HANS = "ZH_HANS", - ZH_HANT = "ZH_HANT" + ZH_HANT = "ZH_HANT", } export enum OrderAction { CAPTURE = "CAPTURE", MARK_AS_PAID = "MARK_AS_PAID", REFUND = "REFUND", - VOID = "VOID" + VOID = "VOID", } export enum OrderDirection { ASC = "ASC", - DESC = "DESC" + DESC = "DESC", } export enum OrderEventsEmailsEnum { @@ -378,7 +378,7 @@ export enum OrderEventsEmailsEnum { ORDER_CONFIRMATION = "ORDER_CONFIRMATION", PAYMENT_CONFIRMATION = "PAYMENT_CONFIRMATION", SHIPPING_CONFIRMATION = "SHIPPING_CONFIRMATION", - TRACKING_UPDATED = "TRACKING_UPDATED" + TRACKING_UPDATED = "TRACKING_UPDATED", } export enum OrderEventsEnum { @@ -402,7 +402,7 @@ export enum OrderEventsEnum { PLACED = "PLACED", PLACED_FROM_DRAFT = "PLACED_FROM_DRAFT", TRACKING_UPDATED = "TRACKING_UPDATED", - UPDATED_ADDRESS = "UPDATED_ADDRESS" + UPDATED_ADDRESS = "UPDATED_ADDRESS", } export enum OrderStatus { @@ -410,7 +410,7 @@ export enum OrderStatus { DRAFT = "DRAFT", FULFILLED = "FULFILLED", PARTIALLY_FULFILLED = "PARTIALLY_FULFILLED", - UNFULFILLED = "UNFULFILLED" + UNFULFILLED = "UNFULFILLED", } export enum OrderStatusFilter { @@ -419,7 +419,7 @@ export enum OrderStatusFilter { PARTIALLY_FULFILLED = "PARTIALLY_FULFILLED", READY_TO_CAPTURE = "READY_TO_CAPTURE", READY_TO_FULFILL = "READY_TO_FULFILL", - UNFULFILLED = "UNFULFILLED" + UNFULFILLED = "UNFULFILLED", } export enum PaymentChargeStatusEnum { @@ -427,7 +427,7 @@ export enum PaymentChargeStatusEnum { FULLY_REFUNDED = "FULLY_REFUNDED", NOT_CHARGED = "NOT_CHARGED", PARTIALLY_CHARGED = "PARTIALLY_CHARGED", - PARTIALLY_REFUNDED = "PARTIALLY_REFUNDED" + PARTIALLY_REFUNDED = "PARTIALLY_REFUNDED", } export enum PermissionEnum { @@ -445,7 +445,7 @@ export enum PermissionEnum { MANAGE_STAFF = "MANAGE_STAFF", MANAGE_TRANSLATIONS = "MANAGE_TRANSLATIONS", MANAGE_USERS = "MANAGE_USERS", - MANAGE_WEBHOOKS = "MANAGE_WEBHOOKS" + MANAGE_WEBHOOKS = "MANAGE_WEBHOOKS", } export enum ProductErrorCode { @@ -459,7 +459,7 @@ export enum ProductErrorCode { NOT_PRODUCTS_IMAGE = "NOT_PRODUCTS_IMAGE", REQUIRED = "REQUIRED", UNIQUE = "UNIQUE", - VARIANT_NO_DIGITAL_CONTENT = "VARIANT_NO_DIGITAL_CONTENT" + VARIANT_NO_DIGITAL_CONTENT = "VARIANT_NO_DIGITAL_CONTENT", } export enum ProductOrderField { @@ -468,37 +468,37 @@ export enum ProductOrderField { NAME = "NAME", PRICE = "PRICE", PUBLISHED = "PUBLISHED", - TYPE = "TYPE" + TYPE = "TYPE", } export enum ProductTypeConfigurable { CONFIGURABLE = "CONFIGURABLE", - SIMPLE = "SIMPLE" + SIMPLE = "SIMPLE", } export enum ProductTypeEnum { DIGITAL = "DIGITAL", - SHIPPABLE = "SHIPPABLE" + SHIPPABLE = "SHIPPABLE", } export enum SaleType { FIXED = "FIXED", - PERCENTAGE = "PERCENTAGE" + PERCENTAGE = "PERCENTAGE", } export enum ShippingMethodTypeEnum { PRICE = "PRICE", - WEIGHT = "WEIGHT" + WEIGHT = "WEIGHT", } export enum StaffMemberStatus { ACTIVE = "ACTIVE", - DEACTIVATED = "DEACTIVATED" + DEACTIVATED = "DEACTIVATED", } export enum StockAvailability { IN_STOCK = "IN_STOCK", - OUT_OF_STOCK = "OUT_OF_STOCK" + OUT_OF_STOCK = "OUT_OF_STOCK", } export enum TaxRateType { @@ -526,19 +526,19 @@ export enum TaxRateType { SOCIAL_HOUSING = "SOCIAL_HOUSING", STANDARD = "STANDARD", WATER = "WATER", - WINE = "WINE" + WINE = "WINE", } export enum VoucherDiscountType { FIXED = "FIXED", PERCENTAGE = "PERCENTAGE", - SHIPPING = "SHIPPING" + SHIPPING = "SHIPPING", } export enum VoucherTypeEnum { ENTIRE_ORDER = "ENTIRE_ORDER", SHIPPING = "SHIPPING", - SPECIFIC_PRODUCT = "SPECIFIC_PRODUCT" + SPECIFIC_PRODUCT = "SPECIFIC_PRODUCT", } export enum WebhookErrorCode { @@ -546,7 +546,7 @@ export enum WebhookErrorCode { INVALID = "INVALID", NOT_FOUND = "NOT_FOUND", REQUIRED = "REQUIRED", - UNIQUE = "UNIQUE" + UNIQUE = "UNIQUE", } export enum WebhookEventTypeEnum { @@ -557,14 +557,14 @@ export enum WebhookEventTypeEnum { ORDER_FULFILLED = "ORDER_FULFILLED", ORDER_FULLY_PAID = "ORDER_FULLY_PAID", ORDER_UPDATED = "ORDER_UPDATED", - PRODUCT_CREATED = "PRODUCT_CREATED" + PRODUCT_CREATED = "PRODUCT_CREATED", } export enum WeightUnitsEnum { G = "G", KG = "KG", LB = "LB", - OZ = "OZ" + OZ = "OZ", } export interface AddressInput {