diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e6250a96..d8ebcf167 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ All notable, unreleased changes to this project will be documented in this file. - Stop using deprecated fields - #357 by @dominik-zeglen - Throw error when API_URI is not set - #375 by @dominik-zeglen - Fix variant stock input - #377 by @dominik-zeglen +- Add filtering to views - #361 by @dominik-zeglen - Do not render password change if authenticating - #378 by @dominik-zeglen ## 2.0.0 diff --git a/locale/messages.pot b/locale/messages.pot index 2d58e0b2e..16ddf2c07 100644 --- a/locale/messages.pot +++ b/locale/messages.pot @@ -1,6 +1,6 @@ msgid "" msgstr "" -"POT-Creation-Date: 2019-12-04T14:17:34.264Z\n" +"POT-Creation-Date: 2020-01-20T16:06:58.433Z\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "MIME-Version: 1.0\n" @@ -95,6 +95,22 @@ msgctxt "table actions" msgid "Actions" msgstr "" +#: build/locale/src/discounts/components/SaleListPage/filters.json +#. [src.discounts.components.SaleListPage.active] - sale status +#. defaultMessage is: +#. Active +msgctxt "sale status" +msgid "Active" +msgstr "" + +#: build/locale/src/discounts/components/VoucherListPage/filters.json +#. [src.discounts.components.VoucherListPage.active] - voucher status +#. defaultMessage is: +#. Active +msgctxt "voucher status" +msgid "Active" +msgstr "" + #: build/locale/src/plugins/components/PluginsList/PluginsList.json #. [src.plugins.components.PluginsList.3247064221] - plugin status #. defaultMessage is: @@ -103,6 +119,22 @@ msgctxt "plugin status" msgid "Active" msgstr "" +#: build/locale/src/plugins/components/PluginsListPage/filters.json +#. [src.plugins.components.PluginsListPage.active] - plugin +#. defaultMessage is: +#. Active +msgctxt "plugin" +msgid "Active" +msgstr "" + +#: build/locale/src/services/components/ServiceListPage/filters.json +#. [src.services.components.ServiceListPage.active] - service account +#. defaultMessage is: +#. Active +msgctxt "service account" +msgid "Active" +msgstr "" + #: build/locale/src/staff/components/StaffList/StaffList.json #. [src.staff.components.StaffList.3247064221] - staff member status #. defaultMessage is: @@ -111,6 +143,22 @@ msgctxt "staff member status" msgid "Active" msgstr "" +#: build/locale/src/staff/components/StaffListPage/filters.json +#. [src.staff.components.StaffListPage.active] - staff member's account +#. defaultMessage is: +#. Active +msgctxt "staff member's account" +msgid "Active" +msgstr "" + +#: build/locale/src/webhooks/components/WebhooksListPage/filters.json +#. [src.webhooks.components.WebhooksListPage.active] - webhook +#. defaultMessage is: +#. Active +msgctxt "webhook" +msgid "Active" +msgstr "" + #: build/locale/src/discounts/components/DiscountDates/DiscountDates.json #. [src.discounts.components.DiscountDates.1662220323] - time during discount is active, header #. defaultMessage is: @@ -167,14 +215,6 @@ msgctxt "page header" msgid "Add Collection" msgstr "" -#: build/locale/src/components/Filter/Filter.json -#. [src.components.Filter.2852521946] - button -#. defaultMessage is: -#. Add Filter -msgctxt "button" -msgid "Add Filter" -msgstr "" - #: build/locale/src/components/RichTextEditor/ImageSource.json #. [src.components.RichTextEditor.1603794322] - dialog header #. defaultMessage is: @@ -255,14 +295,6 @@ msgctxt "button" msgid "Add authentication" msgstr "" -#: build/locale/src/components/Filter/FilterContent.json -#. [src.components.Filter.2851720415] - button -#. defaultMessage is: -#. Add filter -msgctxt "button" -msgid "Add filter" -msgstr "" - #: build/locale/src/siteSettings/components/SiteSettingsKeys/SiteSettingsKeys.json #. [src.siteSettings.components.SiteSettingsKeys.1114030884] - button #. defaultMessage is: @@ -551,6 +583,14 @@ msgctxt "section header" msgid "All Photos" msgstr "" +#: build/locale/src/plugins/components/PluginsListPage/PluginsListPage.json +#. [src.plugins.components.PluginsListPage.3523826683] - tab name +#. defaultMessage is: +#. All Plugins +msgctxt "tab name" +msgid "All Plugins" +msgstr "" + #: build/locale/src/productTypes/components/ProductTypeListPage/ProductTypeListPage.json #. [src.productTypes.components.ProductTypeListPage.1776073799] - tab name #. defaultMessage is: @@ -559,8 +599,8 @@ msgctxt "tab name" msgid "All Product Types" msgstr "" -#: build/locale/src/products/components/ProductListFilter/ProductListFilter.json -#. [src.products.components.ProductListFilter.821159718] - tab name +#: build/locale/src/products/components/ProductListPage/ProductListPage.json +#. [src.products.components.ProductListPage.821159718] - tab name #. defaultMessage is: #. All Products msgctxt "tab name" @@ -867,7 +907,7 @@ msgctxt "description" msgid "Are you sure you want to delete {counter,plural,one{this customer} other{{displayQuantity} customers}}?" msgstr "" -#: build/locale/src/navigation/views/MenuList.json +#: build/locale/src/navigation/views/MenuList/MenuList.json #. [menuListDeleteMenusContent] #. defaultMessage is: #. Are you sure you want to delete {counter,plural,one{this menu} other{{displayQuantity} menus}}? @@ -883,8 +923,8 @@ msgctxt "dialog content" msgid "Are you sure you want to delete {counter,plural,one{this order draft} other{{displayQuantity} orderDrafts}}?" msgstr "" -#: build/locale/src/pages/views/PageList.json -#. [src.pages.views.808633099] - dialog content +#: build/locale/src/pages/views/PageList/PageList.json +#. [src.pages.views.PageList.808633099] - dialog content #. defaultMessage is: #. Are you sure you want to delete {counter,plural,one{this page} other{{displayQuantity} pages}}? msgctxt "dialog content" @@ -971,7 +1011,7 @@ msgctxt "delete customer, dialog content" msgid "Are you sure you want to delete {email}?" msgstr "" -#: build/locale/src/navigation/views/MenuList.json +#: build/locale/src/navigation/views/MenuList/MenuList.json #. [menuListDeleteMenuContent] #. defaultMessage is: #. Are you sure you want to delete {menuName}? @@ -1099,8 +1139,8 @@ msgctxt "description" msgid "Are you sure you want to publish {counter,plural,one{this collection} other{{displayQuantity} collections}}?" msgstr "" -#: build/locale/src/pages/views/PageList.json -#. [src.pages.views.64432778] - dialog content +#: build/locale/src/pages/views/PageList/PageList.json +#. [src.pages.views.PageList.64432778] - dialog content #. defaultMessage is: #. Are you sure you want to publish {counter,plural,one{this page} other{{displayQuantity} pages}}? msgctxt "dialog content" @@ -1191,8 +1231,8 @@ msgctxt "description" msgid "Are you sure you want to unpublish {counter,plural,one{this collection} other{{displayQuantity} collections}}?" msgstr "" -#: build/locale/src/pages/views/PageList.json -#. [src.pages.views.1265636351] - dialog content +#: build/locale/src/pages/views/PageList/PageList.json +#. [src.pages.views.PageList.1265636351] - dialog content #. defaultMessage is: #. Are you sure you want to unpublish {counter,plural,one{this page} other{{displayQuantity} pages}}? msgctxt "dialog content" @@ -1527,8 +1567,8 @@ msgctxt "description" msgid "Availability" msgstr "" -#: build/locale/src/products/components/ProductListFilter/ProductListFilter.json -#. [src.products.components.ProductListFilter.2157131639] - product status +#: build/locale/src/products/components/ProductListPage/filters.json +#. [src.products.components.ProductListPage.available] - product status #. defaultMessage is: #. Available msgctxt "product status" @@ -1543,14 +1583,6 @@ msgctxt "product variant status" msgid "Available" msgstr "" -#: build/locale/src/products/views/ProductList/filters.json -#. [src.products.views.ProductList.available] - filter products by stock -#. defaultMessage is: -#. Available -msgctxt "filter products by stock" -msgid "Available" -msgstr "" - #: build/locale/src/taxes/components/CountryTaxesPage/CountryTaxesPage.json #. [src.taxes.components.CountryTaxesPage.2201910191] - tax rate #. defaultMessage is: @@ -1627,6 +1659,14 @@ msgctxt "tax rate" msgid "Books" msgstr "" +#: build/locale/src/attributes/components/AttributeListPage/filters.json +#. [src.attributes.components.AttributeListPage.availableInGrid] - attribute can be column in product list table +#. defaultMessage is: +#. Can be used as column +msgctxt "attribute can be column in product list table" +msgid "Can be used as column" +msgstr "" + #: build/locale/src/intl.json #. [src.cancel] - button #. defaultMessage is: @@ -1819,10 +1859,6 @@ msgstr "" #. [src.categories.components.CategoryList.1214235329] #. defaultMessage is: #. Category Name -#: build/locale/src/collections/components/CollectionList/CollectionList.json -#. [src.collections.components.CollectionList.1214235329] -#. defaultMessage is: -#. Category Name #: build/locale/src/translations/components/TranslationsCategoriesPage/TranslationsCategoriesPage.json #. [src.translations.components.TranslationsCategoriesPage.1214235329] #. defaultMessage is: @@ -1879,6 +1915,14 @@ msgctxt "button" msgid "Change your password" msgstr "" +#: build/locale/src/webhooks/components/WebhookEvents/WebhookEvents.json +#. [src.webhooks.components.WebhookEvents.40035964] - event +#. defaultMessage is: +#. Changed quantity in checkout +msgctxt "event" +msgid "Changed quantity in checkout" +msgstr "" + #: build/locale/src/products/components/ProductPricing/ProductPricing.json #. [src.products.components.ProductPricing.3015886868] #. defaultMessage is: @@ -1975,6 +2019,10 @@ msgctxt "description" msgid "Collection" msgstr "" +#: build/locale/src/collections/components/CollectionList/CollectionList.json +#. [src.collections.components.CollectionList.2759199473] +#. defaultMessage is: +#. Collection Name #: build/locale/src/translations/components/TranslationsCollectionsPage/TranslationsCollectionsPage.json #. [src.translations.components.TranslationsCollectionsPage.2759199473] #. defaultMessage is: @@ -2075,6 +2123,10 @@ msgstr "" #. [src.productTypes.components.ProductTypeList.2754779425] - product type #. defaultMessage is: #. Configurable +#: build/locale/src/productTypes/components/ProductTypeListPage/filters.json +#. [src.productTypes.components.ProductTypeListPage.configurable] - product type +#. defaultMessage is: +#. Configurable msgctxt "product type" msgid "Configurable" msgstr "" @@ -2743,6 +2795,14 @@ msgctxt "button" msgid "Create webhook" msgstr "" +#: build/locale/src/orders/components/OrderDraftListPage/filters.json +#. [src.orders.components.OrderDraftListPage.created] - draft order +#. defaultMessage is: +#. Created +msgctxt "draft order" +msgid "Created" +msgstr "" + #: build/locale/src/collections/views/CollectionCreate.json #. [src.collections.views.1597339737] #. defaultMessage is: @@ -2751,7 +2811,7 @@ msgctxt "description" msgid "Created collection" msgstr "" -#: build/locale/src/navigation/views/MenuList.json +#: build/locale/src/navigation/views/MenuList/MenuList.json #. [menuListCreatedMenu] #. defaultMessage is: #. Created menu @@ -2807,6 +2867,14 @@ msgctxt "description" msgid "Customer" msgstr "" +#: build/locale/src/orders/components/OrderDraftListPage/filters.json +#. [src.orders.components.OrderDraftListPage.customer] - draft order +#. defaultMessage is: +#. Customer +msgctxt "draft order" +msgid "Customer" +msgstr "" + #: build/locale/src/orders/components/OrderList/OrderList.json #. [src.orders.components.OrderList.3426593715] - e-mail or full name #. defaultMessage is: @@ -2927,36 +2995,12 @@ msgctxt "date when order was placed" msgid "Date" msgstr "" -#: build/locale/src/orders/components/OrderListFilter/OrderListFilter.json -#. [src.orders.components.OrderListFilter.4205493358] +#: build/locale/src/staff/components/StaffListPage/filters.json +#. [src.staff.components.StaffListPage.deactivated] - staff member's account #. defaultMessage is: -#. Date -msgctxt "description" -msgid "Date" -msgstr "" - -#: build/locale/src/orders/views/OrderList/filters.json -#. [src.orders.views.OrderList.dateFrom] - filter by date -#. defaultMessage is: -#. Date from {date} -msgctxt "filter by date" -msgid "Date from {date}" -msgstr "" - -#: build/locale/src/orders/views/OrderList/filters.json -#. [src.orders.views.OrderList.dateIs] - filter by date -#. defaultMessage is: -#. Date is {date} -msgctxt "filter by date" -msgid "Date is {date}" -msgstr "" - -#: build/locale/src/orders/views/OrderList/filters.json -#. [src.orders.views.OrderList.dateTo] - filter by date -#. defaultMessage is: -#. Date to {date} -msgctxt "filter by date" -msgid "Date to {date}" +#. Deactivated +msgctxt "staff member's account" +msgid "Deactivated" msgstr "" #: build/locale/src/customers/components/CustomerAddress/CustomerAddress.json @@ -3083,7 +3127,7 @@ msgstr "" #. [menuDetailsDeleteMenuHeader] - dialog header #. defaultMessage is: #. Delete Menu -#: build/locale/src/navigation/views/MenuList.json +#: build/locale/src/navigation/views/MenuList/MenuList.json #. [menuListDeleteMenuHeader] - dialog header #. defaultMessage is: #. Delete Menu @@ -3091,7 +3135,7 @@ msgctxt "dialog header" msgid "Delete Menu" msgstr "" -#: build/locale/src/navigation/views/MenuList.json +#: build/locale/src/navigation/views/MenuList/MenuList.json #. [menuListDeleteMenusHeader] - dialog header #. defaultMessage is: #. Delete Menus @@ -3115,8 +3159,8 @@ msgctxt "dialog header" msgid "Delete Page" msgstr "" -#: build/locale/src/pages/views/PageList.json -#. [src.pages.views.2782958373] - dialog header +#: build/locale/src/pages/views/PageList/PageList.json +#. [src.pages.views.PageList.2782958373] - dialog header #. defaultMessage is: #. Delete Pages msgctxt "dialog header" @@ -3187,12 +3231,12 @@ msgctxt "custom search delete, dialog header" msgid "Delete Search" msgstr "" -#: build/locale/src/components/Filter/FilterSearch.json -#. [src.components.Filter.2173195312] - button +#: build/locale/src/components/FilterBar/FilterBar.json +#. [src.components.FilterBar.2173195312] - button #. defaultMessage is: #. Delete Search -#: build/locale/src/components/TableFilter/FilterChips.json -#. [src.components.TableFilter.2173195312] - button +#: build/locale/src/components/SearchBar/SearchBar.json +#. [src.components.SearchBar.2173195312] - button #. defaultMessage is: #. Delete Search msgctxt "button" @@ -3403,7 +3447,7 @@ msgctxt "description" msgid "Deleted draft orders" msgstr "" -#: build/locale/src/navigation/views/MenuList.json +#: build/locale/src/navigation/views/MenuList/MenuList.json #. [menuListDeletedMenu] #. defaultMessage is: #. Deleted menu @@ -3455,6 +3499,14 @@ msgctxt "product type" msgid "Digital" msgstr "" +#: build/locale/src/productTypes/components/ProductTypeListPage/filters.json +#. [src.productTypes.components.ProductTypeListPage.digital] - product +#. defaultMessage is: +#. Digital +msgctxt "product" +msgid "Digital" +msgstr "" + #: build/locale/src/translations/components/TranslationFields/TranslationFieldsSave.json #. [src.translations.components.TranslationFields.363646127] - button #. defaultMessage is: @@ -3479,6 +3531,18 @@ msgctxt "description" msgid "Discount Code" msgstr "" +#: build/locale/src/discounts/components/SaleListPage/filters.json +#. [src.discounts.components.SaleListPage.type] +#. defaultMessage is: +#. Discount Type +#: build/locale/src/discounts/components/VoucherListPage/filters.json +#. [src.discounts.components.VoucherListPage.type] +#. defaultMessage is: +#. Discount Type +msgctxt "description" +msgid "Discount Type" +msgstr "" + #: build/locale/src/discounts/components/SaleType/SaleType.json #. [src.discounts.components.SaleType.3216816841] - percentage or fixed, header #. defaultMessage is: @@ -3871,6 +3935,22 @@ msgctxt "webhook events" msgid "Expand or restrict webhooks permissions to register certain events in Saleor system." msgstr "" +#: build/locale/src/discounts/components/SaleListPage/filters.json +#. [src.discounts.components.SaleListPage.expired] - sale status +#. defaultMessage is: +#. Expired +msgctxt "sale status" +msgid "Expired" +msgstr "" + +#: build/locale/src/discounts/components/VoucherListPage/filters.json +#. [src.discounts.components.VoucherListPage.expired] - voucher status +#. defaultMessage is: +#. Expired +msgctxt "voucher status" +msgid "Expired" +msgstr "" + #: build/locale/src/collections/components/CollectionDetailsPage/CollectionDetailsPage.json #. [src.collections.components.CollectionDetailsPage.2906897537] - switch button #. defaultMessage is: @@ -3895,6 +3975,34 @@ msgctxt "search box label" msgid "Filter Countries" msgstr "" +#: build/locale/src/attributes/components/AttributeListPage/filters.json +#. [src.attributes.components.AttributeListPage.filterableInDashboard] - use attribute in filtering +#. defaultMessage is: +#. Filterable in Dashboard +msgctxt "use attribute in filtering" +msgid "Filterable in Dashboard" +msgstr "" + +#: build/locale/src/attributes/components/AttributeListPage/filters.json +#. [src.attributes.components.AttributeListPage.filterableInStorefront] - use attribute in filtering +#. defaultMessage is: +#. Filterable in Storefront +msgctxt "use attribute in filtering" +msgid "Filterable in Storefront" +msgstr "" + +#: build/locale/src/components/Filter/Filter.json +#. [src.components.Filter.996289613] - button +#. defaultMessage is: +#. Filters +msgctxt "button" +msgid "Filters" +msgstr "" + +#: build/locale/src/components/Filter/FilterContent.json +#. [src.components.Filter.996289613] +#. defaultMessage is: +#. Filters #: build/locale/src/components/FilterCard/FilterCard.json #. [src.components.FilterCard.996289613] #. defaultMessage is: @@ -3955,6 +4063,18 @@ msgctxt "voucher discount type" msgid "Fixed Amount" msgstr "" +#: build/locale/src/discounts/components/SaleListPage/filters.json +#. [src.discounts.components.SaleListPage.fixed] - discount type +#. defaultMessage is: +#. Fixed amount +#: build/locale/src/discounts/components/VoucherListPage/filters.json +#. [src.discounts.components.VoucherListPage.fixed] - discount type +#. defaultMessage is: +#. Fixed amount +msgctxt "discount type" +msgid "Fixed amount" +msgstr "" + #: build/locale/src/taxes/components/CountryTaxesPage/CountryTaxesPage.json #. [src.taxes.components.CountryTaxesPage.2332795012] - tax rate #. defaultMessage is: @@ -3999,22 +4119,10 @@ msgstr "" #. [src.fulfilled] - order status #. defaultMessage is: #. Fulfilled -#: build/locale/src/orders/views/OrderList/filters.json -#. [src.orders.views.OrderList.fulfilled] - order status -#. defaultMessage is: -#. Fulfilled msgctxt "order status" msgid "Fulfilled" msgstr "" -#: build/locale/src/orders/components/OrderListFilter/OrderListFilter.json -#. [src.orders.components.OrderListFilter.1712863026] - order fulfillment status -#. defaultMessage is: -#. Fulfilled -msgctxt "order fulfillment status" -msgid "Fulfilled" -msgstr "" - #: build/locale/src/orders/components/OrderFulfillment/OrderFulfillment.json #. [src.orders.components.OrderFulfillment.3494686506] - section header #. defaultMessage is: @@ -4151,6 +4259,14 @@ msgctxt "subheader" msgid "Here is some information we gathered about your store" msgstr "" +#: build/locale/src/collections/components/CollectionListPage/filters.json +#. [src.collections.components.CollectionListPage.hidden] - collection +#. defaultMessage is: +#. Hidden +msgctxt "collection" +msgid "Hidden" +msgstr "" + #: build/locale/src/components/VisibilityCard/VisibilityCard.json #. [src.components.VisibilityCard.77815154] #. defaultMessage is: @@ -4159,22 +4275,14 @@ msgctxt "description" msgid "Hidden" msgstr "" -#: build/locale/src/products/components/ProductListFilter/ProductListFilter.json -#. [src.products.components.ProductListFilter.77815154] - product is hidden +#: build/locale/src/products/components/ProductListPage/filters.json +#. [src.products.components.ProductListPage.hidden] - product is hidden #. defaultMessage is: #. Hidden msgctxt "product is hidden" msgid "Hidden" msgstr "" -#: build/locale/src/products/views/ProductList/filters.json -#. [src.products.views.ProductList.hidden] - filter products by visibility -#. defaultMessage is: -#. Hidden -msgctxt "filter products by visibility" -msgid "Hidden" -msgstr "" - #: build/locale/src/intl.json #. [src.home] - home section name #. defaultMessage is: @@ -4283,6 +4391,22 @@ msgctxt "section header" msgid "Images" msgstr "" +#: build/locale/src/plugins/components/PluginsListPage/filters.json +#. [src.plugins.components.PluginsListPage.deactivated] - plugin +#. defaultMessage is: +#. Inactive +msgctxt "plugin" +msgid "Inactive" +msgstr "" + +#: build/locale/src/services/components/ServiceListPage/filters.json +#. [src.services.components.ServiceListPage.deactivated] - service account +#. defaultMessage is: +#. Inactive +msgctxt "service account" +msgid "Inactive" +msgstr "" + #: build/locale/src/staff/components/StaffList/StaffList.json #. [src.staff.components.StaffList.1004218338] - staff member status #. defaultMessage is: @@ -4291,6 +4415,14 @@ msgctxt "staff member status" msgid "Inactive" msgstr "" +#: build/locale/src/webhooks/components/WebhooksListPage/filters.json +#. [src.webhooks.components.WebhooksListPage.inactive] - webhook +#. defaultMessage is: +#. Inactive +msgctxt "webhook" +msgid "Inactive" +msgstr "" + #: build/locale/src/products/components/ProductStock/ProductStock.json #. [productStockHeader] - product stock, section header #. defaultMessage is: @@ -4363,6 +4495,14 @@ msgctxt "description" msgid "Items successfully fulfilled" msgstr "" +#: build/locale/src/customers/components/CustomerListPage/filters.json +#. [src.customers.components.CustomerListPage.joinDate] - customer +#. defaultMessage is: +#. Join Date +msgctxt "customer" +msgid "Join Date" +msgstr "" + #: build/locale/src/services/components/ServiceTokens/ServiceTokens.json #. [src.services.components.ServiceTokens.2446088470] - service account key #. defaultMessage is: @@ -4403,22 +4543,6 @@ msgctxt "description" msgid "Languages" msgstr "" -#: build/locale/src/orders/components/OrderListFilter/OrderListFilter.json -#. [src.orders.components.OrderListFilter.714411029] -#. defaultMessage is: -#. Last 30 Days -msgctxt "description" -msgid "Last 30 Days" -msgstr "" - -#: build/locale/src/orders/components/OrderListFilter/OrderListFilter.json -#. [src.orders.components.OrderListFilter.3053702458] -#. defaultMessage is: -#. Last 7 Days -msgctxt "description" -msgid "Last 7 Days" -msgstr "" - #: build/locale/src/intl.json #. [src.lastName] #. defaultMessage is: @@ -4427,14 +4551,6 @@ msgctxt "description" msgid "Last Name" msgstr "" -#: build/locale/src/orders/components/OrderListFilter/OrderListFilter.json -#. [src.orders.components.OrderListFilter.2292505663] -#. defaultMessage is: -#. Last Year -msgctxt "description" -msgid "Last Year" -msgstr "" - #: build/locale/src/customers/components/CustomerStats/CustomerStats.json #. [src.customers.components.CustomerStats.1135318032] #. defaultMessage is: @@ -4743,6 +4859,14 @@ msgctxt "description" msgid "Miscellaneous" msgstr "" +#: build/locale/src/customers/components/CustomerListPage/filters.json +#. [src.customers.components.CustomerListPage.moneySpent] - customer +#. defaultMessage is: +#. Money Spent +msgctxt "customer" +msgid "Money Spent" +msgstr "" + #: build/locale/src/attributes/components/AttributeDetails/AttributeDetails.json #. [src.attributes.components.AttributeDetails.3334509011] - product attribute type #. defaultMessage is: @@ -5231,6 +5355,14 @@ msgctxt "description" msgid "No results" msgstr "" +#: build/locale/src/components/Filter/FilterAutocompleteField.json +#. [src.components.Filter.2332404293] - search +#. defaultMessage is: +#. No results +msgctxt "search" +msgid "No results" +msgstr "" + #: build/locale/src/components/MultiAutocompleteSelectField/MultiAutocompleteSelectFieldContent.json #. [src.components.MultiAutocompleteSelectField.4205644805] #. defaultMessage is: @@ -5559,6 +5691,14 @@ msgctxt "notes about customer, header" msgid "Notes" msgstr "" +#: build/locale/src/customers/components/CustomerListPage/filters.json +#. [src.customers.components.CustomerListPage.numberOfOrders] +#. defaultMessage is: +#. Number of Orders +msgctxt "description" +msgid "Number of Orders" +msgstr "" + #: build/locale/src/discounts/components/VoucherValue/VoucherValue.json #. [src.discounts.components.VoucherValue.1492866942] - voucher application, switch button #. defaultMessage is: @@ -5651,14 +5791,6 @@ msgctxt "navigator placeholder" msgid "Order Number" msgstr "" -#: build/locale/src/orders/components/OrderListFilter/OrderListFilter.json -#. [src.orders.components.OrderListFilter.2222765704] -#. defaultMessage is: -#. Order Status -msgctxt "description" -msgid "Order Status" -msgstr "" - #: build/locale/src/orders/components/OrderHistory/OrderHistory.json #. [src.orders.components.OrderHistory.1230178536] - order history message #. defaultMessage is: @@ -5863,22 +5995,14 @@ 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 +#: build/locale/src/products/components/ProductListPage/filters.json +#. [src.products.components.ProductListPage.outOfStock] - product status #. defaultMessage is: #. Out Of Stock msgctxt "product status" msgid "Out Of Stock" msgstr "" -#: build/locale/src/products/views/ProductList/filters.json -#. [src.products.views.ProductList.outOfStock] - filter products by stock -#. defaultMessage is: -#. Out of stock -msgctxt "filter products by stock" -msgid "Out of stock" -msgstr "" - #: build/locale/src/orders/components/OrderPayment/OrderPayment.json #. [src.orders.components.OrderPayment.353147224] - order payment #. defaultMessage is: @@ -5919,22 +6043,6 @@ msgctxt "description" msgid "Pages" msgstr "" -#: build/locale/src/orders/components/OrderListFilter/OrderListFilter.json -#. [src.orders.components.OrderListFilter.210276526] - order fulfillment status -#. defaultMessage is: -#. Partially Fulfilled -msgctxt "order fulfillment status" -msgid "Partially Fulfilled" -msgstr "" - -#: build/locale/src/orders/views/OrderList/filters.json -#. [src.orders.views.OrderList.partiallyFulfilled] - order status -#. defaultMessage is: -#. Partially Fulfilled -msgctxt "order status" -msgid "Partially Fulfilled" -msgstr "" - #: build/locale/src/misc.json #. [src.partiallyFulfilled] - order status #. defaultMessage is: @@ -6075,10 +6183,18 @@ msgctxt "order history message" msgid "Payment was voided" msgstr "" +#: build/locale/src/discounts/components/SaleListPage/filters.json +#. [src.discounts.components.SaleListPage.percentage] - discount type +#. defaultMessage is: +#. Percentage #: build/locale/src/discounts/components/SaleType/SaleType.json #. [src.discounts.components.SaleType.3688224049] - discount type #. defaultMessage is: #. Percentage +#: build/locale/src/discounts/components/VoucherListPage/filters.json +#. [src.discounts.components.VoucherListPage.percentage] - discount type +#. defaultMessage is: +#. Percentage msgctxt "discount type" msgid "Percentage" msgstr "" @@ -6159,6 +6275,14 @@ msgctxt "product type" msgid "Physical" msgstr "" +#: build/locale/src/orders/components/OrderListPage/filters.json +#. [src.orders.components.OrderListPage.placed] - order +#. defaultMessage is: +#. Placed +msgctxt "order" +msgid "Placed" +msgstr "" + #: build/locale/src/staff/components/StaffPreferences/StaffPreferences.json #. [src.staff.components.StaffPreferences.2162129531] #. defaultMessage is: @@ -6303,8 +6427,8 @@ msgctxt "product unit price" msgid "Price" msgstr "" -#: build/locale/src/products/components/ProductListFilter/ProductListFilter.json -#. [src.products.components.ProductListFilter.1134347598] +#: build/locale/src/products/components/ProductListPage/filters.json +#. [src.products.components.ProductListPage.price] #. defaultMessage is: #. Price #: build/locale/src/products/components/ProductVariantCreateDialog/ProductVariantCreatePrices.json @@ -6359,30 +6483,6 @@ msgctxt "price based shipping methods, section header" msgid "Price Based Rates" msgstr "" -#: build/locale/src/products/views/ProductList/filters.json -#. [src.products.views.ProductList.priceFrom] - filter by price -#. defaultMessage is: -#. Price from {price} -msgctxt "filter by price" -msgid "Price from {price}" -msgstr "" - -#: build/locale/src/products/views/ProductList/filters.json -#. [src.products.views.ProductList.priceIs] - filter by price -#. defaultMessage is: -#. Price is {price} -msgctxt "filter by price" -msgid "Price is {price}" -msgstr "" - -#: build/locale/src/products/views/ProductList/filters.json -#. [src.products.views.ProductList.priceTo] - filter by price -#. defaultMessage is: -#. Price to {price} -msgctxt "filter by price" -msgid "Price to {price}" -msgstr "" - #: build/locale/src/products/components/ProductVariantCreateDialog/ProductVariantCreateTabs.json #. [src.products.components.ProductVariantCreateDialog.705096461] - variant creation step #. defaultMessage is: @@ -6675,8 +6775,8 @@ msgctxt "publish collections" msgid "Publish" msgstr "" -#: build/locale/src/pages/views/PageList.json -#. [src.pages.views.1547167026] - publish page, button +#: build/locale/src/pages/views/PageList/PageList.json +#. [src.pages.views.PageList.1547167026] - publish page, button #. defaultMessage is: #. Publish msgctxt "publish page, button" @@ -6691,8 +6791,8 @@ msgctxt "publish product, button" msgid "Publish" msgstr "" -#: build/locale/src/pages/views/PageList.json -#. [src.pages.views.2321087286] - dialog header +#: build/locale/src/pages/views/PageList/PageList.json +#. [src.pages.views.PageList.2321087286] - dialog header #. defaultMessage is: #. Publish Pages msgctxt "dialog header" @@ -6755,6 +6855,18 @@ msgctxt "collection is published" msgid "Published" msgstr "" +#: build/locale/src/collections/components/CollectionListPage/filters.json +#. [src.collections.components.CollectionListPage.published] - collection +#. defaultMessage is: +#. Published +#: 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/collections/components/CollectionProducts/CollectionProducts.json #. [src.collections.components.CollectionProducts.3640454975] - product is published #. defaultMessage is: @@ -6767,14 +6879,6 @@ 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: @@ -6783,16 +6887,8 @@ msgctxt "page status" msgid "Published" msgstr "" -#: build/locale/src/products/views/ProductList/filters.json -#. [src.products.views.ProductList.published] - filter products by visibility -#. defaultMessage is: -#. Published -msgctxt "filter products by visibility" -msgid "Published" -msgstr "" - -#: build/locale/src/pages/views/PageList.json -#. [src.pages.views.2543350562] - notification +#: build/locale/src/pages/views/PageList/PageList.json +#. [src.pages.views.PageList.2543350562] - notification #. defaultMessage is: #. Published pages msgctxt "notification" @@ -6847,18 +6943,6 @@ msgctxt "description" msgid "Quick Pick" msgstr "" -#: build/locale/src/orders/components/OrderListFilter/OrderListFilter.json -#. [src.orders.components.OrderListFilter.2545228781] -#. defaultMessage is: -#. Range -#: build/locale/src/products/components/ProductListFilter/ProductListFilter.json -#. [src.products.components.ProductListFilter.2545228781] -#. defaultMessage is: -#. Range -msgctxt "description" -msgid "Range" -msgstr "" - #: build/locale/src/shipping/components/ShippingZoneRateDialog/ShippingZoneRateDialog.json #. [src.shipping.components.ShippingZoneRateDialog.3213611593] - shipping method #. defaultMessage is: @@ -6883,18 +6967,6 @@ msgctxt "shipping method price" msgid "Rate Price" msgstr "" -#: build/locale/src/orders/components/OrderListFilter/OrderListFilter.json -#. [src.orders.components.OrderListFilter.2415661583] - order status -#. defaultMessage is: -#. Ready to Capture -#: build/locale/src/orders/views/OrderList/filters.json -#. [src.orders.views.OrderList.readyToCapture] - order status -#. defaultMessage is: -#. Ready to Capture -msgctxt "order status" -msgid "Ready to Capture" -msgstr "" - #: build/locale/src/customers/components/CustomerOrders/CustomerOrders.json #. [src.customers.components.CustomerOrders.1899831623] - section header #. defaultMessage is: @@ -6971,8 +7043,8 @@ msgctxt "description" msgid "Removed page" msgstr "" -#: build/locale/src/pages/views/PageList.json -#. [src.pages.views.1080715663] - notification +#: build/locale/src/pages/views/PageList/PageList.json +#. [src.pages.views.PageList.1080715663] - notification #. defaultMessage is: #. Removed pages msgctxt "notification" @@ -7139,18 +7211,6 @@ msgctxt "button" msgid "Save" msgstr "" -#: build/locale/src/components/Filter/FilterSearch.json -#. [src.components.Filter.1514415736] - button -#. defaultMessage is: -#. Save Custom Search -#: build/locale/src/components/TableFilter/FilterChips.json -#. [src.components.TableFilter.1514415736] - button -#. defaultMessage is: -#. Save Custom Search -msgctxt "button" -msgid "Save Custom Search" -msgstr "" - #: build/locale/src/components/SaveFilterTabDialog/SaveFilterTabDialog.json #. [src.components.SaveFilterTabDialog.1514415736] - save filter tab, header #. defaultMessage is: @@ -7159,6 +7219,18 @@ msgctxt "save filter tab, header" msgid "Save Custom Search" msgstr "" +#: build/locale/src/components/FilterBar/FilterBar.json +#. [src.components.FilterBar.3268463180] - button +#. defaultMessage is: +#. Save Search +#: build/locale/src/components/SearchBar/SearchBar.json +#. [src.components.SearchBar.3268463180] - button +#. defaultMessage is: +#. Save Search +msgctxt "button" +msgid "Save Search" +msgstr "" + #: build/locale/src/products/components/ProductVariantCreatePage/ProductVariantCreatePage.json #. [src.products.components.ProductVariantCreatePage.2853608829] - button #. defaultMessage is: @@ -7175,6 +7247,22 @@ msgctxt "description" msgid "Saved changes" msgstr "" +#: build/locale/src/discounts/components/SaleListPage/filters.json +#. [src.discounts.components.SaleListPage.scheduled] - sale status +#. defaultMessage is: +#. Scheduled +msgctxt "sale status" +msgid "Scheduled" +msgstr "" + +#: build/locale/src/discounts/components/VoucherListPage/filters.json +#. [src.discounts.components.VoucherListPage.scheduled] - voucher status +#. defaultMessage is: +#. Scheduled +msgctxt "voucher status" +msgid "Scheduled" +msgstr "" + #: build/locale/src/attributes/components/AttributeListPage/AttributeListPage.json #. [src.attributes.components.AttributeListPage.3916653510] #. defaultMessage is: @@ -7379,6 +7467,14 @@ msgctxt "description" msgid "Search Page" msgstr "" +#: build/locale/src/plugins/components/PluginsListPage/PluginsListPage.json +#. [src.plugins.components.PluginsListPage.3233248823] +#. defaultMessage is: +#. Search Plugins... +msgctxt "description" +msgid "Search Plugins..." +msgstr "" + #: build/locale/src/translations/components/TranslationsEntitiesListPage/TranslationsEntitiesListPage.json #. [src.translations.components.TranslationsEntitiesListPage.2105464697] #. defaultMessage is: @@ -7411,8 +7507,8 @@ msgctxt "description" msgid "Search Products" msgstr "" -#: build/locale/src/products/components/ProductListFilter/ProductListFilter.json -#. [src.products.components.ProductListFilter.3550330425] +#: build/locale/src/products/components/ProductListPage/ProductListPage.json +#. [src.products.components.ProductListPage.3550330425] #. defaultMessage is: #. Search Products... msgctxt "description" @@ -7591,18 +7687,6 @@ msgctxt "webhook" msgid "Secrect Key" msgstr "" -#: build/locale/src/components/Filter/FilterContent.json -#. [src.components.Filter.2230339185] -#. defaultMessage is: -#. Select Filter... -#: build/locale/src/components/Filter/FilterElement.json -#. [src.components.Filter.2230339185] -#. defaultMessage is: -#. Select Filter... -msgctxt "description" -msgid "Select Filter..." -msgstr "" - #: build/locale/src/products/components/ProductVariantCreateDialog/ProductVariantCreateTabs.json #. [src.products.components.ProductVariantCreateDialog.2478977538] - attribute values, variant creation step #. defaultMessage is: @@ -7619,22 +7703,6 @@ msgctxt "description" msgid "Select a specific variant image from product images" msgstr "" -#: build/locale/src/orders/components/OrderListPage/OrderListPage.json -#. [src.orders.components.OrderListPage.3524904717] -#. defaultMessage is: -#. Select all orders where: -msgctxt "description" -msgid "Select all orders where:" -msgstr "" - -#: build/locale/src/products/components/ProductListFilter/ProductListFilter.json -#. [src.products.components.ProductListFilter.1421689426] -#. defaultMessage is: -#. Select all products where: -msgctxt "description" -msgid "Select all products where:" -msgstr "" - #: build/locale/src/components/TableHead/TableHead.json #. [src.components.TableHead.868570480] #. defaultMessage is: @@ -7783,6 +7851,14 @@ msgctxt "voucher discount" msgid "Shipment" msgstr "" +#: build/locale/src/productTypes/components/ProductTypeListPage/filters.json +#. [src.productTypes.components.ProductTypeListPage.shippable] - product +#. defaultMessage is: +#. Shippable +msgctxt "product" +msgid "Shippable" +msgstr "" + #: build/locale/src/orders/components/OrderPayment/OrderPayment.json #. [src.orders.components.OrderPayment.1325966144] - order shipping method name #. defaultMessage is: @@ -7911,6 +7987,14 @@ msgctxt "description" msgid "Show gross prices to customers in the storefront" msgstr "" +#: build/locale/src/components/Filter/FilterAutocompleteField.json +#. [src.components.Filter.4190318230] - search results +#. defaultMessage is: +#. Show more +msgctxt "search results" +msgid "Show more" +msgstr "" + #: build/locale/src/products/components/ProductOrganization/ProductOrganization.json #. [src.products.components.ProductOrganization.150865454] - product is not configurable #. defaultMessage is: @@ -7995,22 +8079,6 @@ msgctxt "description" msgid "Sorry, your username and/or password are incorrect. Please try again." msgstr "" -#: build/locale/src/orders/components/OrderListFilter/OrderListFilter.json -#. [src.orders.components.OrderListFilter.789263812] -#. defaultMessage is: -#. Specific Date -msgctxt "description" -msgid "Specific Date" -msgstr "" - -#: build/locale/src/products/components/ProductListFilter/ProductListFilter.json -#. [src.products.components.ProductListFilter.2844426531] -#. defaultMessage is: -#. Specific Price -msgctxt "description" -msgid "Specific Price" -msgstr "" - #: build/locale/src/discounts/translations.json #. [src.discounts.products] - voucher discount #. defaultMessage is: @@ -8075,6 +8143,22 @@ msgctxt "description" msgid "Start typing to begin search..." msgstr "" +#: build/locale/src/discounts/components/SaleListPage/filters.json +#. [src.discounts.components.SaleListPage.started] - sale start date +#. defaultMessage is: +#. Started +msgctxt "sale start date" +msgid "Started" +msgstr "" + +#: build/locale/src/discounts/components/VoucherListPage/filters.json +#. [src.discounts.components.VoucherListPage.started] - voucher start date +#. defaultMessage is: +#. Started +msgctxt "voucher start date" +msgid "Started" +msgstr "" + #: build/locale/src/discounts/components/SaleList/SaleList.json #. [src.discounts.components.SaleList.47059407] - sale start date #. defaultMessage is: @@ -8099,11 +8183,27 @@ msgctxt "order status" msgid "Status" msgstr "" -#: build/locale/src/orders/components/OrderListFilter/OrderListFilter.json -#. [src.orders.components.OrderListFilter.1756106276] - order fulfillment status +#: build/locale/src/discounts/components/SaleListPage/filters.json +#. [src.discounts.components.SaleListPage.status] - sale status #. defaultMessage is: #. Status -msgctxt "order fulfillment status" +msgctxt "sale status" +msgid "Status" +msgstr "" + +#: build/locale/src/discounts/components/VoucherListPage/filters.json +#. [src.discounts.components.VoucherListPage.status] - voucher status +#. defaultMessage is: +#. Status +msgctxt "voucher status" +msgid "Status" +msgstr "" + +#: build/locale/src/intl.json +#. [src.status] +#. defaultMessage is: +#. Status +msgctxt "description" msgid "Status" msgstr "" @@ -8115,14 +8215,6 @@ msgctxt "plugin status" msgid "Status" msgstr "" -#: build/locale/src/products/components/ProductListFilter/ProductListFilter.json -#. [src.products.components.ProductListFilter.1756106276] - product status -#. defaultMessage is: -#. Status -msgctxt "product status" -msgid "Status" -msgstr "" - #: build/locale/src/products/components/ProductVariants/ProductVariants.json #. [src.products.components.ProductVariants.1756106276] - product variant status #. defaultMessage is: @@ -8131,12 +8223,12 @@ msgctxt "product variant status" msgid "Status" msgstr "" -#: build/locale/src/products/components/ProductListFilter/ProductListFilter.json -#. [src.products.components.ProductListFilter.3841616483] - product stock +#: build/locale/src/staff/components/StaffListPage/filters.json +#. [src.staff.components.StaffListPage.status] - staff member's account #. defaultMessage is: -#. Stock -msgctxt "product stock" -msgid "Stock" +#. Status +msgctxt "staff member's account" +msgid "Status" msgstr "" #: build/locale/src/products/components/ProductVariantCreateDialog/ProductVariantCreatePrices.json @@ -8171,11 +8263,11 @@ msgctxt "product variant stock, section header" msgid "Stock" msgstr "" -#: build/locale/src/products/components/ProductListFilter/ProductListFilter.json -#. [src.products.components.ProductListFilter.3645081351] +#: build/locale/src/products/components/ProductListPage/filters.json +#. [src.products.components.ProductListPage.quantity] - product #. defaultMessage is: #. Stock quantity -msgctxt "description" +msgctxt "product" msgid "Stock quantity" msgstr "" @@ -8607,6 +8699,14 @@ msgctxt "email sender" msgid "This will be visible as \"from\" name" msgstr "" +#: build/locale/src/discounts/components/VoucherListPage/filters.json +#. [src.discounts.components.VoucherListPage.timesUsed] - voucher +#. defaultMessage is: +#. Times used +msgctxt "voucher" +msgid "Times used" +msgstr "" + #: build/locale/src/pages/components/PageInfo/PageInfo.json #. [src.pages.components.PageInfo.1124600214] - page title #. defaultMessage is: @@ -8859,6 +8959,14 @@ msgctxt "product type is either simple or configurable" msgid "Type" msgstr "" +#: build/locale/src/productTypes/components/ProductTypeListPage/filters.json +#. [src.productTypes.components.ProductTypeListPage.type] - product type is digital or physical +#. defaultMessage is: +#. Type +msgctxt "product type is digital or physical" +msgid "Type" +msgstr "" + #: build/locale/src/components/Navigator/NavigatorInput.json #. [src.components.Navigator.1167695965] - navigator placeholder #. defaultMessage is: @@ -9071,22 +9179,10 @@ msgstr "" #. [src.unfulfilled] - order status #. defaultMessage is: #. Unfulfilled -#: build/locale/src/orders/views/OrderList/filters.json -#. [src.orders.views.OrderList.unfulfilled] - order status -#. defaultMessage is: -#. Unfulfilled msgctxt "order status" msgid "Unfulfilled" msgstr "" -#: build/locale/src/orders/components/OrderListFilter/OrderListFilter.json -#. [src.orders.components.OrderListFilter.1751787272] - order fulfillment status -#. defaultMessage is: -#. Unfulfilled -msgctxt "order fulfillment status" -msgid "Unfulfilled" -msgstr "" - #: build/locale/src/orders/components/OrderUnfulfilledItems/OrderUnfulfilledItems.json #. [src.orders.components.OrderUnfulfilledItems.2886647373] - section header #. defaultMessage is: @@ -9111,8 +9207,8 @@ msgctxt "unpublish collections" msgid "Unpublish" msgstr "" -#: build/locale/src/pages/views/PageList.json -#. [src.pages.views.2237014112] - unpublish page, button +#: build/locale/src/pages/views/PageList/PageList.json +#. [src.pages.views.PageList.2237014112] - unpublish page, button #. defaultMessage is: #. Unpublish msgctxt "unpublish page, button" @@ -9127,8 +9223,8 @@ msgctxt "unpublish product, button" msgid "Unpublish" msgstr "" -#: build/locale/src/pages/views/PageList.json -#. [src.pages.views.158565417] - dialog header +#: build/locale/src/pages/views/PageList/PageList.json +#. [src.pages.views.PageList.158565417] - dialog header #. defaultMessage is: #. Unpublish Pages msgctxt "dialog header" @@ -9351,6 +9447,14 @@ msgctxt "check to require attribute to have value" msgid "Value Required" msgstr "" +#: build/locale/src/attributes/components/AttributeListPage/filters.json +#. [src.attributes.components.AttributeListPage.valueRequired] - attribute value is required +#. defaultMessage is: +#. Value Required +msgctxt "attribute value is required" +msgid "Value Required" +msgstr "" + #: build/locale/src/attributes/views/AttributeDetails/AttributeDetails.json #. [src.attributes.views.AttributeDetails.423042761] - attribute value deleted #. defaultMessage is: @@ -9407,6 +9511,14 @@ msgctxt "section header" msgid "Variant Attributes" msgstr "" +#: build/locale/src/attributes/components/AttributeListPage/filters.json +#. [src.attributes.components.AttributeListPage.isVariantOnly] - attribute can be used only in variants +#. defaultMessage is: +#. Variant Only +msgctxt "attribute can be used only in variants" +msgid "Variant Only" +msgstr "" + #: build/locale/src/products/views/ProductVariant.json #. [src.products.views.2279302139] #. defaultMessage is: @@ -9483,8 +9595,8 @@ msgctxt "page status" msgid "Visibility" msgstr "" -#: build/locale/src/products/components/ProductListFilter/ProductListFilter.json -#. [src.products.components.ProductListFilter.1459686496] - product visibility +#: build/locale/src/products/components/ProductListPage/filters.json +#. [src.products.components.ProductListPage.visibility] - product visibility #. defaultMessage is: #. Visibility msgctxt "product visibility" @@ -9507,14 +9619,18 @@ msgctxt "description" msgid "Visible" msgstr "" -#: build/locale/src/products/components/ProductListFilter/ProductListFilter.json -#. [src.products.components.ProductListFilter.643174786] - product is visible +#: build/locale/src/products/components/ProductListPage/filters.json +#. [src.products.components.ProductListPage.visible] - product is visible #. defaultMessage is: #. Visible msgctxt "product is visible" msgid "Visible" msgstr "" +#: build/locale/src/attributes/components/AttributeListPage/filters.json +#. [src.attributes.components.AttributeListPage.visibleInStorefront] - attribute +#. defaultMessage is: +#. Visible on Product Page in Storefront #: build/locale/src/attributes/components/AttributeProperties/AttributeProperties.json #. [src.attributes.components.AttributeProperties.3876764312] - attribute #. defaultMessage is: @@ -9751,6 +9867,22 @@ msgctxt "account status" msgid "active" msgstr "" +#: build/locale/src/components/Filter/FilterContent.json +#. [src.components.Filter.2779594451] - filter range separator +#. defaultMessage is: +#. and +msgctxt "filter range separator" +msgid "and" +msgstr "" + +#: build/locale/src/components/Filter/FilterContent.json +#. [src.components.Filter.773313536] - is filter range or value +#. defaultMessage is: +#. between +msgctxt "is filter range or value" +msgid "between" +msgstr "" + #: build/locale/src/productTypes/components/ProductTypeListPage/ProductTypeListPage.json #. [src.productTypes.components.ProductTypeListPage.3479705616] - button #. defaultMessage is: @@ -9783,28 +9915,12 @@ msgctxt "order does not require shipping" msgid "does not apply" msgstr "" -#: build/locale/src/orders/components/OrderListFilter/OrderListFilter.json -#. [src.orders.components.OrderListFilter.3477667254] +#: build/locale/src/components/Filter/FilterContent.json +#. [src.components.Filter.2683154806] - is filter range or value #. defaultMessage is: -#. equals -msgctxt "description" -msgid "equals" -msgstr "" - -#: build/locale/src/products/components/ProductListFilter/ProductListFilter.json -#. [src.products.components.ProductListFilter.3477667254] - product price -#. defaultMessage is: -#. equals -msgctxt "product price" -msgid "equals" -msgstr "" - -#: build/locale/src/components/Filter/FilterElement.json -#. [src.components.Filter.2755325844] -#. defaultMessage is: -#. from -msgctxt "description" -msgid "from" +#. equal to +msgctxt "is filter range or value" +msgid "equal to" msgstr "" #: build/locale/src/components/MoneyRange/MoneyRange.json @@ -9831,22 +9947,6 @@ msgctxt "account status" msgid "inactive" msgstr "" -#: build/locale/src/orders/components/OrderListFilter/OrderListFilter.json -#. [src.orders.components.OrderListFilter.1438173764] - date is set as -#. defaultMessage is: -#. is set as -msgctxt "date is set as" -msgid "is set as" -msgstr "" - -#: build/locale/src/products/components/ProductListFilter/ProductListFilter.json -#. [src.products.components.ProductListFilter.1438173764] - product status is set as -#. defaultMessage is: -#. is set as -msgctxt "product status is set as" -msgid "is set as" -msgstr "" - #: build/locale/src/webhooks/components/WebhookInfo/WebhookInfo.json #. [src.webhooks.components.WebhookInfo.3809115222] - webhook secret key help text #. defaultMessage is: @@ -9887,14 +9987,6 @@ msgctxt "product" msgid "since {date}" msgstr "" -#: build/locale/src/components/Filter/FilterElement.json -#. [src.components.Filter.152217691] -#. defaultMessage is: -#. to -msgctxt "description" -msgid "to" -msgstr "" - #: build/locale/src/components/MoneyRange/MoneyRange.json #. [src.components.MoneyRange.12301532] - money #. defaultMessage is: diff --git a/package.json b/package.json index 7666c5561..e6e32cdc6 100644 --- a/package.json +++ b/package.json @@ -164,6 +164,7 @@ "@assets(.*)$": "/assets/$1", "@locale(.*)$": "/locale/$1", "@saleor(.*)$": "/src/$1", + "@test/(.*)$": "/testUtils/$1", "^lodash-es(.*)$": "lodash/$1" } }, diff --git a/react-intl.d.ts b/react-intl.d.ts index efafa921d..152e90425 100644 --- a/react-intl.d.ts +++ b/react-intl.d.ts @@ -1,4 +1,5 @@ declare module "react-intl" { + import { OptionalIntlConfig } from "react-intl/dist/components/provider"; import * as ReactIntl from "node_modules/react-intl"; export * from "node_modules/react-intl"; @@ -51,4 +52,6 @@ declare module "react-intl" { > extends React.Component> {} export function useIntl(): IntlShape; + + export function createIntl(config: OptionalIntlConfig): IntlShape; } diff --git a/schema.graphql b/schema.graphql index 1c6b82287..55858921b 100644 --- a/schema.graphql +++ b/schema.graphql @@ -66,7 +66,7 @@ input AccountInput { type AccountRegister { errors: [Error!] - requiresConfirmation: Boolean! + requiresConfirmation: Boolean accountErrors: [AccountError!] user: User } diff --git a/src/attributes/components/AttributeListPage/AttributeListPage.tsx b/src/attributes/components/AttributeListPage/AttributeListPage.tsx index aadb1d933..70f8733c2 100644 --- a/src/attributes/components/AttributeListPage/AttributeListPage.tsx +++ b/src/attributes/components/AttributeListPage/AttributeListPage.tsx @@ -4,7 +4,7 @@ import React from "react"; import { FormattedMessage, useIntl } from "react-intl"; import AppHeader from "@saleor/components/AppHeader"; -import SearchBar from "@saleor/components/SearchBar"; +import FilterBar from "@saleor/components/FilterBar"; import { sectionNames } from "@saleor/intl"; import { AttributeListUrlSortField } from "@saleor/attributes/urls"; import Container from "../../../components/Container"; @@ -12,17 +12,22 @@ import PageHeader from "../../../components/PageHeader"; import { ListActions, PageListProps, - SearchPageProps, + FilterPageProps, TabPageProps, SortPage } from "../../../types"; import { AttributeList_attributes_edges_node } from "../../types/AttributeList"; import AttributeList from "../AttributeList/AttributeList"; +import { + createFilterStructure, + AttributeListFilterOpts, + AttributeFilterKeys +} from "./filters"; export interface AttributeListPageProps extends PageListProps, ListActions, - SearchPageProps, + FilterPageProps, SortPage, TabPageProps { attributes: AttributeList_attributes_edges_node[]; @@ -30,9 +35,12 @@ export interface AttributeListPageProps } const AttributeListPage: React.FC = ({ + currencySymbol, + filterOpts, + initialSearch, onAdd, onBack, - initialSearch, + onFilterChange, onSearchChange, currentTab, onAll, @@ -44,6 +52,8 @@ const AttributeListPage: React.FC = ({ }) => { const intl = useIntl(); + const structure = createFilterStructure(intl, filterOpts); + return ( @@ -58,18 +68,21 @@ const AttributeListPage: React.FC = ({ - ; + filterableInDashboard: FilterOpts; + filterableInStorefront: FilterOpts; + isVariantOnly: FilterOpts; + valueRequired: FilterOpts; + visibleInStorefront: FilterOpts; +} + +const messages = defineMessages({ + availableInGrid: { + defaultMessage: "Can be used as column", + description: "attribute can be column in product list table" + }, + filterableInDashboard: { + defaultMessage: "Filterable in Dashboard", + description: "use attribute in filtering" + }, + filterableInStorefront: { + defaultMessage: "Filterable in Storefront", + description: "use attribute in filtering" + }, + isVariantOnly: { + defaultMessage: "Variant Only", + description: "attribute can be used only in variants" + }, + valueRequired: { + defaultMessage: "Value Required", + description: "attribute value is required" + }, + visibleInStorefront: { + defaultMessage: "Visible on Product Page in Storefront", + description: "attribute" + } +}); + +export function createFilterStructure( + intl: IntlShape, + opts: AttributeListFilterOpts +): IFilter { + return [ + { + ...createBooleanField( + AttributeFilterKeys.availableInGrid, + intl.formatMessage(messages.availableInGrid), + opts.availableInGrid.value, + { + negative: intl.formatMessage(commonMessages.no), + positive: intl.formatMessage(commonMessages.yes) + } + ), + active: opts.availableInGrid.active + }, + { + ...createBooleanField( + AttributeFilterKeys.filterableInDashboard, + intl.formatMessage(messages.filterableInDashboard), + opts.filterableInDashboard.value, + { + negative: intl.formatMessage(commonMessages.no), + positive: intl.formatMessage(commonMessages.yes) + } + ), + active: opts.filterableInDashboard.active + }, + { + ...createBooleanField( + AttributeFilterKeys.filterableInStorefront, + intl.formatMessage(messages.filterableInStorefront), + opts.filterableInStorefront.value, + { + negative: intl.formatMessage(commonMessages.no), + positive: intl.formatMessage(commonMessages.yes) + } + ), + active: opts.filterableInStorefront.active + }, + { + ...createBooleanField( + AttributeFilterKeys.isVariantOnly, + intl.formatMessage(messages.isVariantOnly), + opts.isVariantOnly.value, + { + negative: intl.formatMessage(commonMessages.no), + positive: intl.formatMessage(commonMessages.yes) + } + ), + active: opts.isVariantOnly.active + }, + { + ...createBooleanField( + AttributeFilterKeys.valueRequired, + intl.formatMessage(messages.valueRequired), + opts.valueRequired.value, + { + negative: intl.formatMessage(commonMessages.no), + positive: intl.formatMessage(commonMessages.yes) + } + ), + active: opts.valueRequired.active + }, + { + ...createBooleanField( + AttributeFilterKeys.visibleInStorefront, + intl.formatMessage(messages.visibleInStorefront), + opts.visibleInStorefront.value, + { + negative: intl.formatMessage(commonMessages.no), + positive: intl.formatMessage(commonMessages.yes) + } + ), + active: opts.visibleInStorefront.active + } + ]; +} diff --git a/src/attributes/components/AttributeListPage/index.ts b/src/attributes/components/AttributeListPage/index.ts index 2633c7769..dea1bb374 100644 --- a/src/attributes/components/AttributeListPage/index.ts +++ b/src/attributes/components/AttributeListPage/index.ts @@ -1,2 +1,3 @@ export { default } from "./AttributeListPage"; export * from "./AttributeListPage"; +export * from "./filters"; diff --git a/src/attributes/urls.ts b/src/attributes/urls.ts index fc0312068..9e498a6d9 100644 --- a/src/attributes/urls.ts +++ b/src/attributes/urls.ts @@ -15,6 +15,12 @@ import { export const attributeSection = "/attributes/"; export enum AttributeListUrlFiltersEnum { + availableInGrid = "availableInGrid", + filterableInDashboard = "filterableInDashboard", + filterableInStorefront = "filterableInStorefront", + isVariantOnly = "isVariantOnly", + valueRequired = "valueRequired", + visibleInStorefront = "visibleInStorefront", query = "query" } export type AttributeListUrlFilters = Filters; diff --git a/src/attributes/views/AttributeList/AttributeList.tsx b/src/attributes/views/AttributeList/AttributeList.tsx index 81e49d3f0..63defa847 100644 --- a/src/attributes/views/AttributeList/AttributeList.tsx +++ b/src/attributes/views/AttributeList/AttributeList.tsx @@ -9,7 +9,8 @@ import { getActiveFilters, getFilterTabs, getFilterVariables, - saveFilterTab + saveFilterTab, + getFilterOpts } from "@saleor/attributes/views/AttributeList/filters"; import DeleteFilterTabDialog from "@saleor/components/DeleteFilterTabDialog"; import SaveFilterTabDialog, { @@ -24,6 +25,8 @@ import usePaginator, { import { getSortParams } from "@saleor/utils/sort"; import createSortHandler from "@saleor/utils/handlers/sortHandler"; import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers"; +import createFilterHandlers from "@saleor/utils/handlers/filterHandlers"; +import useShop from "@saleor/hooks/useShop"; import { PAGINATE_BY } from "../../../config"; import useBulkActions from "../../../hooks/useBulkActions"; import { maybe } from "../../../misc"; @@ -35,12 +38,12 @@ import { AttributeBulkDelete } from "../../types/AttributeBulkDelete"; import { attributeAddUrl, attributeListUrl, - AttributeListUrlFilters, AttributeListUrlQueryParams, attributeUrl, AttributeListUrlDialog } from "../../urls"; import { getSortQueryVariables } from "./sort"; +import { getFilterQueryParam } from "./filters"; interface AttributeListProps { params: AttributeListUrlQueryParams; @@ -50,6 +53,7 @@ const AttributeList: React.FC = ({ params }) => { const navigate = useNavigator(); const paginate = usePaginator(); const notify = useNotifier(); + const shop = useShop(); const { isSelected, listElements, reset, toggle, toggleAll } = useBulkActions( params.ids ); @@ -82,16 +86,17 @@ const AttributeList: React.FC = ({ params }) => { AttributeListUrlQueryParams >(navigate, attributeListUrl, params); - const changeFilterField = (filter: AttributeListUrlFilters) => { - reset(); - navigate( - attributeListUrl({ - ...getActiveFilters(params), - ...filter, - activeTab: undefined - }) - ); - }; + const [ + changeFilters, + resetFilters, + handleSearchChange + ] = createFilterHandlers({ + cleanupFn: reset, + createUrl: attributeListUrl, + getFilterQueryParam, + navigate, + params + }); const handleTabChange = (tab: number) => { reset(); @@ -135,6 +140,7 @@ const AttributeList: React.FC = ({ params }) => { }; const handleSort = createSortHandler(navigate, attributeListUrl, params); + const currencySymbol = maybe(() => shop.defaultCurrency, "USD"); return ( @@ -144,17 +150,20 @@ const AttributeList: React.FC = ({ params }) => { attributes={maybe(() => data.attributes.edges.map(edge => edge.node) )} + currencySymbol={currencySymbol} currentTab={currentTab} disabled={loading || attributeBulkDeleteOpts.loading} + filterOpts={getFilterOpts(params)} initialSearch={params.query || ""} isChecked={isSelected} onAdd={() => navigate(attributeAddUrl())} - onAll={() => navigate(attributeListUrl())} + onAll={resetFilters} onBack={() => navigate(configurationMenuUrl)} + onFilterChange={changeFilters} onNextPage={loadNextPage} onPreviousPage={loadPreviousPage} onRowClick={id => () => navigate(attributeUrl(id))} - onSearchChange={query => changeFilterField({ query })} + onSearchChange={handleSearchChange} onSort={handleSort} onTabChange={handleTabChange} onTabDelete={() => openModal("delete-search")} diff --git a/src/attributes/views/AttributeList/__snapshots__/filters.test.ts.snap b/src/attributes/views/AttributeList/__snapshots__/filters.test.ts.snap new file mode 100644 index 000000000..425e23c5c --- /dev/null +++ b/src/attributes/views/AttributeList/__snapshots__/filters.test.ts.snap @@ -0,0 +1,14 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Filtering URL params should not be empty if active filters are present 1`] = ` +Object { + "availableInGrid": "true", + "filterableInDashboard": "true", + "filterableInStorefront": "true", + "isVariantOnly": "true", + "valueRequired": "true", + "visibleInStorefront": "true", +} +`; + +exports[`Filtering URL params should not be empty if active filters are present 2`] = `"availableInGrid=true&filterableInDashboard=true&filterableInStorefront=true&isVariantOnly=true&valueRequired=true&visibleInStorefront=true"`; diff --git a/src/attributes/views/AttributeList/filters.test.ts b/src/attributes/views/AttributeList/filters.test.ts new file mode 100644 index 000000000..5a50d1f1c --- /dev/null +++ b/src/attributes/views/AttributeList/filters.test.ts @@ -0,0 +1,77 @@ +import { createIntl } from "react-intl"; +import { stringify as stringifyQs } from "qs"; + +import { AttributeListUrlFilters } from "@saleor/attributes/urls"; +import { createFilterStructure } from "@saleor/attributes/components/AttributeListPage"; +import { getFilterQueryParams } from "@saleor/utils/filters"; +import { config } from "@test/intl"; +import { getExistingKeys, setFilterOptsStatus } from "@test/filters"; +import { getFilterVariables, getFilterQueryParam } from "./filters"; + +describe("Filtering query params", () => { + it("should be empty object if no params given", () => { + const params: AttributeListUrlFilters = {}; + const filterVariables = getFilterVariables(params); + + expect(getExistingKeys(filterVariables)).toHaveLength(0); + }); + + it("should not be empty object if params given", () => { + const params: AttributeListUrlFilters = { + availableInGrid: true.toString() + }; + const filterVariables = getFilterVariables(params); + + expect(getExistingKeys(filterVariables)).toHaveLength(1); + }); +}); + +describe("Filtering URL params", () => { + const intl = createIntl(config); + + const filters = createFilterStructure(intl, { + availableInGrid: { + active: false, + value: true + }, + filterableInDashboard: { + active: false, + value: true + }, + filterableInStorefront: { + active: false, + value: true + }, + isVariantOnly: { + active: false, + value: true + }, + valueRequired: { + active: false, + value: true + }, + visibleInStorefront: { + active: false, + value: true + } + }); + + it("should be empty if no active filters", () => { + const filterQueryParams = getFilterQueryParams( + filters, + getFilterQueryParam + ); + + expect(getExistingKeys(filterQueryParams)).toHaveLength(0); + }); + + it("should not be empty if active filters are present", () => { + const filterQueryParams = getFilterQueryParams( + setFilterOptsStatus(filters, true), + getFilterQueryParam + ); + + expect(filterQueryParams).toMatchSnapshot(); + expect(stringifyQs(filterQueryParams)).toMatchSnapshot(); + }); +}); diff --git a/src/attributes/views/AttributeList/filters.ts b/src/attributes/views/AttributeList/filters.ts index e6f2b9d1d..5c39a51ca 100644 --- a/src/attributes/views/AttributeList/filters.ts +++ b/src/attributes/views/AttributeList/filters.ts @@ -1,7 +1,14 @@ import { AttributeFilterInput } from "@saleor/types/globalTypes"; +import { maybe, parseBoolean } from "@saleor/misc"; +import { IFilterElement } from "@saleor/components/Filter"; +import { + AttributeListFilterOpts, + AttributeFilterKeys +} from "@saleor/attributes/components/AttributeListPage"; import { createFilterTabUtils, - createFilterUtils + createFilterUtils, + getSingleValueQueryParam } from "../../../utils/filters"; import { AttributeListUrlFilters, @@ -11,14 +18,113 @@ import { export const PRODUCT_FILTERS_KEY = "productFilters"; +export function getFilterOpts( + params: AttributeListUrlFilters +): AttributeListFilterOpts { + return { + availableInGrid: { + active: params.availableInGrid !== undefined, + value: maybe(() => parseBoolean(params.availableInGrid, true)) + }, + filterableInDashboard: { + active: params.filterableInDashboard !== undefined, + value: maybe(() => parseBoolean(params.filterableInDashboard, true)) + }, + filterableInStorefront: { + active: params.filterableInStorefront !== undefined, + value: maybe(() => parseBoolean(params.filterableInStorefront, true)) + }, + isVariantOnly: { + active: params.isVariantOnly !== undefined, + value: maybe(() => parseBoolean(params.isVariantOnly, true)) + }, + valueRequired: { + active: params.valueRequired !== undefined, + value: maybe(() => parseBoolean(params.valueRequired, true)) + }, + visibleInStorefront: { + active: params.visibleInStorefront !== undefined, + value: maybe(() => parseBoolean(params.visibleInStorefront, true)) + } + }; +} + export function getFilterVariables( params: AttributeListUrlFilters ): AttributeFilterInput { return { - search: params.query + availableInGrid: + params.availableInGrid !== undefined + ? parseBoolean(params.availableInGrid, false) + : undefined, + filterableInDashboard: + params.filterableInDashboard !== undefined + ? parseBoolean(params.filterableInDashboard, false) + : undefined, + filterableInStorefront: + params.filterableInStorefront !== undefined + ? parseBoolean(params.filterableInStorefront, false) + : undefined, + isVariantOnly: + params.isVariantOnly !== undefined + ? parseBoolean(params.isVariantOnly, false) + : undefined, + search: params.query, + valueRequired: + params.valueRequired !== undefined + ? parseBoolean(params.valueRequired, false) + : undefined, + visibleInStorefront: + params.visibleInStorefront !== undefined + ? parseBoolean(params.visibleInStorefront, false) + : undefined }; } +export function getFilterQueryParam( + filter: IFilterElement +): AttributeListUrlFilters { + const { name } = filter; + + switch (name) { + case AttributeFilterKeys.availableInGrid: + return getSingleValueQueryParam( + filter, + AttributeListUrlFiltersEnum.availableInGrid + ); + + case AttributeFilterKeys.filterableInDashboard: + return getSingleValueQueryParam( + filter, + AttributeListUrlFiltersEnum.filterableInDashboard + ); + + case AttributeFilterKeys.filterableInStorefront: + return getSingleValueQueryParam( + filter, + AttributeListUrlFiltersEnum.filterableInStorefront + ); + + case AttributeFilterKeys.isVariantOnly: + return getSingleValueQueryParam( + filter, + AttributeListUrlFiltersEnum.isVariantOnly + ); + + case AttributeFilterKeys.valueRequired: + return getSingleValueQueryParam( + filter, + AttributeListUrlFiltersEnum.valueRequired + ); + + case AttributeFilterKeys.visibleInStorefront: + return getSingleValueQueryParam( + filter, + AttributeListUrlFiltersEnum.visibleInStorefront + ); + } +} + export const { deleteFilterTab, getFilterTabs, diff --git a/src/collections/components/CollectionListPage/CollectionListPage.tsx b/src/collections/components/CollectionListPage/CollectionListPage.tsx index df72134ba..dcf23c717 100644 --- a/src/collections/components/CollectionListPage/CollectionListPage.tsx +++ b/src/collections/components/CollectionListPage/CollectionListPage.tsx @@ -5,34 +5,42 @@ import { FormattedMessage, useIntl } from "react-intl"; import { Container } from "@saleor/components/Container"; import PageHeader from "@saleor/components/PageHeader"; -import SearchBar from "@saleor/components/SearchBar"; +import FilterBar from "@saleor/components/FilterBar"; import { sectionNames } from "@saleor/intl"; import { ListActions, PageListProps, - SearchPageProps, + FilterPageProps, TabPageProps, SortPage } from "@saleor/types"; import { CollectionListUrlSortField } from "@saleor/collections/urls"; import { CollectionList_collections_edges_node } from "../../types/CollectionList"; import CollectionList from "../CollectionList/CollectionList"; +import { + CollectionFilterKeys, + CollectionListFilterOpts, + createFilterStructure +} from "./filters"; export interface CollectionListPageProps extends PageListProps, ListActions, - SearchPageProps, + FilterPageProps, SortPage, TabPageProps { collections: CollectionList_collections_edges_node[]; } const CollectionListPage: React.FC = ({ + currencySymbol, currentTab, disabled, + filterOpts, initialSearch, onAdd, onAll, + onFilterChange, onSearchChange, onTabChange, onTabDelete, @@ -42,6 +50,8 @@ const CollectionListPage: React.FC = ({ }) => { const intl = useIntl(); + const structure = createFilterStructure(intl, filterOpts); + return ( @@ -58,18 +68,21 @@ const CollectionListPage: React.FC = ({ - ; +} + +export enum CollectionFilterKeys { + status = "status" +} + +const messages = defineMessages({ + hidden: { + defaultMessage: "Hidden", + description: "collection" + }, + published: { + defaultMessage: "Published", + description: "collection" + } +}); + +export function createFilterStructure( + intl: IntlShape, + opts: CollectionListFilterOpts +): IFilter { + return [ + { + ...createOptionsField( + CollectionFilterKeys.status, + intl.formatMessage(commonMessages.status), + [opts.status.value], + false, + [ + { + label: intl.formatMessage(messages.published), + value: CollectionPublished.PUBLISHED + }, + { + label: intl.formatMessage(messages.hidden), + value: CollectionPublished.HIDDEN + } + ] + ), + active: opts.status.active + } + ]; +} diff --git a/src/collections/components/CollectionListPage/index.ts b/src/collections/components/CollectionListPage/index.ts index 3324e1926..353abb74d 100644 --- a/src/collections/components/CollectionListPage/index.ts +++ b/src/collections/components/CollectionListPage/index.ts @@ -1,2 +1,3 @@ export { default } from "./CollectionListPage"; export * from "./CollectionListPage"; +export * from "./filters"; diff --git a/src/collections/urls.ts b/src/collections/urls.ts index 908fc998a..d22345b8a 100644 --- a/src/collections/urls.ts +++ b/src/collections/urls.ts @@ -15,6 +15,7 @@ const collectionSectionUrl = "/collections/"; export const collectionListPath = collectionSectionUrl; export enum CollectionListUrlFiltersEnum { + status = "status", query = "query" } export type CollectionListUrlFilters = Filters; diff --git a/src/collections/views/CollectionList/CollectionList.tsx b/src/collections/views/CollectionList/CollectionList.tsx index f5c30df11..dbc0c3be2 100644 --- a/src/collections/views/CollectionList/CollectionList.tsx +++ b/src/collections/views/CollectionList/CollectionList.tsx @@ -23,6 +23,8 @@ import { ListViews } from "@saleor/types"; import { getSortParams } from "@saleor/utils/sort"; import createSortHandler from "@saleor/utils/handlers/sortHandler"; import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers"; +import useShop from "@saleor/hooks/useShop"; +import createFilterHandlers from "@saleor/utils/handlers/filterHandlers"; import CollectionListPage from "../../components/CollectionListPage/CollectionListPage"; import { TypedCollectionBulkDelete, @@ -34,7 +36,6 @@ import { CollectionBulkPublish } from "../../types/CollectionBulkPublish"; import { collectionAddUrl, collectionListUrl, - CollectionListUrlFilters, CollectionListUrlQueryParams, collectionUrl, CollectionListUrlDialog @@ -45,8 +46,10 @@ import { getActiveFilters, getFilterTabs, getFilterVariables, - saveFilterTab -} from "./filter"; + saveFilterTab, + getFilterQueryParam, + getFilterOpts +} from "./filters"; import { getSortQueryVariables } from "./sort"; interface CollectionListProps { @@ -57,6 +60,7 @@ export const CollectionList: React.FC = ({ params }) => { const navigate = useNavigator(); const notify = useNotifier(); const paginate = usePaginator(); + const shop = useShop(); const { isSelected, listElements, reset, toggle, toggleAll } = useBulkActions( params.ids ); @@ -88,16 +92,17 @@ export const CollectionList: React.FC = ({ params }) => { : 0 : parseInt(params.activeTab, 0); - const changeFilterField = (filter: CollectionListUrlFilters) => { - reset(); - navigate( - collectionListUrl({ - ...getActiveFilters(params), - ...filter, - activeTab: undefined - }) - ); - }; + const [ + changeFilters, + resetFilters, + handleSearchChange + ] = createFilterHandlers({ + cleanupFn: reset, + createUrl: collectionListUrl, + getFilterQueryParam, + navigate, + params + }); const [openModal, closeModal] = createDialogActionHandlers< CollectionListUrlDialog, @@ -154,6 +159,7 @@ export const CollectionList: React.FC = ({ params }) => { }; const handleSort = createSortHandler(navigate, collectionListUrl, params); + const currencySymbol = maybe(() => shop.defaultCurrency, "USD"); return ( @@ -162,11 +168,14 @@ export const CollectionList: React.FC = ({ params }) => { {(collectionBulkPublish, collectionBulkPublishOpts) => ( <> changeFilterField({ query })} + onSearchChange={handleSearchChange} + onFilterChange={changeFilters} onAdd={() => navigate(collectionAddUrl)} - onAll={() => navigate(collectionListUrl())} + onAll={resetFilters} onTabChange={handleTabChange} onTabDelete={() => openModal("delete-search")} onTabSave={() => openModal("save-search")} diff --git a/src/collections/views/CollectionList/__snapshots__/filters.test.ts.snap b/src/collections/views/CollectionList/__snapshots__/filters.test.ts.snap new file mode 100644 index 000000000..37f201d26 --- /dev/null +++ b/src/collections/views/CollectionList/__snapshots__/filters.test.ts.snap @@ -0,0 +1,9 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Filtering URL params should not be empty if active filters are present 1`] = ` +Object { + "status": "PUBLISHED", +} +`; + +exports[`Filtering URL params should not be empty if active filters are present 2`] = `"status=PUBLISHED"`; diff --git a/src/collections/views/CollectionList/filter.ts b/src/collections/views/CollectionList/filter.ts deleted file mode 100644 index 47a710945..000000000 --- a/src/collections/views/CollectionList/filter.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { CollectionFilterInput } from "@saleor/types/globalTypes"; -import { - createFilterTabUtils, - createFilterUtils -} from "../../../utils/filters"; -import { - CollectionListUrlFilters, - CollectionListUrlFiltersEnum, - CollectionListUrlQueryParams -} from "../../urls"; - -export const COLLECTION_FILTERS_KEY = "collectionFilters"; - -export function getFilterVariables( - params: CollectionListUrlFilters -): CollectionFilterInput { - return { - search: params.query - }; -} - -export const { - deleteFilterTab, - getFilterTabs, - saveFilterTab -} = createFilterTabUtils(COLLECTION_FILTERS_KEY); - -export const { areFiltersApplied, getActiveFilters } = createFilterUtils< - CollectionListUrlQueryParams, - CollectionListUrlFilters ->(CollectionListUrlFiltersEnum); diff --git a/src/collections/views/CollectionList/filters.test.ts b/src/collections/views/CollectionList/filters.test.ts new file mode 100644 index 000000000..1dc360a69 --- /dev/null +++ b/src/collections/views/CollectionList/filters.test.ts @@ -0,0 +1,65 @@ +import { createIntl } from "react-intl"; +import { stringify as stringifyQs } from "qs"; + +import { CollectionListUrlFilters } from "@saleor/collections/urls"; +import { createFilterStructure } from "@saleor/collections/components/CollectionListPage"; +import { getFilterQueryParams } from "@saleor/utils/filters"; +import { CollectionPublished } from "@saleor/types/globalTypes"; +import { config } from "@test/intl"; +import { getExistingKeys, setFilterOptsStatus } from "@test/filters"; +import { getFilterVariables, getFilterQueryParam } from "./filters"; + +describe("Filtering query params", () => { + it("should be empty object if no params given", () => { + const params: CollectionListUrlFilters = {}; + const filterVariables = getFilterVariables(params); + + expect(getExistingKeys(filterVariables)).toHaveLength(0); + }); + + it("should not be empty object if params given", () => { + const params: CollectionListUrlFilters = { + status: CollectionPublished.PUBLISHED + }; + const filterVariables = getFilterVariables(params); + + expect(getExistingKeys(filterVariables)).toHaveLength(1); + }); +}); + +describe("Filtering URL params", () => { + const intl = createIntl(config); + + const filters = createFilterStructure(intl, { + status: { + active: false, + value: CollectionPublished.PUBLISHED + } + }); + + it("should be empty if no active filters", () => { + const filterQueryParams = getFilterQueryParams( + filters, + getFilterQueryParam + ); + + expect(getExistingKeys(filterQueryParams)).toHaveLength(0); + }); + + it("should not be empty if active filters are present", () => { + const filters = createFilterStructure(intl, { + status: { + active: true, + value: CollectionPublished.PUBLISHED + } + }); + + const filterQueryParams = getFilterQueryParams( + setFilterOptsStatus(filters, true), + getFilterQueryParam + ); + + expect(filterQueryParams).toMatchSnapshot(); + expect(stringifyQs(filterQueryParams)).toMatchSnapshot(); + }); +}); diff --git a/src/collections/views/CollectionList/filters.ts b/src/collections/views/CollectionList/filters.ts new file mode 100644 index 000000000..9b42354a5 --- /dev/null +++ b/src/collections/views/CollectionList/filters.ts @@ -0,0 +1,70 @@ +import { + CollectionFilterInput, + CollectionPublished +} from "@saleor/types/globalTypes"; +import { IFilterElement } from "@saleor/components/Filter"; +import { maybe, findValueInEnum } from "@saleor/misc"; +import { + CollectionListFilterOpts, + CollectionFilterKeys +} from "@saleor/collections/components/CollectionListPage"; +import { + CollectionListUrlFilters, + CollectionListUrlFiltersEnum, + CollectionListUrlQueryParams +} from "../../urls"; +import { + createFilterTabUtils, + createFilterUtils, + getSingleEnumValueQueryParam +} from "../../../utils/filters"; + +export const COLLECTION_FILTERS_KEY = "collectionFilters"; + +export function getFilterOpts( + params: CollectionListUrlFilters +): CollectionListFilterOpts { + return { + status: { + active: maybe(() => params.status !== undefined, false), + value: maybe(() => findValueInEnum(status, CollectionPublished)) + } + }; +} + +export function getFilterVariables( + params: CollectionListUrlFilters +): CollectionFilterInput { + return { + published: params.status + ? findValueInEnum(params.status, CollectionPublished) + : undefined, + search: params.query + }; +} + +export function getFilterQueryParam( + filter: IFilterElement +): CollectionListUrlFilters { + const { name } = filter; + + switch (name) { + case CollectionFilterKeys.status: + return getSingleEnumValueQueryParam( + filter, + CollectionListUrlFiltersEnum.status, + CollectionPublished + ); + } +} + +export const { + deleteFilterTab, + getFilterTabs, + saveFilterTab +} = createFilterTabUtils(COLLECTION_FILTERS_KEY); + +export const { areFiltersApplied, getActiveFilters } = createFilterUtils< + CollectionListUrlQueryParams, + CollectionListUrlFilters +>(CollectionListUrlFiltersEnum); diff --git a/src/components/AppLayout/AppLayout.tsx b/src/components/AppLayout/AppLayout.tsx index 32517e68e..1db6cb01c 100644 --- a/src/components/AppLayout/AppLayout.tsx +++ b/src/components/AppLayout/AppLayout.tsx @@ -47,7 +47,6 @@ const useStyles = makeStyles( }, bottom: 0, gridColumn: 2, - height: 70, position: "sticky", zIndex: 10 }, diff --git a/src/components/Filter/Arrow.tsx b/src/components/Filter/Arrow.tsx new file mode 100644 index 000000000..ef4cbe5d8 --- /dev/null +++ b/src/components/Filter/Arrow.tsx @@ -0,0 +1,22 @@ +import React from "react"; + +const Arrow: React.FC> = props => ( + + + +); + +Arrow.displayName = "Arrow"; +export default Arrow; diff --git a/src/components/Filter/Filter.tsx b/src/components/Filter/Filter.tsx index 7b0774307..bcaeefc26 100644 --- a/src/components/Filter/Filter.tsx +++ b/src/components/Filter/Filter.tsx @@ -1,24 +1,22 @@ import ButtonBase from "@material-ui/core/ButtonBase"; +import ClickAwayListener from "@material-ui/core/ClickAwayListener"; import Grow from "@material-ui/core/Grow"; -import Paper from "@material-ui/core/Paper"; import Popper from "@material-ui/core/Popper"; import { makeStyles } from "@material-ui/core/styles"; import { fade } from "@material-ui/core/styles/colorManipulator"; import Typography from "@material-ui/core/Typography"; -import ArrowDropDownIcon from "@material-ui/icons/ArrowDropDown"; import classNames from "classnames"; import React from "react"; import { FormattedMessage } from "react-intl"; -import { FilterContentSubmitData } from "./FilterContent"; -import { IFilter } from "./types"; +import { IFilter, IFilterElement } from "./types"; +import useFilter from "./useFilter"; import { FilterContent } from "."; -export interface FilterProps { +export interface FilterProps { currencySymbol: string; menu: IFilter; - filterLabel: string; - onFilterAdd: (filter: FilterContentSubmitData) => void; + onFilterAdd: (filter: Array>) => void; } const useStyles = makeStyles( @@ -49,7 +47,6 @@ const useStyles = makeStyles( color: theme.palette.primary.main, fontSize: 14, fontWeight: 600 as 600, - marginRight: 4, textTransform: "uppercase" }, filterButton: { @@ -73,70 +70,106 @@ const useStyles = makeStyles( width: 240 }, popover: { - zIndex: 1 + width: 376, + zIndex: 3 }, rotate: { transform: "rotate(180deg)" + }, + separator: { + backgroundColor: theme.palette.primary.main, + display: "inline-block", + height: 28, + margin: theme.spacing(0, 1.5, 0, 1), + width: 1 } }), { name: "Filter" } ); const Filter: React.FC = props => { - const { currencySymbol, filterLabel, menu, onFilterAdd } = props; + const { currencySymbol, menu, onFilterAdd } = props; const classes = useStyles(props); const anchor = React.useRef(); const [isFilterMenuOpened, setFilterMenuOpened] = React.useState(false); + const [data, dispatch, reset] = useFilter(menu); + + const isFilterActive = menu.some(filterElement => filterElement.active); return ( -
- setFilterMenuOpened(!isFilterMenuOpened)} - > - - - - { + if ((event.target as HTMLElement).getAttribute("role") !== "option") { + setFilterMenuOpened(false); + } + }} + > +
+ - - - {({ TransitionProps, placement }) => ( - - - {filterLabel} + onClick={() => setFilterMenuOpened(!isFilterMenuOpened)} + > + + + + {isFilterActive && ( + <> + + + {menu.reduce( + (acc, filterElement) => acc + (filterElement.active ? 1 : 0), + 0 + )} + + + )} + + + {({ TransitionProps, placement }) => ( + { + filters={data} + onClear={reset} + onFilterPropertyChange={dispatch} + onSubmit={() => { onFilterAdd(data); setFilterMenuOpened(false); }} /> - - - )} - -
+ + )} + +
+ ); }; Filter.displayName = "Filter"; diff --git a/src/components/Filter/FilterActions.tsx b/src/components/Filter/FilterActions.tsx deleted file mode 100644 index 60eee1d97..000000000 --- a/src/components/Filter/FilterActions.tsx +++ /dev/null @@ -1,114 +0,0 @@ -import { makeStyles } from "@material-ui/core/styles"; -import TextField, { TextFieldProps } from "@material-ui/core/TextField"; -import classNames from "classnames"; -import React from "react"; - -import { FilterContentSubmitData, IFilter } from "../Filter"; -import Filter from "./Filter"; - -const useInputStyles = makeStyles( - { - input: { - padding: "10.5px 12px" - }, - root: { - flex: 1 - } - }, - { name: "FilterActions" } -); - -const Search: React.FC = props => { - const classes = useInputStyles({}); - - return ( - - ); -}; - -const useStyles = makeStyles( - theme => ({ - actionContainer: { - display: "flex", - flexWrap: "wrap", - padding: theme.spacing(1, 3) - }, - searchOnly: { - paddingBottom: theme.spacing(1.5) - } - }), - { - name: "FilterActions" - } -); - -export interface FilterActionsPropsSearch { - placeholder: string; - search: string; - onSearchChange: (event: React.ChangeEvent) => void; -} -export interface FilterActionsPropsFilters { - currencySymbol: string; - menu: IFilter; - filterLabel: string; - onFilterAdd: (filter: FilterContentSubmitData) => void; -} - -export const FilterActionsOnlySearch: React.FC< - FilterActionsPropsSearch -> = props => { - const { onSearchChange, placeholder, search } = props; - const classes = useStyles(props); - - return ( -
- -
- ); -}; - -export type FilterActionsProps = FilterActionsPropsSearch & - FilterActionsPropsFilters; -const FilterActions: React.FC = props => { - const { - currencySymbol, - filterLabel, - menu, - onFilterAdd, - onSearchChange, - placeholder, - search - } = props; - const classes = useStyles(props); - - return ( -
- - -
- ); -}; - -FilterActions.displayName = "FilterActions"; -export default FilterActions; diff --git a/src/components/Filter/FilterAutocompleteField.tsx b/src/components/Filter/FilterAutocompleteField.tsx new file mode 100644 index 000000000..7a909c899 --- /dev/null +++ b/src/components/Filter/FilterAutocompleteField.tsx @@ -0,0 +1,154 @@ +import React from "react"; +import FormControlLabel from "@material-ui/core/FormControlLabel"; +import Typography from "@material-ui/core/Typography"; +import TextField from "@material-ui/core/TextField"; +import makeStyles from "@material-ui/core/styles/makeStyles"; +import { FormattedMessage } from "react-intl"; + +import { toggle } from "@saleor/utils/lists"; +import { MultiAutocompleteChoiceType } from "../MultiAutocompleteSelectField"; +import Link from "../Link"; +import Checkbox from "../Checkbox"; +import Hr from "../Hr"; +import { FilterBaseFieldProps } from "./types"; + +interface FilterAutocompleteFieldProps extends FilterBaseFieldProps { + displayValues: Record; + setDisplayValues: ( + values: Record + ) => void; +} + +const useStyles = makeStyles( + theme => ({ + hr: { + backgroundColor: theme.palette.primary.light, + margin: theme.spacing(1, 0) + }, + input: { + padding: "12px 0 9px 12px" + }, + inputContainer: { + marginBottom: theme.spacing(1) + }, + noResults: { + marginTop: theme.spacing(1) + }, + option: { + left: -theme.spacing(0.5), + position: "relative" + }, + showMore: { + display: "inline-block", + marginTop: theme.spacing(1) + } + }), + { name: "FilterAutocompleteField" } +); + +const FilterAutocompleteField: React.FC = ({ + displayValues, + filterField, + setDisplayValues, + onFilterPropertyChange +}) => { + const classes = useStyles({}); + + const fieldDisplayValues = displayValues[filterField.name]; + const availableOptions = filterField.options.filter(option => + fieldDisplayValues.every( + displayValue => displayValue.value !== option.value + ) + ); + const displayNoResults = + availableOptions.length === 0 && fieldDisplayValues.length === 0; + const displayHr = !( + (fieldDisplayValues.length === 0 && availableOptions.length > 0) || + (availableOptions.length === 0 && fieldDisplayValues.length > 0) || + displayNoResults + ); + + const handleChange = (option: MultiAutocompleteChoiceType) => { + onFilterPropertyChange({ + payload: { + name: filterField.name, + update: { + value: toggle(option.value, filterField.value, (a, b) => a === b) + } + }, + type: "set-property" + }); + + setDisplayValues({ + ...displayValues, + [filterField.name]: toggle( + option, + fieldDisplayValues, + (a, b) => a.value === b.value + ) + }); + }; + + return ( +
+ filterField.onSearchChange(event.target.value)} + /> + {fieldDisplayValues.map(displayValue => ( +
+ + } + label={displayValue.label} + name={filterField.name} + onChange={() => handleChange(displayValue)} + /> +
+ ))} + {displayHr &&
} + {displayNoResults && ( + + + + )} + {availableOptions.map(option => ( +
+ + } + label={option.label} + name={filterField.name} + onChange={() => handleChange(option)} + /> +
+ ))} + {filterField.hasMore && ( + + + + )} +
+ ); +}; + +FilterAutocompleteField.displayName = "FilterAutocompleteField"; +export default FilterAutocompleteField; diff --git a/src/components/Filter/FilterContent.tsx b/src/components/Filter/FilterContent.tsx index 08ec466cd..9a9021ccc 100644 --- a/src/components/Filter/FilterContent.tsx +++ b/src/components/Filter/FilterContent.tsx @@ -1,147 +1,395 @@ import Button from "@material-ui/core/Button"; +import Paper from "@material-ui/core/Paper"; +import Radio from "@material-ui/core/Radio"; import Typography from "@material-ui/core/Typography"; +import FormControlLabel from "@material-ui/core/FormControlLabel"; import React from "react"; -import { FormattedMessage, useIntl } from "react-intl"; +import { FormattedMessage, useIntl, IntlShape } from "react-intl"; +import classNames from "classnames"; -import { makeStyles } from "@material-ui/core/styles"; -import { getMenuItemByValue, isLeaf, walkToRoot } from "../../utils/menu"; -import FormSpacer from "../FormSpacer"; +import makeStyles from "@material-ui/core/styles/makeStyles"; +import { fade } from "@material-ui/core/styles/colorManipulator"; +import { buttonMessages } from "@saleor/intl"; +import { TextField } from "@material-ui/core"; +import useStateFromProps from "@saleor/hooks/useStateFromProps"; +import Hr from "../Hr"; +import Checkbox from "../Checkbox"; import SingleSelectField from "../SingleSelectField"; -import FilterElement from "./FilterElement"; -import { IFilter } from "./types"; +import { SingleAutocompleteChoiceType } from "../SingleAutocompleteSelectField"; +import FormSpacer from "../FormSpacer"; +import { MultiAutocompleteChoiceType } from "../MultiAutocompleteSelectField"; +import { IFilter, FieldType, FilterType } from "./types"; +import Arrow from "./Arrow"; +import { FilterReducerAction } from "./reducer"; +import FilterAutocompleteField from "./FilterAutocompleteField"; +import FilterOptionField from "./FilterOptionField"; -export interface FilterContentSubmitData { - name: TKeys; - value: string | string[]; -} -export interface FilterContentProps { +export interface FilterContentProps { currencySymbol: string; - filters: IFilter; - onSubmit: (data: FilterContentSubmitData) => void; -} - -function checkFilterValue(value: string | string[]): boolean { - if (typeof value === "string") { - return !!value; - } - return value.some(v => !!v); -} - -function getFilterChoices(items: IFilter) { - return items.map(filterItem => ({ - label: filterItem.label, - value: filterItem.value.toString() - })); + filters: IFilter; + onFilterPropertyChange: React.Dispatch>; + onClear: () => void; + onSubmit: () => void; } const useStyles = makeStyles( - { + theme => ({ + actionBar: { + alignItems: "center", + display: "flex", + justifyContent: "space-between", + padding: theme.spacing(1, 3) + }, + andLabel: { + margin: theme.spacing(0, 2) + }, + arrow: { + marginRight: theme.spacing(2) + }, + clear: { + marginRight: theme.spacing(1) + }, + filterFieldBar: { + "&:not(:last-of-type)": { + borderBottom: `1px solid ${theme.palette.divider}` + }, + padding: theme.spacing(1, 2.5) + }, + filterSettings: { + background: fade(theme.palette.primary.main, 0.2), + padding: theme.spacing(2, 3) + }, input: { - padding: "20px 12px 17px" + padding: "12px 0 9px 12px" + }, + inputRange: { + alignItems: "center", + display: "flex" + }, + label: { + fontWeight: 600 + }, + option: { + left: -theme.spacing(0.5), + position: "relative" + }, + optionRadio: { + left: -theme.spacing(0.25) } - }, + }), { name: "FilterContent" } ); +function getIsFilterMultipleChoices( + intl: IntlShape +): SingleAutocompleteChoiceType[] { + return [ + { + label: intl.formatMessage({ + defaultMessage: "equal to", + description: "is filter range or value" + }), + value: FilterType.SINGULAR + }, + { + label: intl.formatMessage({ + defaultMessage: "between", + description: "is filter range or value" + }), + value: FilterType.MULTIPLE + } + ]; +} + const FilterContent: React.FC = ({ currencySymbol, filters, + onClear, + onFilterPropertyChange, onSubmit }) => { const intl = useIntl(); - const [menuValue, setMenuValue] = React.useState(null); - const [filterValue, setFilterValue] = React.useState(""); const classes = useStyles({}); + const [ + autocompleteDisplayValues, + setAutocompleteDisplayValues + ] = useStateFromProps>( + filters.reduce((acc, filterField) => { + if (filterField.type === FieldType.autocomplete) { + acc[filterField.name] = filterField.displayValues; + } - const activeMenu = menuValue - ? getMenuItemByValue(filters, menuValue) - : undefined; - const menus = menuValue - ? walkToRoot(filters, menuValue).slice(-1) - : undefined; - - const onMenuChange = (event: React.ChangeEvent) => { - setMenuValue(event.target.value); - setFilterValue(""); - }; + return acc; + }, {}) + ); return ( - <> - +
{ + event.preventDefault(); + onSubmit(); }} - value={menus ? menus[0].value : menuValue} - placeholder={intl.formatMessage({ - defaultMessage: "Select Filter..." - })} - /> - {menus && - menus.map( - (filterItem, filterItemIndex) => - !isLeaf(filterItem) && ( - - - +
+ + + +
+ + +
+
+
+ {filters + .sort((a, b) => (a.name > b.name ? 1 : -1)) + .map(filterField => ( + +
+ } + label={filterField.label} + onChange={() => + onFilterPropertyChange({ + payload: { + name: filterField.name, + update: { + active: !filterField.active + } + }, + type: "set-property" + }) } - placeholder={intl.formatMessage({ - defaultMessage: "Select Filter..." - })} /> - - ) - )} - {activeMenu && isLeaf(activeMenu) && ( - <> - - {activeMenu.data.additionalText && ( - {activeMenu.data.additionalText} - )} - setFilterValue(value)} - /> - {checkFilterValue(filterValue) && ( - <> - - - - )} - - )} - +
+ {filterField.active && ( +
+ {filterField.type === FieldType.text && ( + + onFilterPropertyChange({ + payload: { + name: filterField.name, + update: { + value: [event.target.value, filterField.value[1]] + } + }, + type: "set-property" + }) + } + /> + )} + {[FieldType.date, FieldType.price, FieldType.number].includes( + filterField.type + ) && ( + <> + + onFilterPropertyChange({ + payload: { + name: filterField.name, + update: { + multiple: + event.target.value === FilterType.MULTIPLE + } + }, + type: "set-property" + }) + } + /> + +
+
+ +
+ {filterField.multiple ? ( + <> + + onFilterPropertyChange({ + payload: { + name: filterField.name, + update: { + value: [ + event.target.value, + filterField.value[1] + ] + } + }, + type: "set-property" + }) + } + /> + + + + + onFilterPropertyChange({ + payload: { + name: filterField.name, + update: { + value: [ + filterField.value[0], + event.target.value + ] + } + }, + type: "set-property" + }) + } + /> + + ) : ( + + onFilterPropertyChange({ + payload: { + name: filterField.name, + update: { + value: [ + event.target.value, + filterField.value[1] + ] + } + }, + type: "set-property" + }) + } + /> + )} +
+ + )} + {filterField.type === FieldType.options && ( + + )} + {filterField.type === FieldType.boolean && + filterField.options.map(option => ( +
+ + } + label={option.label} + name={filterField.name} + onChange={() => + onFilterPropertyChange({ + payload: { + name: filterField.name, + update: { + value: [option.value] + } + }, + type: "set-property" + }) + } + /> +
+ ))} + {filterField.type === FieldType.autocomplete && + filterField.multiple && ( + + )} +
+ )} +
+ ))} + + ); }; FilterContent.displayName = "FilterContent"; diff --git a/src/components/Filter/FilterElement.tsx b/src/components/Filter/FilterElement.tsx deleted file mode 100644 index b5cb5c050..000000000 --- a/src/components/Filter/FilterElement.tsx +++ /dev/null @@ -1,229 +0,0 @@ -import { makeStyles } from "@material-ui/core/styles"; -import TextField from "@material-ui/core/TextField"; -import Typography from "@material-ui/core/Typography"; -import React from "react"; -import { FormattedMessage, useIntl } from "react-intl"; - -import Calendar from "../../icons/Calendar"; -import FormSpacer from "../FormSpacer"; -import PriceField from "../PriceField"; -import SingleSelectField from "../SingleSelectField"; -import { FieldType, IFilterItem } from "./types"; - -export interface FilterElementProps { - className?: string; - filter: IFilterItem; - value: string | string[]; - onChange: (value: string | string[]) => void; -} - -const useStyles = makeStyles( - { - calendar: { - margin: 8 - }, - input: { - padding: "20px 12px 17px" - } - }, - { name: "FilterElement" } -); - -export interface FilterElementProps { - className?: string; - currencySymbol: string; - filter: IFilterItem; - value: string | string[]; - onChange: (value: string | string[]) => void; -} - -const FilterElement: React.FC = ({ - currencySymbol, - className, - filter, - onChange, - value -}) => { - const intl = useIntl(); - const classes = useStyles({}); - - if (filter.data.type === FieldType.date) { - return ( - onChange(event.target.value)} - value={value} - InputProps={{ - classes: { - input: classes.input - }, - startAdornment: - }} - /> - ); - } else if (filter.data.type === FieldType.rangeDate) { - return ( - <> - - - - onChange([event.target.value, value[1]])} - InputProps={{ - classes: { - input: classes.input - }, - startAdornment: - }} - /> - - - - - onChange([value[0], event.target.value])} - InputProps={{ - classes: { - input: classes.input - }, - startAdornment: - }} - /> - - ); - } else if (filter.data.type === FieldType.range) { - return ( - <> - - - - onChange([event.target.value, value[1]])} - type="number" - InputProps={{ - classes: { - input: classes.input - } - }} - /> - - - - - onChange([value[0], event.target.value])} - type="number" - InputProps={{ - classes: { - input: classes.input - } - }} - /> - - ); - } else if (filter.data.type === FieldType.rangePrice) { - return ( - <> - - - - onChange([event.target.value, value[1]])} - InputProps={{ - classes: { - input: classes.input - } - }} - /> - - - - - onChange([value[0], event.target.value])} - InputProps={{ - classes: { - input: classes.input - } - }} - /> - - ); - } else if (filter.data.type === FieldType.select) { - return ( - ({ - ...option, - value: option.value.toString() - }))} - selectProps={{ - className, - inputProps: { - className: classes.input - } - }} - value={value as string} - placeholder={intl.formatMessage({ - defaultMessage: "Select Filter..." - })} - onChange={event => onChange(event.target.value)} - /> - ); - } else if (filter.data.type === FieldType.price) { - return ( - onChange(event.target.value)} - InputProps={{ - classes: { - input: !filter.data.fieldLabel && classes.input - } - }} - value={value as string} - /> - ); - } else if (filter.data.type === FieldType.hidden) { - onChange(filter.data.value); - return ; - } - return ( - onChange(event.target.value)} - value={value as string} - /> - ); -}; -FilterElement.displayName = "FilterElement"; -export default FilterElement; diff --git a/src/components/Filter/FilterOptionField.tsx b/src/components/Filter/FilterOptionField.tsx new file mode 100644 index 000000000..5900b3822 --- /dev/null +++ b/src/components/Filter/FilterOptionField.tsx @@ -0,0 +1,74 @@ +import FormControlLabel from "@material-ui/core/FormControlLabel"; +import Radio from "@material-ui/core/Radio"; +import React from "react"; +import makeStyles from "@material-ui/core/styles/makeStyles"; +import classNames from "classnames"; + +import { toggle } from "@saleor/utils/lists"; +import Checkbox from "../Checkbox"; +import { FilterBaseFieldProps } from "./types"; + +const useStyles = makeStyles( + theme => ({ + option: { + left: -theme.spacing(0.5), + position: "relative" + }, + optionRadio: { + left: -theme.spacing(0.25) + }, + root: {} + }), + { name: "FilterOptionField" } +); + +const FilterOptionField: React.FC = ({ + filterField, + onFilterPropertyChange +}) => { + const classes = useStyles({}); + const handleSelect = (value: string) => + onFilterPropertyChange({ + payload: { + name: filterField.name, + update: { + value: filterField.multiple + ? toggle(value, filterField.value, (a, b) => a === b) + : [value] + } + }, + type: "set-property" + }); + + return ( +
+ {filterField.options.map(option => ( +
+ + ) : ( + + ) + } + label={option.label} + name={filterField.name} + onChange={() => handleSelect(option.value)} + /> +
+ ))} +
+ ); +}; + +FilterOptionField.displayName = "FilterOptionField"; +export default FilterOptionField; diff --git a/src/components/Filter/FilterSearch.tsx b/src/components/Filter/FilterSearch.tsx deleted file mode 100644 index 770de6d18..000000000 --- a/src/components/Filter/FilterSearch.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import { makeStyles } from "@material-ui/core/styles"; -import React from "react"; -import { FormattedMessage } from "react-intl"; - -import { SearchPageProps } from "../../types"; -import Debounce from "../Debounce"; -import { FilterActionsOnlySearch } from "../Filter/FilterActions"; -import Hr from "../Hr"; -import Link from "../Link"; - -export interface FilterSearchProps extends SearchPageProps { - displaySearchAction: "save" | "delete" | null; - searchPlaceholder: string; - onSearchDelete?: () => void; - onSearchSave?: () => void; -} - -const useStyles = makeStyles( - theme => ({ - tabAction: { - display: "inline-block" - }, - tabActionContainer: { - borderBottom: `1px solid ${theme.palette.divider}`, - display: "flex", - justifyContent: "flex-end", - marginTop: theme.spacing(), - padding: theme.spacing(0, 1, 3, 1) - } - }), - { - name: "FilterSearch" - } -); - -const FilterSearch: React.FC = props => { - const { - displaySearchAction, - initialSearch, - onSearchChange, - onSearchDelete, - onSearchSave, - searchPlaceholder - } = props; - const classes = useStyles(props); - const [search, setSearch] = React.useState(initialSearch); - React.useEffect(() => setSearch(initialSearch), [initialSearch]); - - return ( - - {debounceSearchChange => { - const handleSearchChange = (event: React.ChangeEvent) => { - const value = event.target.value; - setSearch(value); - debounceSearchChange(value); - }; - - return ( - <> - - {!!displaySearchAction ? ( -
-
- {displaySearchAction === "save" ? ( - - - - ) : ( - - - - )} -
-
- ) : ( -
- )} - - ); - }} -
- ); -}; - -FilterSearch.displayName = "FilterSearch"; -export default FilterSearch; diff --git a/src/components/Filter/reducer.ts b/src/components/Filter/reducer.ts new file mode 100644 index 000000000..e8d837c7c --- /dev/null +++ b/src/components/Filter/reducer.ts @@ -0,0 +1,69 @@ +import { update } from "@saleor/utils/lists"; +import { IFilter, IFilterElementMutableData } from "./types"; + +export type FilterReducerActionType = + | "clear" + | "merge" + | "reset" + | "set-property"; +export interface FilterReducerAction { + type: FilterReducerActionType; + payload: Partial<{ + name: T; + update: Partial; + new: IFilter; + }>; +} + +function merge( + prevState: IFilter, + newState: IFilter +): IFilter { + return newState.map(newFilter => { + const prevFilter = prevState.find( + prevFilter => prevFilter.name === newFilter.name + ); + if (!!prevFilter) { + return { + ...newFilter, + active: prevFilter.active, + value: prevFilter.value + }; + } + + return newFilter; + }); +} + +function setProperty( + prevState: IFilter, + filter: T, + updateData: Partial +): IFilter { + const field = prevState.find(f => f.name === filter); + const updatedField = { + ...field, + ...updateData + }; + + return update(updatedField, prevState, (a, b) => a.name === b.name); +} + +function reduceFilter( + prevState: IFilter, + action: FilterReducerAction +): IFilter { + switch (action.type) { + case "set-property": + return setProperty(prevState, action.payload.name, action.payload.update); + case "merge": + return merge(prevState, action.payload.new); + case "reset": + return action.payload.new; + + default: + return prevState; + } +} + +export default reduceFilter; diff --git a/src/components/Filter/types.ts b/src/components/Filter/types.ts index c991801b8..a7b2f28a4 100644 --- a/src/components/Filter/types.ts +++ b/src/components/Filter/types.ts @@ -1,30 +1,43 @@ -import { IMenu, IMenuItem } from "../../utils/menu"; +import { FetchMoreProps, SearchPageProps } from "@saleor/types"; +import { MultiAutocompleteChoiceType } from "../MultiAutocompleteSelectField"; +import { FilterReducerAction } from "./reducer"; export enum FieldType { + autocomplete, + boolean, date, - hidden, + dateTime, number, price, - range, - rangeDate, - rangePrice, - select, + options, text } -export interface FilterChoice { +export interface IFilterElementMutableData { + active: boolean; + multiple: boolean; + options?: MultiAutocompleteChoiceType[]; + value: T[]; +} +export interface IFilterElement + extends IFilterElementMutableData, + Partial { + autocomplete?: boolean; + displayValues?: MultiAutocompleteChoiceType[]; + group?: T; label: string; - value: string | boolean; -} - -export interface FilterData { - additionalText?: string; - fieldLabel: string; - options?: FilterChoice[]; + name: T; type: FieldType; - value?: string; } -export type IFilterItem = IMenuItem; +export interface FilterBaseFieldProps { + filterField: IFilterElement; + onFilterPropertyChange: React.Dispatch>; +} -export type IFilter = IMenu; +export type IFilter = Array>; + +export enum FilterType { + MULTIPLE = "MULTIPLE", + SINGULAR = "SINGULAR" +} diff --git a/src/components/Filter/useFilter.ts b/src/components/Filter/useFilter.ts new file mode 100644 index 000000000..e51bebe56 --- /dev/null +++ b/src/components/Filter/useFilter.ts @@ -0,0 +1,38 @@ +import { useReducer, useEffect, Dispatch } from "react"; + +import reduceFilter, { FilterReducerAction } from "./reducer"; +import { IFilter, IFilterElement } from "./types"; + +export type UseFilter = [ + Array>, + Dispatch>, + () => void +]; + +function useFilter(initialFilter: IFilter): UseFilter { + const [data, dispatchFilterAction] = useReducer< + React.Reducer, FilterReducerAction> + >(reduceFilter, initialFilter); + + const reset = () => + dispatchFilterAction({ + payload: { + new: initialFilter + }, + type: "reset" + }); + + const refresh = () => + dispatchFilterAction({ + payload: { + new: initialFilter + }, + type: "merge" + }); + + useEffect(refresh, [initialFilter]); + + return [data, dispatchFilterAction, reset]; +} + +export default useFilter; diff --git a/src/components/FilterBar/FilterBar.tsx b/src/components/FilterBar/FilterBar.tsx index f4a71b815..db4f6445e 100644 --- a/src/components/FilterBar/FilterBar.tsx +++ b/src/components/FilterBar/FilterBar.tsx @@ -1,37 +1,66 @@ import React from "react"; -import { useIntl } from "react-intl"; +import { FormattedMessage, useIntl } from "react-intl"; +import makeStyles from "@material-ui/core/styles/makeStyles"; +import Button from "@material-ui/core/Button"; import { FilterProps } from "../../types"; -import Debounce from "../Debounce"; import { IFilter } from "../Filter/types"; -import FilterTabs, { FilterChips, FilterTab } from "../TableFilter"; +import FilterTabs, { FilterTab } from "../TableFilter"; +import { SearchBarProps } from "../SearchBar"; +import SearchInput from "../SearchBar/SearchInput"; +import Filter from "../Filter"; -export interface FilterBarProps extends FilterProps { - filterMenu: IFilter; +export interface FilterBarProps + extends FilterProps, + SearchBarProps { + filterStructure: IFilter; } -const FilterBar: React.FC = ({ - allTabLabel, - currencySymbol, - filterLabel, - filtersList, - filterMenu, - currentTab, - initialSearch, - searchPlaceholder, - tabs, - onAll, - onSearchChange, - onFilterAdd, - onTabChange, - onTabDelete, - onTabSave -}) => { +const useStyles = makeStyles( + theme => ({ + root: { + borderBottom: `1px solid ${theme.palette.divider}`, + display: "flex", + flexWrap: "wrap", + padding: theme.spacing(1, 3) + }, + tabActionButton: { + marginLeft: theme.spacing(2), + paddingLeft: theme.spacing(3), + paddingRight: theme.spacing(3) + } + }), + { + name: "FilterBar" + } +); + +const FilterBar: React.FC = props => { + const { + allTabLabel, + currencySymbol, + filterStructure, + currentTab, + initialSearch, + searchPlaceholder, + tabs, + onAll, + onSearchChange, + onFilterChange, + onTabChange, + onTabDelete, + onTabSave + } = props; + + const classes = useStyles(props); const intl = useIntl(); - const [search, setSearch] = React.useState(initialSearch); - React.useEffect(() => setSearch(initialSearch), [currentTab, initialSearch]); const isCustom = currentTab === tabs.length + 1; + const displayTabAction = isCustom + ? "save" + : currentTab === 0 + ? null + : "delete"; return ( <> @@ -53,34 +82,44 @@ const FilterBar: React.FC = ({ /> )} - - {debounceSearchChange => { - const handleSearchChange = (event: React.ChangeEvent) => { - const value = event.target.value; - setSearch(value); - debounceSearchChange(value); - }; - - return ( - - ); - }} - +
+ + + {displayTabAction && + (displayTabAction === "save" ? ( + + ) : ( + displayTabAction === "delete" && ( + + ) + ))} +
); }; diff --git a/src/components/Link.tsx b/src/components/Link.tsx index 3086f1bc5..3ddcec8a6 100644 --- a/src/components/Link.tsx +++ b/src/components/Link.tsx @@ -44,7 +44,7 @@ const Link: React.FC = props => { return ( -> = ({ allowCustomValues, enableLoadMore }) => { +const Story: React.FC> = ({ allowCustomValues, enableLoadMore }) => { const { change, data: countries } = useMultiAutocomplete([suggestions[0]]); return ( - {({ choices, fetchChoices, fetchMore, hasMore, loading }) => ( + {({ choices, fetchChoices, onFetchMore, hasMore, loading }) => ( country.value)} loading={loading} hasMore={enableLoadMore ? hasMore : false} - onFetchMore={enableLoadMore ? fetchMore : undefined} + onFetchMore={enableLoadMore ? onFetchMore : undefined} allowCustomValues={allowCustomValues} /> )} diff --git a/src/components/SaveButtonBar/SaveButtonBar.tsx b/src/components/SaveButtonBar/SaveButtonBar.tsx index 6086349f9..83ab6548a 100644 --- a/src/components/SaveButtonBar/SaveButtonBar.tsx +++ b/src/components/SaveButtonBar/SaveButtonBar.tsx @@ -41,7 +41,7 @@ const useStyles = makeStyles( background: theme.palette.background.default, borderTop: "1px solid transparent", boxShadow: `0 -5px 5px 0 ${theme.palette.divider}`, - height: "100%", + height: 70, transition: `box-shadow ${theme.transitions.duration.shortest}ms` }, spacer: { diff --git a/src/components/SearchBar/SearchBar.tsx b/src/components/SearchBar/SearchBar.tsx index 9e34a024b..17c66b4bb 100644 --- a/src/components/SearchBar/SearchBar.tsx +++ b/src/components/SearchBar/SearchBar.tsx @@ -1,15 +1,36 @@ import React from "react"; -import { useIntl } from "react-intl"; +import { FormattedMessage, useIntl } from "react-intl"; +import makeStyles from "@material-ui/core/styles/makeStyles"; +import Button from "@material-ui/core/Button"; import { SearchPageProps, TabPageProps } from "@saleor/types"; -import FilterSearch from "../Filter/FilterSearch"; import FilterTabs, { FilterTab } from "../TableFilter"; +import SearchInput from "./SearchInput"; export interface SearchBarProps extends SearchPageProps, TabPageProps { allTabLabel: string; searchPlaceholder: string; } +const useStyles = makeStyles( + theme => ({ + root: { + borderBottom: `1px solid ${theme.palette.divider}`, + display: "flex", + flexWrap: "wrap", + padding: theme.spacing(1, 3) + }, + tabActionButton: { + marginLeft: theme.spacing(2), + paddingLeft: theme.spacing(3), + paddingRight: theme.spacing(3) + } + }), + { + name: "SearchBar" + } +); + const SearchBar: React.FC = props => { const { allTabLabel, @@ -23,9 +44,16 @@ const SearchBar: React.FC = props => { onTabDelete, onTabSave } = props; + + const classes = useStyles(props); const intl = useIntl(); const isCustom = currentTab === tabs.length + 1; + const displayTabAction = isCustom + ? "save" + : currentTab === 0 + ? null + : "delete"; return ( <> @@ -47,16 +75,39 @@ const SearchBar: React.FC = props => { /> )} - +
+ + {displayTabAction && + (displayTabAction === "save" ? ( + + ) : ( + displayTabAction === "delete" && ( + + ) + ))} +
); }; diff --git a/src/components/SearchBar/SearchInput.tsx b/src/components/SearchBar/SearchInput.tsx new file mode 100644 index 000000000..5d8b38e3e --- /dev/null +++ b/src/components/SearchBar/SearchInput.tsx @@ -0,0 +1,59 @@ +import { makeStyles } from "@material-ui/core/styles"; +import React from "react"; +import TextField from "@material-ui/core/TextField"; + +import { SearchPageProps } from "../../types"; +import Debounce from "../Debounce"; + +export interface SearchInputProps extends SearchPageProps { + placeholder: string; +} + +const useStyles = makeStyles( + { + input: { + padding: "10.5px 12px" + }, + root: { + flex: 1 + } + }, + { + name: "SearchInput" + } +); + +const SearchInput: React.FC = props => { + const { initialSearch, onSearchChange, placeholder } = props; + + const classes = useStyles(props); + const [search, setSearch] = React.useState(initialSearch); + React.useEffect(() => setSearch(initialSearch), [initialSearch]); + + return ( + + {debounceSearchChange => { + const handleSearchChange = (event: React.ChangeEvent) => { + const value = event.target.value; + setSearch(value); + debounceSearchChange(value); + }; + + return ( + + ); + }} + + ); +}; + +SearchInput.displayName = "SearchInput"; +export default SearchInput; diff --git a/src/components/SingleAutocompleteSelectField/SingleAutocompleteSelectField.stories.tsx b/src/components/SingleAutocompleteSelectField/SingleAutocompleteSelectField.stories.tsx index 1d8468f30..ff1c03143 100644 --- a/src/components/SingleAutocompleteSelectField/SingleAutocompleteSelectField.stories.tsx +++ b/src/components/SingleAutocompleteSelectField/SingleAutocompleteSelectField.stories.tsx @@ -27,20 +27,18 @@ const props: SingleAutocompleteSelectFieldProps = { value: suggestions[0].value }; -const Story: React.FC< - Partial< - SingleAutocompleteSelectFieldProps & { - enableLoadMore: boolean; - } - > -> = ({ allowCustomValues, emptyOption, enableLoadMore }) => { +const Story: React.FC> = ({ allowCustomValues, emptyOption, enableLoadMore }) => { const [displayValue, setDisplayValue] = React.useState(suggestions[0].label); return (
{({ change, data }) => ( - {({ choices, fetchChoices, fetchMore, hasMore, loading }) => { + {({ choices, fetchChoices, onFetchMore, hasMore, loading }) => { const handleSelect = createSingleAutocompleteSelectHandler( change, setDisplayValue, @@ -58,7 +56,7 @@ const Story: React.FC< onChange={handleSelect} value={data.country} hasMore={enableLoadMore ? hasMore : false} - onFetchMore={enableLoadMore ? fetchMore : undefined} + onFetchMore={enableLoadMore ? onFetchMore : undefined} allowCustomValues={allowCustomValues} emptyOption={emptyOption} /> diff --git a/src/components/SingleAutocompleteSelectField/SingleAutocompleteSelectField.tsx b/src/components/SingleAutocompleteSelectField/SingleAutocompleteSelectField.tsx index 902103756..48b6e0692 100644 --- a/src/components/SingleAutocompleteSelectField/SingleAutocompleteSelectField.tsx +++ b/src/components/SingleAutocompleteSelectField/SingleAutocompleteSelectField.tsx @@ -41,17 +41,14 @@ export interface SingleAutocompleteSelectFieldProps onChange: (event: React.ChangeEvent) => void; } -const DebounceAutocomplete: React.ComponentType< - DebounceProps -> = Debounce; +const DebounceAutocomplete: React.ComponentType> = Debounce; -const SingleAutocompleteSelectFieldComponent: React.FC< - SingleAutocompleteSelectFieldProps -> = props => { +const SingleAutocompleteSelectFieldComponent: React.FC = props => { const { - choices, - allowCustomValues, + choices, disabled, displayValue, emptyOption, @@ -169,9 +166,11 @@ const SingleAutocompleteSelectFieldComponent: React.FC< ); }; -const SingleAutocompleteSelectField: React.FC< - SingleAutocompleteSelectFieldProps -> = ({ choices, fetchChoices, ...rest }) => { +const SingleAutocompleteSelectField: React.FC = ({ + choices, + fetchChoices, + ...rest +}) => { const [query, setQuery] = React.useState(""); if (fetchChoices) { return ( diff --git a/src/components/SingleSelectField/SingleSelectField.tsx b/src/components/SingleSelectField/SingleSelectField.tsx index 39810f834..87bd69de3 100644 --- a/src/components/SingleSelectField/SingleSelectField.tsx +++ b/src/components/SingleSelectField/SingleSelectField.tsx @@ -8,6 +8,7 @@ import { makeStyles } from "@material-ui/core/styles"; import classNames from "classnames"; import React from "react"; import { FormattedMessage } from "react-intl"; +import { InputProps } from "@material-ui/core/Input"; const useStyles = makeStyles( theme => ({ @@ -38,13 +39,13 @@ interface SingleSelectFieldProps { selectProps?: SelectProps; placeholder?: string; value?: string; + InputProps?: InputProps; onChange(event: any); } export const SingleSelectField: React.FC = props => { const { className, - disabled, error, label, @@ -54,7 +55,8 @@ export const SingleSelectField: React.FC = props => { name, hint, selectProps, - placeholder + placeholder, + InputProps } = props; const classes = useStyles(props); @@ -90,6 +92,7 @@ export const SingleSelectField: React.FC = props => { }} name={name} labelWidth={180} + {...InputProps} /> } {...selectProps} diff --git a/src/components/TableFilter/FilterChips.tsx b/src/components/TableFilter/FilterChips.tsx deleted file mode 100644 index c71d71984..000000000 --- a/src/components/TableFilter/FilterChips.tsx +++ /dev/null @@ -1,156 +0,0 @@ -import ButtonBase from "@material-ui/core/ButtonBase"; -import { makeStyles, useTheme } from "@material-ui/core/styles"; -import { fade } from "@material-ui/core/styles/colorManipulator"; -import Typography from "@material-ui/core/Typography"; -import ClearIcon from "@material-ui/icons/Clear"; -import React from "react"; -import { FormattedMessage } from "react-intl"; - -import FilterActions, { FilterActionsProps } from "../Filter/FilterActions"; -import Hr from "../Hr"; -import Link from "../Link"; - -export interface Filter { - label: string; - onClick: () => void; -} - -const useStyles = makeStyles( - theme => ({ - filterButton: { - alignItems: "center", - backgroundColor: fade(theme.palette.primary.main, 0.8), - borderRadius: "19px", - display: "flex", - height: "38px", - justifyContent: "space-around", - margin: theme.spacing(0, 1, 2), - marginLeft: 0, - padding: theme.spacing(0, 2) - }, - filterChipContainer: { - display: "flex", - flex: 1, - flexWrap: "wrap", - paddingTop: theme.spacing(2) - }, - filterContainer: { - "& a": { - paddingBottom: 10, - paddingTop: theme.spacing(1) - }, - borderBottom: `1px solid ${theme.palette.divider}`, - display: "flex", - marginTop: -theme.spacing(1), - padding: theme.spacing(0, 2) - }, - filterIcon: { - color: theme.palette.common.white, - height: 16, - width: 16 - }, - filterIconContainer: { - WebkitAppearance: "none", - background: "transparent", - border: "none", - borderRadius: "100%", - cursor: "pointer", - height: 32, - marginRight: -13, - padding: 8, - width: 32 - }, - filterLabel: { - marginBottom: theme.spacing(1) - }, - filterText: { - color: theme.palette.common.white, - fontSize: 14, - fontWeight: 400 as 400, - lineHeight: "38px" - } - }), - { - name: "FilterChips" - } -); - -interface FilterChipProps extends FilterActionsProps { - displayTabAction: "save" | "delete" | null; - filtersList: Filter[]; - search: string; - isCustomSearch: boolean; - onFilterDelete: () => void; - onFilterSave: () => void; -} - -export const FilterChips: React.FC = ({ - currencySymbol, - displayTabAction, - filtersList, - menu, - filterLabel, - placeholder, - onSearchChange, - search, - onFilterAdd, - onFilterSave, - onFilterDelete -}) => { - const theme = useTheme(); - const classes = useStyles({ theme }); - - return ( - <> - - {search || (filtersList && filtersList.length > 0) ? ( -
-
- {filtersList.map(filter => ( -
- - {filter.label} - - - - -
- ))} -
- {displayTabAction === "save" ? ( - - - - ) : ( - displayTabAction === "delete" && ( - - - - ) - )} -
- ) : ( -
- )} - - ); -}; - -export default FilterChips; diff --git a/src/components/TableFilter/index.ts b/src/components/TableFilter/index.ts index 5cb8830ab..c1bffcf95 100644 --- a/src/components/TableFilter/index.ts +++ b/src/components/TableFilter/index.ts @@ -1,4 +1,3 @@ export { default } from "./FilterTabs"; export * from "./FilterTabs"; export * from "./FilterTab"; -export * from "./FilterChips"; diff --git a/src/customers/components/CustomerListPage/CustomerListPage.tsx b/src/customers/components/CustomerListPage/CustomerListPage.tsx index 772fa7e33..2c45b6f05 100644 --- a/src/customers/components/CustomerListPage/CustomerListPage.tsx +++ b/src/customers/components/CustomerListPage/CustomerListPage.tsx @@ -5,33 +5,41 @@ import { FormattedMessage, useIntl } from "react-intl"; import Container from "@saleor/components/Container"; import PageHeader from "@saleor/components/PageHeader"; -import SearchBar from "@saleor/components/SearchBar"; import { sectionNames } from "@saleor/intl"; import { ListActions, PageListProps, - SearchPageProps, TabPageProps, - SortPage + SortPage, + FilterPageProps } from "@saleor/types"; import { CustomerListUrlSortField } from "@saleor/customers/urls"; -import { ListCustomers_customers_edges_node } from "../../types/ListCustomers"; +import FilterBar from "@saleor/components/FilterBar"; import CustomerList from "../CustomerList/CustomerList"; +import { ListCustomers_customers_edges_node } from "../../types/ListCustomers"; +import { + CustomerFilterKeys, + CustomerListFilterOpts, + createFilterStructure +} from "./filters"; export interface CustomerListPageProps extends PageListProps, ListActions, - SearchPageProps, + FilterPageProps, SortPage, TabPageProps { customers: ListCustomers_customers_edges_node[]; } const CustomerListPage: React.FC = ({ + currencySymbol, currentTab, + filterOpts, initialSearch, onAdd, onAll, + onFilterChange, onSearchChange, onTabChange, onTabDelete, @@ -41,6 +49,8 @@ const CustomerListPage: React.FC = ({ }) => { const intl = useIntl(); + const structure = createFilterStructure(intl, filterOpts); + return ( @@ -52,18 +62,21 @@ const CustomerListPage: React.FC = ({ - ; + moneySpent: FilterOpts; + numberOfOrders: FilterOpts; +} + +const messages = defineMessages({ + joinDate: { + defaultMessage: "Join Date", + description: "customer" + }, + moneySpent: { + defaultMessage: "Money Spent", + description: "customer" + }, + numberOfOrders: { + defaultMessage: "Number of Orders" + } +}); + +export function createFilterStructure( + intl: IntlShape, + opts: CustomerListFilterOpts +): IFilter { + return [ + { + ...createDateField( + CustomerFilterKeys.joined, + intl.formatMessage(messages.joinDate), + opts.joined.value + ), + active: opts.joined.active + }, + { + ...createNumberField( + CustomerFilterKeys.moneySpent, + intl.formatMessage(messages.moneySpent), + opts.moneySpent.value + ), + active: opts.moneySpent.active + }, + { + ...createNumberField( + CustomerFilterKeys.numberOfOrders, + intl.formatMessage(messages.numberOfOrders), + opts.numberOfOrders.value + ), + active: opts.numberOfOrders.active + } + ]; +} diff --git a/src/customers/components/CustomerListPage/index.ts b/src/customers/components/CustomerListPage/index.ts index 59481bef5..fe790fd4f 100644 --- a/src/customers/components/CustomerListPage/index.ts +++ b/src/customers/components/CustomerListPage/index.ts @@ -1,2 +1,3 @@ export { default } from "./CustomerListPage"; export * from "./CustomerListPage"; +export * from "./filters"; diff --git a/src/customers/urls.ts b/src/customers/urls.ts index ebfb188fd..819672388 100644 --- a/src/customers/urls.ts +++ b/src/customers/urls.ts @@ -16,6 +16,12 @@ export const customerSection = "/customers/"; export const customerListPath = customerSection; export enum CustomerListUrlFiltersEnum { + joinedFrom = "joinedFrom", + joinedTo = "joinedTo", + moneySpentFrom = "moneySpentFrom", + moneySpentTo = "moneySpentTo", + numberOfOrdersFrom = "numberOfOrdersFrom", + numberOfOrdersTo = "numberOfOrdersTo", query = "query" } export type CustomerListUrlFilters = Filters; diff --git a/src/customers/views/CustomerList/CustomerList.tsx b/src/customers/views/CustomerList/CustomerList.tsx index 92a28a9d0..7a5161949 100644 --- a/src/customers/views/CustomerList/CustomerList.tsx +++ b/src/customers/views/CustomerList/CustomerList.tsx @@ -22,6 +22,8 @@ import { ListViews } from "@saleor/types"; import { getSortParams } from "@saleor/utils/sort"; import createSortHandler from "@saleor/utils/handlers/sortHandler"; import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers"; +import useShop from "@saleor/hooks/useShop"; +import createFilterHandlers from "@saleor/utils/handlers/filterHandlers"; import CustomerListPage from "../../components/CustomerListPage"; import { TypedBulkRemoveCustomers } from "../../mutations"; import { useCustomerListQuery } from "../../queries"; @@ -29,7 +31,6 @@ import { BulkRemoveCustomers } from "../../types/BulkRemoveCustomers"; import { customerAddUrl, customerListUrl, - CustomerListUrlFilters, CustomerListUrlQueryParams, customerUrl, CustomerListUrlDialog @@ -40,8 +41,10 @@ import { getActiveFilters, getFilterTabs, getFilterVariables, - saveFilterTab -} from "./filter"; + saveFilterTab, + getFilterQueryParam, + getFilterOpts +} from "./filters"; import { getSortQueryVariables } from "./sort"; interface CustomerListProps { @@ -59,6 +62,7 @@ export const CustomerList: React.FC = ({ params }) => { ListViews.CUSTOMER_LIST ); const intl = useIntl(); + const shop = useShop(); const paginationState = createPaginationState(settings.rowNumber, params); const queryVariables = React.useMemo( @@ -83,16 +87,17 @@ export const CustomerList: React.FC = ({ params }) => { : 0 : parseInt(params.activeTab, 0); - const changeFilterField = (filter: CustomerListUrlFilters) => { - reset(); - navigate( - customerListUrl({ - ...getActiveFilters(params), - ...filter, - activeTab: undefined - }) - ); - }; + const [ + changeFilters, + resetFilters, + handleSearchChange + ] = createFilterHandlers({ + cleanupFn: reset, + createUrl: customerListUrl, + getFilterQueryParam, + navigate, + params + }); const [openModal, closeModal] = createDialogActionHandlers< CustomerListUrlDialog, @@ -138,16 +143,20 @@ export const CustomerList: React.FC = ({ params }) => { }; const handleSort = createSortHandler(navigate, customerListUrl, params); + const currencySymbol = maybe(() => shop.defaultCurrency, "USD"); return ( {(bulkRemoveCustomers, bulkRemoveCustomersOpts) => ( <> changeFilterField({ query })} - onAll={() => navigate(customerListUrl())} + onSearchChange={handleSearchChange} + onFilterChange={changeFilters} + onAll={resetFilters} onTabChange={handleTabChange} onTabDelete={() => openModal("delete-search")} onTabSave={() => openModal("save-search")} diff --git a/src/customers/views/CustomerList/__snapshots__/filters.test.ts.snap b/src/customers/views/CustomerList/__snapshots__/filters.test.ts.snap new file mode 100644 index 000000000..d9973216a --- /dev/null +++ b/src/customers/views/CustomerList/__snapshots__/filters.test.ts.snap @@ -0,0 +1,14 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Filtering URL params should not be empty if active filters are present 1`] = ` +Object { + "joinedFrom": "2019-12-09", + "joinedTo": "2019-12-38", + "moneySpentFrom": "2", + "moneySpentTo": "39.50", + "numberOfOrdersFrom": "1", + "numberOfOrdersTo": "5", +} +`; + +exports[`Filtering URL params should not be empty if active filters are present 2`] = `"joinedFrom=2019-12-09&joinedTo=2019-12-38&moneySpentFrom=2&moneySpentTo=39.50&numberOfOrdersFrom=1&numberOfOrdersTo=5"`; diff --git a/src/customers/views/CustomerList/filter.ts b/src/customers/views/CustomerList/filter.ts deleted file mode 100644 index 98e29cf77..000000000 --- a/src/customers/views/CustomerList/filter.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { CustomerFilterInput } from "@saleor/types/globalTypes"; -import { - createFilterTabUtils, - createFilterUtils -} from "../../../utils/filters"; -import { - CustomerListUrlFilters, - CustomerListUrlFiltersEnum, - CustomerListUrlQueryParams -} from "../../urls"; - -export const CUSTOMER_FILTERS_KEY = "customerFilters"; - -export function getFilterVariables( - params: CustomerListUrlFilters -): CustomerFilterInput { - return { - search: params.query - }; -} - -export const { - deleteFilterTab, - getFilterTabs, - saveFilterTab -} = createFilterTabUtils(CUSTOMER_FILTERS_KEY); - -export const { areFiltersApplied, getActiveFilters } = createFilterUtils< - CustomerListUrlQueryParams, - CustomerListUrlFilters ->(CustomerListUrlFiltersEnum); diff --git a/src/customers/views/CustomerList/filters.test.ts b/src/customers/views/CustomerList/filters.test.ts new file mode 100644 index 000000000..7f73e826a --- /dev/null +++ b/src/customers/views/CustomerList/filters.test.ts @@ -0,0 +1,78 @@ +import { createIntl } from "react-intl"; +import { stringify as stringifyQs } from "qs"; + +import { CustomerListUrlFilters } from "@saleor/customers/urls"; +import { createFilterStructure } from "@saleor/customers/components/CustomerListPage"; +import { getFilterQueryParams } from "@saleor/utils/filters"; +import { date } from "@saleor/fixtures"; +import { getExistingKeys, setFilterOptsStatus } from "@test/filters"; +import { config } from "@test/intl"; +import { getFilterVariables, getFilterQueryParam } from "./filters"; + +describe("Filtering query params", () => { + it("should be empty object if no params given", () => { + const params: CustomerListUrlFilters = {}; + const filterVariables = getFilterVariables(params); + + expect(getExistingKeys(filterVariables)).toHaveLength(0); + }); + + it("should not be empty object if params given", () => { + const params: CustomerListUrlFilters = { + joinedFrom: date.from, + moneySpentFrom: "2", + moneySpentTo: "39.50", + numberOfOrdersTo: "5" + }; + const filterVariables = getFilterVariables(params); + + expect(getExistingKeys(filterVariables)).toHaveLength(3); + }); +}); + +describe("Filtering URL params", () => { + const intl = createIntl(config); + + const filters = createFilterStructure(intl, { + joined: { + active: false, + value: { + max: date.to, + min: date.from + } + }, + moneySpent: { + active: false, + value: { + max: "39.50", + min: "2" + } + }, + numberOfOrders: { + active: false, + value: { + max: "5", + min: "1" + } + } + }); + + it("should be empty if no active filters", () => { + const filterQueryParams = getFilterQueryParams( + filters, + getFilterQueryParam + ); + + expect(getExistingKeys(filterQueryParams)).toHaveLength(0); + }); + + it("should not be empty if active filters are present", () => { + const filterQueryParams = getFilterQueryParams( + setFilterOptsStatus(filters, true), + getFilterQueryParam + ); + + expect(filterQueryParams).toMatchSnapshot(); + expect(stringifyQs(filterQueryParams)).toMatchSnapshot(); + }); +}); diff --git a/src/customers/views/CustomerList/filters.ts b/src/customers/views/CustomerList/filters.ts new file mode 100644 index 000000000..fb72e2d0b --- /dev/null +++ b/src/customers/views/CustomerList/filters.ts @@ -0,0 +1,126 @@ +import { CustomerFilterInput } from "@saleor/types/globalTypes"; +import { maybe } from "@saleor/misc"; +import { IFilterElement } from "@saleor/components/Filter"; +import { + CustomerFilterKeys, + CustomerListFilterOpts +} from "@saleor/customers/components/CustomerListPage"; +import { + createFilterTabUtils, + createFilterUtils, + getGteLteVariables, + getMinMaxQueryParam +} from "../../../utils/filters"; +import { + CustomerListUrlFilters, + CustomerListUrlFiltersEnum, + CustomerListUrlQueryParams +} from "../../urls"; + +export const CUSTOMER_FILTERS_KEY = "customerFilters"; + +export function getFilterOpts( + params: CustomerListUrlFilters +): CustomerListFilterOpts { + return { + joined: { + active: maybe( + () => + [params.joinedFrom, params.joinedTo].some( + field => field !== undefined + ), + false + ), + value: { + max: maybe(() => params.joinedTo, ""), + min: maybe(() => params.joinedFrom, "") + } + }, + moneySpent: { + active: maybe( + () => + [params.moneySpentFrom, params.moneySpentTo].some( + field => field !== undefined + ), + false + ), + value: { + max: maybe(() => params.moneySpentTo, ""), + min: maybe(() => params.moneySpentFrom, "") + } + }, + numberOfOrders: { + active: maybe( + () => + [params.numberOfOrdersFrom, params.numberOfOrdersTo].some( + field => field !== undefined + ), + false + ), + value: { + max: maybe(() => params.numberOfOrdersTo, ""), + min: maybe(() => params.numberOfOrdersFrom, "") + } + } + }; +} + +export function getFilterVariables( + params: CustomerListUrlFilters +): CustomerFilterInput { + return { + dateJoined: getGteLteVariables({ + gte: params.joinedFrom, + lte: params.joinedTo + }), + moneySpent: getGteLteVariables({ + gte: parseInt(params.moneySpentFrom, 0), + lte: parseInt(params.moneySpentTo, 0) + }), + numberOfOrders: getGteLteVariables({ + gte: parseInt(params.numberOfOrdersFrom, 0), + lte: parseInt(params.numberOfOrdersTo, 0) + }), + search: params.query + }; +} + +export function getFilterQueryParam( + filter: IFilterElement +): CustomerListUrlFilters { + const { name } = filter; + + switch (name) { + case CustomerFilterKeys.joined: + return getMinMaxQueryParam( + filter, + CustomerListUrlFiltersEnum.joinedFrom, + CustomerListUrlFiltersEnum.joinedTo + ); + + case CustomerFilterKeys.moneySpent: + return getMinMaxQueryParam( + filter, + CustomerListUrlFiltersEnum.moneySpentFrom, + CustomerListUrlFiltersEnum.moneySpentTo + ); + + case CustomerFilterKeys.numberOfOrders: + return getMinMaxQueryParam( + filter, + CustomerListUrlFiltersEnum.numberOfOrdersFrom, + CustomerListUrlFiltersEnum.numberOfOrdersTo + ); + } +} + +export const { + deleteFilterTab, + getFilterTabs, + saveFilterTab +} = createFilterTabUtils(CUSTOMER_FILTERS_KEY); + +export const { areFiltersApplied, getActiveFilters } = createFilterUtils< + CustomerListUrlQueryParams, + CustomerListUrlFilters +>(CustomerListUrlFiltersEnum); diff --git a/src/discounts/components/SaleListPage/SaleListPage.tsx b/src/discounts/components/SaleListPage/SaleListPage.tsx index f7f5754fd..b6c5ef9bd 100644 --- a/src/discounts/components/SaleListPage/SaleListPage.tsx +++ b/src/discounts/components/SaleListPage/SaleListPage.tsx @@ -5,23 +5,28 @@ import { FormattedMessage, useIntl } from "react-intl"; import Container from "@saleor/components/Container"; import PageHeader from "@saleor/components/PageHeader"; -import SearchBar from "@saleor/components/SearchBar"; +import FilterBar from "@saleor/components/FilterBar"; import { sectionNames } from "@saleor/intl"; import { ListActions, PageListProps, - SearchPageProps, TabPageProps, - SortPage + SortPage, + FilterPageProps } from "@saleor/types"; import { SaleListUrlSortField } from "@saleor/discounts/urls"; import { SaleList_sales_edges_node } from "../../types/SaleList"; import SaleList from "../SaleList"; +import { + SaleFilterKeys, + SaleListFilterOpts, + createFilterStructure +} from "./filters"; export interface SaleListPageProps extends PageListProps, ListActions, - SearchPageProps, + FilterPageProps, SortPage, TabPageProps { defaultCurrency: string; @@ -29,10 +34,13 @@ export interface SaleListPageProps } const SaleListPage: React.FC = ({ + currencySymbol, currentTab, + filterOpts, initialSearch, onAdd, onAll, + onFilterChange, onSearchChange, onTabChange, onTabDelete, @@ -42,6 +50,8 @@ const SaleListPage: React.FC = ({ }) => { const intl = useIntl(); + const structure = createFilterStructure(intl, filterOpts); + return ( @@ -50,18 +60,21 @@ const SaleListPage: React.FC = ({ - ; + started: FilterOpts; + status: FilterOpts; +} + +const messages = defineMessages({ + active: { + defaultMessage: "Active", + description: "sale status" + }, + expired: { + defaultMessage: "Expired", + description: "sale status" + }, + fixed: { + defaultMessage: "Fixed amount", + description: "discount type" + }, + percentage: { + defaultMessage: "Percentage", + description: "discount type" + }, + scheduled: { + defaultMessage: "Scheduled", + description: "sale status" + }, + started: { + defaultMessage: "Started", + description: "sale start date" + }, + status: { + defaultMessage: "Status", + description: "sale status" + }, + type: { + defaultMessage: "Discount Type" + } +}); + +export function createFilterStructure( + intl: IntlShape, + opts: SaleListFilterOpts +): IFilter { + return [ + { + ...createDateField( + SaleFilterKeys.started, + intl.formatMessage(messages.started), + opts.started.value + ), + active: opts.started.active + }, + { + ...createOptionsField( + SaleFilterKeys.status, + intl.formatMessage(messages.status), + opts.status.value, + true, + [ + { + label: intl.formatMessage(messages.active), + value: DiscountStatusEnum.ACTIVE + }, + { + label: intl.formatMessage(messages.expired), + value: DiscountStatusEnum.EXPIRED + }, + { + label: intl.formatMessage(messages.scheduled), + value: DiscountStatusEnum.SCHEDULED + } + ] + ), + active: opts.status.active + }, + { + ...createOptionsField( + SaleFilterKeys.saleType, + intl.formatMessage(messages.type), + [opts.saleType.value], + false, + [ + { + label: intl.formatMessage(messages.fixed), + value: DiscountValueTypeEnum.FIXED + }, + { + label: intl.formatMessage(messages.percentage), + value: DiscountValueTypeEnum.PERCENTAGE + } + ] + ), + active: opts.saleType.active + } + ]; +} diff --git a/src/discounts/components/SaleListPage/index.ts b/src/discounts/components/SaleListPage/index.ts index 59dd2585a..adb62f183 100644 --- a/src/discounts/components/SaleListPage/index.ts +++ b/src/discounts/components/SaleListPage/index.ts @@ -1,2 +1,3 @@ export { default } from "./SaleListPage"; export * from "./SaleListPage"; +export * from "./filters"; diff --git a/src/discounts/components/VoucherListPage/VoucherListPage.tsx b/src/discounts/components/VoucherListPage/VoucherListPage.tsx index bf7cc6079..91dbf982d 100644 --- a/src/discounts/components/VoucherListPage/VoucherListPage.tsx +++ b/src/discounts/components/VoucherListPage/VoucherListPage.tsx @@ -5,23 +5,28 @@ import { FormattedMessage, useIntl } from "react-intl"; import Container from "@saleor/components/Container"; import PageHeader from "@saleor/components/PageHeader"; -import SearchBar from "@saleor/components/SearchBar"; +import FilterBar from "@saleor/components/FilterBar"; import { sectionNames } from "@saleor/intl"; import { ListActions, PageListProps, - SearchPageProps, TabPageProps, - SortPage + SortPage, + FilterPageProps } from "@saleor/types"; import { VoucherListUrlSortField } from "@saleor/discounts/urls"; import { VoucherList_vouchers_edges_node } from "../../types/VoucherList"; import VoucherList from "../VoucherList"; +import { + VoucherFilterKeys, + VoucherListFilterOpts, + createFilterStructure +} from "./filters"; export interface VoucherListPageProps extends PageListProps, ListActions, - SearchPageProps, + FilterPageProps, SortPage, TabPageProps { defaultCurrency: string; @@ -29,10 +34,13 @@ export interface VoucherListPageProps } const VoucherListPage: React.FC = ({ + currencySymbol, currentTab, + filterOpts, initialSearch, onAdd, onAll, + onFilterChange, onSearchChange, onTabChange, onTabDelete, @@ -42,6 +50,8 @@ const VoucherListPage: React.FC = ({ }) => { const intl = useIntl(); + const structure = createFilterStructure(intl, filterOpts); + return ( @@ -53,18 +63,21 @@ const VoucherListPage: React.FC = ({ - ; + started: FilterOpts; + status: FilterOpts; + timesUsed: FilterOpts; +} + +const messages = defineMessages({ + active: { + defaultMessage: "Active", + description: "voucher status" + }, + expired: { + defaultMessage: "Expired", + description: "voucher status" + }, + fixed: { + defaultMessage: "Fixed amount", + description: "discount type" + }, + percentage: { + defaultMessage: "Percentage", + description: "discount type" + }, + scheduled: { + defaultMessage: "Scheduled", + description: "voucher status" + }, + started: { + defaultMessage: "Started", + description: "voucher start date" + }, + status: { + defaultMessage: "Status", + description: "voucher status" + }, + timesUsed: { + defaultMessage: "Times used", + description: "voucher" + }, + type: { + defaultMessage: "Discount Type" + } +}); + +export function createFilterStructure( + intl: IntlShape, + opts: VoucherListFilterOpts +): IFilter { + return [ + { + ...createDateField( + VoucherFilterKeys.started, + intl.formatMessage(messages.started), + opts.started.value + ), + active: opts.started.active + }, + { + ...createNumberField( + VoucherFilterKeys.timesUsed, + intl.formatMessage(messages.timesUsed), + opts.timesUsed.value + ), + active: opts.timesUsed.active + }, + { + ...createOptionsField( + VoucherFilterKeys.status, + intl.formatMessage(messages.status), + opts.status.value, + true, + [ + { + label: intl.formatMessage(messages.active), + value: DiscountStatusEnum.ACTIVE + }, + { + label: intl.formatMessage(messages.expired), + value: DiscountStatusEnum.EXPIRED + }, + { + label: intl.formatMessage(messages.scheduled), + value: DiscountStatusEnum.SCHEDULED + } + ] + ), + active: opts.status.active + }, + { + ...createOptionsField( + VoucherFilterKeys.saleType, + intl.formatMessage(messages.type), + opts.saleType.value, + false, + [ + { + label: intl.formatMessage(messages.fixed), + value: VoucherDiscountType.FIXED + }, + { + label: intl.formatMessage(messages.percentage), + value: VoucherDiscountType.PERCENTAGE + }, + { + label: intl.formatMessage(messages.percentage), + value: VoucherDiscountType.SHIPPING + } + ] + ), + active: opts.saleType.active + } + ]; +} diff --git a/src/discounts/components/VoucherListPage/index.ts b/src/discounts/components/VoucherListPage/index.ts index 84b3fa702..6736b0f16 100644 --- a/src/discounts/components/VoucherListPage/index.ts +++ b/src/discounts/components/VoucherListPage/index.ts @@ -1,2 +1,3 @@ export { default } from "./VoucherListPage"; export * from "./VoucherListPage"; +export * from "./filters"; diff --git a/src/discounts/urls.ts b/src/discounts/urls.ts index efdd37f9c..186e9a722 100644 --- a/src/discounts/urls.ts +++ b/src/discounts/urls.ts @@ -8,7 +8,8 @@ import { Filters, Pagination, TabActionDialog, - Sort + Sort, + FiltersWithMultipleValues } from "../types"; import { SaleDetailsPageTab } from "./components/SaleDetailsPage"; import { VoucherDetailsPageTab } from "./components/VoucherDetailsPage"; @@ -18,9 +19,16 @@ export const discountSection = "/discounts/"; export const saleSection = urlJoin(discountSection, "sales"); export const saleListPath = saleSection; export enum SaleListUrlFiltersEnum { + type = "type", + startedFrom = "startedFrom", + startedTo = "startedTo", query = "query" } -export type SaleListUrlFilters = Filters; +export enum SaleListUrlFiltersWithMultipleValues { + status = "status" +} +export type SaleListUrlFilters = Filters & + FiltersWithMultipleValues; export type SaleListUrlDialog = "remove" | TabActionDialog; export enum SaleListUrlSortField { name = "name", @@ -59,9 +67,18 @@ export const saleAddUrl = saleAddPath; export const voucherSection = urlJoin(discountSection, "vouchers"); export const voucherListPath = voucherSection; export enum VoucherListUrlFiltersEnum { + startedFrom = "startedFrom", + startedTo = "startedTo", + timesUsedFrom = "timesUsedFrom", + timesUsedTo = "timesUsedTo", query = "query" } -export type VoucherListUrlFilters = Filters; +export enum VoucherListUrlFiltersWithMultipleValues { + status = "status", + type = "type" +} +export type VoucherListUrlFilters = Filters & + FiltersWithMultipleValues; export type VoucherListUrlDialog = "remove" | TabActionDialog; export enum VoucherListUrlSortField { code = "code", diff --git a/src/discounts/views/SaleList/SaleList.tsx b/src/discounts/views/SaleList/SaleList.tsx index b068377ec..e17631d28 100644 --- a/src/discounts/views/SaleList/SaleList.tsx +++ b/src/discounts/views/SaleList/SaleList.tsx @@ -24,13 +24,13 @@ import { ListViews } from "@saleor/types"; import { getSortParams } from "@saleor/utils/sort"; import createSortHandler from "@saleor/utils/handlers/sortHandler"; import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers"; +import createFilterHandlers from "@saleor/utils/handlers/filterHandlers"; import SaleListPage from "../../components/SaleListPage"; import { TypedSaleBulkDelete } from "../../mutations"; import { useSaleListQuery } from "../../queries"; import { SaleBulkDelete } from "../../types/SaleBulkDelete"; import { saleAddUrl, - SaleListUrlFilters, SaleListUrlQueryParams, saleUrl, saleListUrl, @@ -42,8 +42,10 @@ import { getActiveFilters, getFilterTabs, getFilterVariables, - saveFilterTab -} from "./filter"; + saveFilterTab, + getFilterQueryParam, + getFilterOpts +} from "./filters"; import { getSortQueryVariables } from "./sort"; interface SaleListProps { @@ -86,16 +88,17 @@ export const SaleList: React.FC = ({ params }) => { : 0 : parseInt(params.activeTab, 0); - const changeFilterField = (filter: SaleListUrlFilters) => { - reset(); - navigate( - saleListUrl({ - ...getActiveFilters(params), - ...filter, - activeTab: undefined - }) - ); - }; + const [ + changeFilters, + resetFilters, + handleSearchChange + ] = createFilterHandlers({ + cleanupFn: reset, + createUrl: saleListUrl, + getFilterQueryParam, + navigate, + params + }); const [openModal, closeModal] = createDialogActionHandlers< SaleListUrlDialog, @@ -143,6 +146,7 @@ export const SaleList: React.FC = ({ params }) => { }; const handleSort = createSortHandler(navigate, saleListUrl, params); + const currencySymbol = maybe(() => shop.defaultCurrency, "USD"); return ( @@ -158,10 +162,13 @@ export const SaleList: React.FC = ({ params }) => { <> changeFilterField({ query })} - onAll={() => navigate(saleListUrl())} + onSearchChange={handleSearchChange} + onFilterChange={filter => changeFilters(filter)} + onAll={resetFilters} onTabChange={handleTabChange} onTabDelete={() => openModal("delete-search")} onTabSave={() => openModal("save-search")} diff --git a/src/discounts/views/SaleList/__snapshots__/filters.test.ts.snap b/src/discounts/views/SaleList/__snapshots__/filters.test.ts.snap new file mode 100644 index 000000000..13faf2f2d --- /dev/null +++ b/src/discounts/views/SaleList/__snapshots__/filters.test.ts.snap @@ -0,0 +1,15 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Filtering URL params should not be empty if active filters are present 1`] = ` +Object { + "startedFrom": "2019-12-09", + "startedTo": "2019-12-38", + "status": Array [ + "ACTIVE", + "EXPIRED", + ], + "type": "FIXED", +} +`; + +exports[`Filtering URL params should not be empty if active filters are present 2`] = `"startedFrom=2019-12-09&startedTo=2019-12-38&status%5B0%5D=ACTIVE&status%5B1%5D=EXPIRED&type=FIXED"`; diff --git a/src/discounts/views/SaleList/filter.ts b/src/discounts/views/SaleList/filter.ts deleted file mode 100644 index 69362c3d3..000000000 --- a/src/discounts/views/SaleList/filter.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { SaleFilterInput } from "@saleor/types/globalTypes"; -import { - createFilterTabUtils, - createFilterUtils -} from "../../../utils/filters"; -import { - SaleListUrlFilters, - SaleListUrlFiltersEnum, - SaleListUrlQueryParams -} from "../../urls"; - -export const SALE_FILTERS_KEY = "saleFilters"; - -export function getFilterVariables( - params: SaleListUrlFilters -): SaleFilterInput { - return { - search: params.query - }; -} - -export const { - deleteFilterTab, - getFilterTabs, - saveFilterTab -} = createFilterTabUtils(SALE_FILTERS_KEY); - -export const { areFiltersApplied, getActiveFilters } = createFilterUtils< - SaleListUrlQueryParams, - SaleListUrlFilters ->(SaleListUrlFiltersEnum); diff --git a/src/discounts/views/SaleList/filters.test.ts b/src/discounts/views/SaleList/filters.test.ts new file mode 100644 index 000000000..f6d7f7743 --- /dev/null +++ b/src/discounts/views/SaleList/filters.test.ts @@ -0,0 +1,76 @@ +import { createIntl } from "react-intl"; +import { stringify as stringifyQs } from "qs"; + +import { SaleListUrlFilters } from "@saleor/discounts/urls"; +import { createFilterStructure } from "@saleor/discounts/components/SaleListPage"; +import { getFilterQueryParams } from "@saleor/utils/filters"; +import { date } from "@saleor/fixtures"; +import { getExistingKeys, setFilterOptsStatus } from "@test/filters"; +import { config } from "@test/intl"; +import { + DiscountValueTypeEnum, + DiscountStatusEnum +} from "@saleor/types/globalTypes"; +import { getFilterVariables, getFilterQueryParam } from "./filters"; + +describe("Filtering query params", () => { + it("should be empty object if no params given", () => { + const params: SaleListUrlFilters = {}; + const filterVariables = getFilterVariables(params); + + expect(getExistingKeys(filterVariables)).toHaveLength(0); + }); + + it("should not be empty object if params given", () => { + const params: SaleListUrlFilters = { + startedFrom: date.from, + startedTo: date.to, + status: [DiscountStatusEnum.ACTIVE, DiscountStatusEnum.EXPIRED], + type: DiscountValueTypeEnum.FIXED + }; + const filterVariables = getFilterVariables(params); + + expect(getExistingKeys(filterVariables)).toHaveLength(3); + }); +}); + +describe("Filtering URL params", () => { + const intl = createIntl(config); + + const filters = createFilterStructure(intl, { + saleType: { + active: false, + value: DiscountValueTypeEnum.FIXED + }, + started: { + active: false, + value: { + max: date.to, + min: date.from + } + }, + status: { + active: false, + value: [DiscountStatusEnum.ACTIVE, DiscountStatusEnum.EXPIRED] + } + }); + + it("should be empty if no active filters", () => { + const filterQueryParams = getFilterQueryParams( + filters, + getFilterQueryParam + ); + + expect(getExistingKeys(filterQueryParams)).toHaveLength(0); + }); + + it("should not be empty if active filters are present", () => { + const filterQueryParams = getFilterQueryParams( + setFilterOptsStatus(filters, true), + getFilterQueryParam + ); + + expect(filterQueryParams).toMatchSnapshot(); + expect(stringifyQs(filterQueryParams)).toMatchSnapshot(); + }); +}); diff --git a/src/discounts/views/SaleList/filters.ts b/src/discounts/views/SaleList/filters.ts new file mode 100644 index 000000000..d2683e58f --- /dev/null +++ b/src/discounts/views/SaleList/filters.ts @@ -0,0 +1,119 @@ +import { + DiscountStatusEnum, + DiscountValueTypeEnum, + SaleFilterInput +} from "@saleor/types/globalTypes"; +import { maybe, findValueInEnum, joinDateTime } from "@saleor/misc"; +import { IFilterElement } from "@saleor/components/Filter"; +import { + SaleListFilterOpts, + SaleFilterKeys +} from "@saleor/discounts/components/SaleListPage"; +import { + createFilterTabUtils, + createFilterUtils, + dedupeFilter, + getGteLteVariables, + getSingleEnumValueQueryParam, + getMinMaxQueryParam, + getMultipleEnumValueQueryParam +} from "../../../utils/filters"; +import { + SaleListUrlFilters, + SaleListUrlFiltersEnum, + SaleListUrlQueryParams, + SaleListUrlFiltersWithMultipleValues +} from "../../urls"; + +export const SALE_FILTERS_KEY = "saleFilters"; + +export function getFilterOpts(params: SaleListUrlFilters): SaleListFilterOpts { + return { + saleType: { + active: !!maybe(() => params.type), + value: maybe(() => findValueInEnum(params.type, DiscountValueTypeEnum)) + }, + started: { + active: maybe( + () => + [params.startedFrom, params.startedTo].some( + field => field !== undefined + ), + false + ), + value: { + max: maybe(() => params.startedTo, ""), + min: maybe(() => params.startedFrom, "") + } + }, + status: { + active: !!maybe(() => params.status), + value: maybe( + () => + dedupeFilter( + params.status.map(status => + findValueInEnum(status, DiscountStatusEnum) + ) + ), + [] + ) + } + }; +} + +export function getFilterVariables( + params: SaleListUrlFilters +): SaleFilterInput { + return { + saleType: + params.type && findValueInEnum(params.type, DiscountValueTypeEnum), + search: params.query, + started: getGteLteVariables({ + gte: joinDateTime(params.startedFrom), + lte: joinDateTime(params.startedTo) + }), + status: + params.status && + params.status.map(status => findValueInEnum(status, DiscountStatusEnum)) + }; +} + +export function getFilterQueryParam( + filter: IFilterElement +): SaleListUrlFilters { + const { name } = filter; + + switch (name) { + case SaleFilterKeys.saleType: + return getSingleEnumValueQueryParam( + filter, + SaleListUrlFiltersEnum.type, + DiscountValueTypeEnum + ); + + case SaleFilterKeys.started: + return getMinMaxQueryParam( + filter, + SaleListUrlFiltersEnum.startedFrom, + SaleListUrlFiltersEnum.startedTo + ); + + case SaleFilterKeys.status: + return getMultipleEnumValueQueryParam( + filter, + SaleListUrlFiltersWithMultipleValues.status, + DiscountStatusEnum + ); + } +} + +export const { + deleteFilterTab, + getFilterTabs, + saveFilterTab +} = createFilterTabUtils(SALE_FILTERS_KEY); + +export const { areFiltersApplied, getActiveFilters } = createFilterUtils< + SaleListUrlQueryParams, + SaleListUrlFilters +>(SaleListUrlFiltersEnum); diff --git a/src/discounts/views/VoucherList/VoucherList.tsx b/src/discounts/views/VoucherList/VoucherList.tsx index 522a3ac74..a517c45ac 100644 --- a/src/discounts/views/VoucherList/VoucherList.tsx +++ b/src/discounts/views/VoucherList/VoucherList.tsx @@ -24,6 +24,7 @@ import { ListViews } from "@saleor/types"; import { getSortParams } from "@saleor/utils/sort"; import createSortHandler from "@saleor/utils/handlers/sortHandler"; import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers"; +import createFilterHandlers from "@saleor/utils/handlers/filterHandlers"; import VoucherListPage from "../../components/VoucherListPage"; import { TypedVoucherBulkDelete } from "../../mutations"; import { useVoucherListQuery } from "../../queries"; @@ -31,7 +32,6 @@ import { VoucherBulkDelete } from "../../types/VoucherBulkDelete"; import { voucherAddUrl, voucherListUrl, - VoucherListUrlFilters, VoucherListUrlQueryParams, voucherUrl, VoucherListUrlDialog @@ -42,8 +42,10 @@ import { getActiveFilters, getFilterTabs, getFilterVariables, - saveFilterTab -} from "./filter"; + saveFilterTab, + getFilterQueryParam, + getFilterOpts +} from "./filters"; import { getSortQueryVariables } from "./sort"; interface VoucherListProps { @@ -86,16 +88,17 @@ export const VoucherList: React.FC = ({ params }) => { : 0 : parseInt(params.activeTab, 0); - const changeFilterField = (filter: VoucherListUrlFilters) => { - reset(); - navigate( - voucherListUrl({ - ...getActiveFilters(params), - ...filter, - activeTab: undefined - }) - ); - }; + const [ + changeFilters, + resetFilters, + handleSearchChange + ] = createFilterHandlers({ + cleanupFn: reset, + createUrl: voucherListUrl, + getFilterQueryParam, + navigate, + params + }); const [openModal, closeModal] = createDialogActionHandlers< VoucherListUrlDialog, @@ -143,6 +146,7 @@ export const VoucherList: React.FC = ({ params }) => { }; const handleSort = createSortHandler(navigate, voucherListUrl, params); + const currencySymbol = maybe(() => shop.defaultCurrency, "USD"); return ( @@ -158,10 +162,13 @@ export const VoucherList: React.FC = ({ params }) => { <> changeFilterField({ query })} - onAll={() => navigate(voucherListUrl())} + onSearchChange={handleSearchChange} + onFilterChange={filter => changeFilters(filter)} + onAll={resetFilters} onTabChange={handleTabChange} onTabDelete={() => openModal("delete-search")} onTabSave={() => openModal("save-search")} diff --git a/src/discounts/views/VoucherList/__snapshots__/filters.test.ts.snap b/src/discounts/views/VoucherList/__snapshots__/filters.test.ts.snap new file mode 100644 index 000000000..1926087dc --- /dev/null +++ b/src/discounts/views/VoucherList/__snapshots__/filters.test.ts.snap @@ -0,0 +1,20 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Filtering URL params should not be empty if active filters are present 1`] = ` +Object { + "startedFrom": "2019-12-09", + "startedTo": "2019-12-38", + "status": Array [ + "ACTIVE", + "EXPIRED", + ], + "timesUsedFrom": "1", + "timesUsedTo": "6", + "type": Array [ + "FIXED", + "SHIPPING", + ], +} +`; + +exports[`Filtering URL params should not be empty if active filters are present 2`] = `"startedFrom=2019-12-09&startedTo=2019-12-38×UsedFrom=1×UsedTo=6&status%5B0%5D=ACTIVE&status%5B1%5D=EXPIRED&type%5B0%5D=FIXED&type%5B1%5D=SHIPPING"`; diff --git a/src/discounts/views/VoucherList/filter.ts b/src/discounts/views/VoucherList/filter.ts deleted file mode 100644 index 6f5d7ce57..000000000 --- a/src/discounts/views/VoucherList/filter.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { VoucherFilterInput } from "@saleor/types/globalTypes"; -import { - createFilterTabUtils, - createFilterUtils -} from "../../../utils/filters"; -import { - VoucherListUrlFilters, - VoucherListUrlFiltersEnum, - VoucherListUrlQueryParams -} from "../../urls"; - -export const VOUCHER_FILTERS_KEY = "VoucherFilters"; - -export function getFilterVariables( - params: VoucherListUrlFilters -): VoucherFilterInput { - return { - search: params.query - }; -} - -export const { - deleteFilterTab, - getFilterTabs, - saveFilterTab -} = createFilterTabUtils(VOUCHER_FILTERS_KEY); - -export const { areFiltersApplied, getActiveFilters } = createFilterUtils< - VoucherListUrlQueryParams, - VoucherListUrlFilters ->(VoucherListUrlFiltersEnum); diff --git a/src/discounts/views/VoucherList/filters.test.ts b/src/discounts/views/VoucherList/filters.test.ts new file mode 100644 index 000000000..0000a4e6b --- /dev/null +++ b/src/discounts/views/VoucherList/filters.test.ts @@ -0,0 +1,85 @@ +import { createIntl } from "react-intl"; +import { stringify as stringifyQs } from "qs"; + +import { VoucherListUrlFilters } from "@saleor/discounts/urls"; +import { createFilterStructure } from "@saleor/discounts/components/VoucherListPage"; +import { getFilterQueryParams } from "@saleor/utils/filters"; +import { date } from "@saleor/fixtures"; +import { getExistingKeys, setFilterOptsStatus } from "@test/filters"; +import { config } from "@test/intl"; +import { + DiscountStatusEnum, + VoucherDiscountType +} from "@saleor/types/globalTypes"; +import { getFilterVariables, getFilterQueryParam } from "./filters"; + +describe("Filtering query params", () => { + it("should be empty object if no params given", () => { + const params: VoucherListUrlFilters = {}; + const filterVariables = getFilterVariables(params); + + expect(getExistingKeys(filterVariables)).toHaveLength(0); + }); + + it("should not be empty object if params given", () => { + const params: VoucherListUrlFilters = { + startedFrom: date.from, + startedTo: date.to, + status: [DiscountStatusEnum.ACTIVE, DiscountStatusEnum.EXPIRED], + timesUsedFrom: date.from, + timesUsedTo: date.to, + type: [VoucherDiscountType.FIXED, VoucherDiscountType.SHIPPING] + }; + const filterVariables = getFilterVariables(params); + + expect(getExistingKeys(filterVariables)).toHaveLength(4); + }); +}); + +describe("Filtering URL params", () => { + const intl = createIntl(config); + + const filters = createFilterStructure(intl, { + saleType: { + active: false, + value: [VoucherDiscountType.FIXED, VoucherDiscountType.SHIPPING] + }, + started: { + active: false, + value: { + max: date.to, + min: date.from + } + }, + status: { + active: false, + value: [DiscountStatusEnum.ACTIVE, DiscountStatusEnum.EXPIRED] + }, + timesUsed: { + active: false, + value: { + max: "6", + min: "1" + } + } + }); + + it("should be empty if no active filters", () => { + const filterQueryParams = getFilterQueryParams( + filters, + getFilterQueryParam + ); + + expect(getExistingKeys(filterQueryParams)).toHaveLength(0); + }); + + it("should not be empty if active filters are present", () => { + const filterQueryParams = getFilterQueryParams( + setFilterOptsStatus(filters, true), + getFilterQueryParam + ); + + expect(filterQueryParams).toMatchSnapshot(); + expect(stringifyQs(filterQueryParams)).toMatchSnapshot(); + }); +}); diff --git a/src/discounts/views/VoucherList/filters.ts b/src/discounts/views/VoucherList/filters.ts new file mode 100644 index 000000000..691ec9a1e --- /dev/null +++ b/src/discounts/views/VoucherList/filters.ts @@ -0,0 +1,151 @@ +import { + VoucherFilterInput, + DiscountStatusEnum, + VoucherDiscountType +} from "@saleor/types/globalTypes"; +import { maybe, findValueInEnum, joinDateTime } from "@saleor/misc"; +import { IFilterElement } from "@saleor/components/Filter"; +import { + VoucherListFilterOpts, + VoucherFilterKeys +} from "@saleor/discounts/components/VoucherListPage"; +import { + createFilterTabUtils, + createFilterUtils, + dedupeFilter, + getGteLteVariables, + getMultipleEnumValueQueryParam, + getMinMaxQueryParam +} from "../../../utils/filters"; +import { + VoucherListUrlFilters, + VoucherListUrlFiltersEnum, + VoucherListUrlQueryParams, + VoucherListUrlFiltersWithMultipleValues +} from "../../urls"; + +export const VOUCHER_FILTERS_KEY = "voucherFilters"; + +export function getFilterOpts( + params: VoucherListUrlFilters +): VoucherListFilterOpts { + return { + saleType: { + active: !!maybe(() => params.type), + value: maybe( + () => + dedupeFilter( + params.type.map(type => findValueInEnum(type, VoucherDiscountType)) + ), + [] + ) + }, + started: { + active: maybe( + () => + [params.startedFrom, params.startedTo].some( + field => field !== undefined + ), + false + ), + value: { + max: maybe(() => params.startedTo, ""), + min: maybe(() => params.startedFrom, "") + } + }, + status: { + active: !!maybe(() => params.status), + value: maybe( + () => + dedupeFilter( + params.status.map(status => + findValueInEnum(status, DiscountStatusEnum) + ) + ), + [] + ) + }, + timesUsed: { + active: maybe( + () => + [params.timesUsedFrom, params.timesUsedTo].some( + field => field !== undefined + ), + false + ), + value: { + max: maybe(() => params.timesUsedTo, ""), + min: maybe(() => params.timesUsedFrom, "") + } + } + }; +} + +export function getFilterVariables( + params: VoucherListUrlFilters +): VoucherFilterInput { + return { + discountType: + params.type && + params.type.map(type => findValueInEnum(type, VoucherDiscountType)), + search: params.query, + started: getGteLteVariables({ + gte: joinDateTime(params.startedFrom), + lte: joinDateTime(params.startedTo) + }), + status: + params.status && + params.status.map(status => findValueInEnum(status, DiscountStatusEnum)), + timesUsed: getGteLteVariables({ + gte: parseInt(params.timesUsedFrom, 0), + lte: parseInt(params.timesUsedTo, 0) + }) + }; +} + +export function getFilterQueryParam( + filter: IFilterElement +): VoucherListUrlFilters { + const { name } = filter; + + switch (name) { + case VoucherFilterKeys.saleType: + return getMultipleEnumValueQueryParam( + filter, + VoucherListUrlFiltersWithMultipleValues.type, + VoucherDiscountType + ); + + case VoucherFilterKeys.started: + return getMinMaxQueryParam( + filter, + VoucherListUrlFiltersEnum.startedFrom, + VoucherListUrlFiltersEnum.startedTo + ); + + case VoucherFilterKeys.timesUsed: + return getMinMaxQueryParam( + filter, + VoucherListUrlFiltersEnum.timesUsedFrom, + VoucherListUrlFiltersEnum.timesUsedTo + ); + + case VoucherFilterKeys.status: + return getMultipleEnumValueQueryParam( + filter, + VoucherListUrlFiltersWithMultipleValues.status, + DiscountStatusEnum + ); + } +} + +export const { + deleteFilterTab, + getFilterTabs, + saveFilterTab +} = createFilterTabUtils(VOUCHER_FILTERS_KEY); + +export const { areFiltersApplied, getActiveFilters } = createFilterUtils< + VoucherListUrlQueryParams, + VoucherListUrlFilters +>(VoucherListUrlFiltersEnum); diff --git a/src/fixtures.ts b/src/fixtures.ts index aff42ef0d..c466112cd 100644 --- a/src/fixtures.ts +++ b/src/fixtures.ts @@ -1,5 +1,4 @@ import { ShopInfo_shop_permissions } from "./components/Shop/types/ShopInfo"; -import { Filter } from "./components/TableFilter"; import { FetchMoreProps, FilterPageProps, @@ -303,80 +302,14 @@ export const searchPageProps: SearchPageProps = { onSearchChange: () => undefined }; -export const filterPageProps: FilterPageProps = { +export const filterPageProps: FilterPageProps = { ...searchPageProps, ...tabPageProps, currencySymbol: "USD", - filtersList: [], - onFilterAdd: () => undefined + filterOpts: {}, + onFilterChange: () => undefined }; -export const filters: Filter[] = [ - { - label: "Property X is ", - onClick: () => undefined - }, - { - label: "Property Y is ", - onClick: () => undefined - }, - { - label: "Property Z is ", - onClick: () => undefined - }, - { - label: "Property X is ", - onClick: () => undefined - }, - { - label: "Property Y is ", - onClick: () => undefined - }, - { - label: "Property Z is ", - onClick: () => undefined - }, - { - label: "Property X is ", - onClick: () => undefined - }, - { - label: "Property Y is ", - onClick: () => undefined - }, - { - label: "Property Z is ", - onClick: () => undefined - }, - { - label: "Property X is ", - onClick: () => undefined - }, - { - label: "Property Y is ", - onClick: () => undefined - }, - { - label: "Property Z is ", - onClick: () => undefined - }, - { - label: "Property X is ", - onClick: () => undefined - }, - { - label: "Property Y is ", - onClick: () => undefined - }, - { - label: "Property Z is ", - onClick: () => undefined - } -].map((filter, filterIndex) => ({ - ...filter, - label: filter.label + filterIndex -})); - export const fetchMoreProps: FetchMoreProps = { hasMore: true, loading: false, @@ -443,3 +376,8 @@ export const permissions: ShopInfo_shop_permissions[] = [ __typename: "PermissionDisplay" as "PermissionDisplay", ...perm })); + +export const date = { + from: "2019-12-09", + to: "2019-12-38" +}; diff --git a/src/home/views/index.tsx b/src/home/views/index.tsx index 8e6aaa1f8..d42da2298 100644 --- a/src/home/views/index.tsx +++ b/src/home/views/index.tsx @@ -48,7 +48,7 @@ const HomeSection = () => { onProductsOutOfStockClick={() => navigate( productListUrl({ - status: StockAvailability.OUT_OF_STOCK + stockStatus: StockAvailability.OUT_OF_STOCK }) ) } diff --git a/src/hooks/debug/useOnMount.ts b/src/hooks/debug/useOnMount.ts new file mode 100644 index 000000000..48bb57f11 --- /dev/null +++ b/src/hooks/debug/useOnMount.ts @@ -0,0 +1,8 @@ +import { useEffect } from "react"; + +function useOnMount(name: string) { + // eslint-disable-next-line no-console + useEffect(() => console.log(`mounted node ${name}`), []); +} + +export default useOnMount; diff --git a/src/intl.ts b/src/intl.ts index fee264968..afdc1a18b 100644 --- a/src/intl.ts +++ b/src/intl.ts @@ -65,6 +65,9 @@ export const commonMessages = defineMessages({ startHour: { defaultMessage: "Start Hour" }, + status: { + defaultMessage: "Status" + }, summary: { defaultMessage: "Summary" }, diff --git a/src/misc.ts b/src/misc.ts index 5df0e61c2..5672298d9 100644 --- a/src/misc.ts +++ b/src/misc.ts @@ -115,7 +115,7 @@ export const transformPaymentStatus = (status: string, intl: IntlShape) => { } }; -const orderStatusMessages = defineMessages({ +export const orderStatusMessages = defineMessages({ cancelled: { defaultMessage: "Cancelled", description: "order status" @@ -351,14 +351,14 @@ export function findInEnum( export function findValueInEnum( needle: string, haystack: TEnum -) { +): TEnum[keyof TEnum] { const match = Object.entries(haystack).find(([_, value]) => value === needle); - if (!!match) { - return match[1] as TEnum; + if (!match) { + throw new Error(`Value ${needle} not found in enum`); } - throw new Error(`Value ${needle} not found in enum`); + return (needle as unknown) as TEnum[keyof TEnum]; } export function parseBoolean(a: string, defaultValue: boolean): boolean { diff --git a/src/orders/components/OrderDraftListPage/OrderDraftListPage.tsx b/src/orders/components/OrderDraftListPage/OrderDraftListPage.tsx index c42bbc0a8..83b6ec50d 100644 --- a/src/orders/components/OrderDraftListPage/OrderDraftListPage.tsx +++ b/src/orders/components/OrderDraftListPage/OrderDraftListPage.tsx @@ -6,34 +6,42 @@ import { FormattedMessage, useIntl } from "react-intl"; import Container from "@saleor/components/Container"; import PageHeader from "@saleor/components/PageHeader"; -import SearchBar from "@saleor/components/SearchBar"; import { sectionNames } from "@saleor/intl"; import { ListActions, PageListProps, - SearchPageProps, TabPageProps, - SortPage + SortPage, + FilterPageProps } from "@saleor/types"; import { OrderDraftListUrlSortField } from "@saleor/orders/urls"; -import { OrderDraftList_draftOrders_edges_node } from "../../types/OrderDraftList"; +import FilterBar from "@saleor/components/FilterBar"; import OrderDraftList from "../OrderDraftList"; +import { OrderDraftList_draftOrders_edges_node } from "../../types/OrderDraftList"; +import { + OrderDraftListFilterOpts, + OrderDraftFilterKeys, + createFilterStructure +} from "./filters"; export interface OrderDraftListPageProps extends PageListProps, ListActions, - SearchPageProps, + FilterPageProps, SortPage, TabPageProps { orders: OrderDraftList_draftOrders_edges_node[]; } const OrderDraftListPage: React.FC = ({ + currencySymbol, currentTab, disabled, + filterOpts, initialSearch, onAdd, onAll, + onFilterChange, onSearchChange, onTabChange, onTabDelete, @@ -43,6 +51,8 @@ const OrderDraftListPage: React.FC = ({ }) => { const intl = useIntl(); + const structure = createFilterStructure(intl, filterOpts); + return ( @@ -59,18 +69,21 @@ const OrderDraftListPage: React.FC = ({ - ; + customer: FilterOpts; +} + +const messages = defineMessages({ + created: { + defaultMessage: "Created", + description: "draft order" + }, + customer: { + defaultMessage: "Customer", + description: "draft order" + } +}); + +export function createFilterStructure( + intl: IntlShape, + opts: OrderDraftListFilterOpts +): IFilter { + return [ + { + ...createDateField( + OrderDraftFilterKeys.created, + intl.formatMessage(messages.created), + opts.created.value + ), + active: opts.created.active + }, + { + ...createTextField( + OrderDraftFilterKeys.customer, + intl.formatMessage(messages.customer), + opts.customer.value + ), + active: opts.customer.active + } + ]; +} diff --git a/src/orders/components/OrderDraftListPage/index.ts b/src/orders/components/OrderDraftListPage/index.ts index de5e8daab..93b2da611 100644 --- a/src/orders/components/OrderDraftListPage/index.ts +++ b/src/orders/components/OrderDraftListPage/index.ts @@ -1,2 +1,3 @@ export { default } from "./OrderDraftListPage"; export * from "./OrderDraftListPage"; +export * from "./filters"; diff --git a/src/orders/components/OrderListFilter/OrderListFilter.tsx b/src/orders/components/OrderListFilter/OrderListFilter.tsx deleted file mode 100644 index df399abcf..000000000 --- a/src/orders/components/OrderListFilter/OrderListFilter.tsx +++ /dev/null @@ -1,169 +0,0 @@ -import moment from "moment-timezone"; -import React from "react"; -import { useIntl } from "react-intl"; - -import { DateContext } from "@saleor/components/Date/DateContext"; -import { FieldType, IFilter } from "@saleor/components/Filter"; -import FilterBar from "@saleor/components/FilterBar"; -import TimezoneContext from "@saleor/components/Timezone"; -import { FilterProps } from "../../../types"; -import { OrderStatusFilter } from "../../../types/globalTypes"; - -type OrderListFilterProps = FilterProps; - -export enum OrderFilterKeys { - date = "date", - dateEqual = "dateEqual", - dateRange = "dateRange", - dateLastWeek = "dateLastWeek", - dateLastMonth = "dateLastMonth", - dateLastYear = "dateLastYear", - status = "status" -} - -const OrderListFilter: React.FC = props => { - const date = React.useContext(DateContext); - const tz = React.useContext(TimezoneContext); - const intl = useIntl(); - - const filterMenu: IFilter = [ - { - children: [ - { - children: [], - data: { - fieldLabel: null, - type: FieldType.hidden, - value: (tz ? moment(date).tz(tz) : moment(date)) - .subtract(7, "days") - .toISOString() - .split("T")[0] // Remove timezone - }, - label: intl.formatMessage({ - defaultMessage: "Last 7 Days" - }), - value: OrderFilterKeys.dateLastWeek - }, - { - children: [], - data: { - fieldLabel: null, - type: FieldType.hidden, - value: (tz ? moment(date).tz(tz) : moment(date)) - .subtract(30, "days") - .toISOString() - .split("T")[0] // Remove timezone - }, - label: intl.formatMessage({ - defaultMessage: "Last 30 Days" - }), - value: OrderFilterKeys.dateLastMonth - }, - { - children: [], - data: { - fieldLabel: null, - type: FieldType.hidden, - value: (tz ? moment(date).tz(tz) : moment(date)) - .subtract(1, "years") - .toISOString() - .split("T")[0] // Remove timezone - }, - label: intl.formatMessage({ - defaultMessage: "Last Year" - }), - value: OrderFilterKeys.dateLastYear - }, - { - children: [], - data: { - additionalText: intl.formatMessage({ - defaultMessage: "equals" - }), - fieldLabel: null, - type: FieldType.date - }, - label: intl.formatMessage({ - defaultMessage: "Specific Date" - }), - value: OrderFilterKeys.dateEqual - }, - { - children: [], - data: { - fieldLabel: intl.formatMessage({ - defaultMessage: "Range" - }), - type: FieldType.rangeDate - }, - label: intl.formatMessage({ - defaultMessage: "Range" - }), - value: OrderFilterKeys.dateRange - } - ], - data: { - fieldLabel: intl.formatMessage({ - defaultMessage: "Date" - }), - type: FieldType.select - }, - label: intl.formatMessage({ - defaultMessage: "Date" - }), - value: OrderFilterKeys.date - }, - { - children: [], - data: { - additionalText: intl.formatMessage({ - defaultMessage: "is set as", - description: "date is set as" - }), - fieldLabel: intl.formatMessage({ - defaultMessage: "Status", - description: "order fulfillment status" - }), - options: [ - { - label: intl.formatMessage({ - defaultMessage: "Fulfilled", - description: "order fulfillment status" - }), - value: OrderStatusFilter.FULFILLED.toString() - }, - { - label: intl.formatMessage({ - defaultMessage: "Partially Fulfilled", - description: "order fulfillment status" - }), - value: OrderStatusFilter.PARTIALLY_FULFILLED.toString() - }, - { - label: intl.formatMessage({ - defaultMessage: "Unfulfilled", - description: "order fulfillment status" - }), - value: OrderStatusFilter.UNFULFILLED.toString() - }, - { - label: intl.formatMessage({ - defaultMessage: "Ready to Capture", - description: "order status" - }), - value: OrderStatusFilter.READY_TO_CAPTURE.toString() - } - ], - type: FieldType.select - }, - label: intl.formatMessage({ - defaultMessage: "Order Status" - }), - value: OrderFilterKeys.status - } - ]; - - return ; -}; -OrderListFilter.displayName = "OrderListFilter"; -export default OrderListFilter; diff --git a/src/orders/components/OrderListFilter/index.ts b/src/orders/components/OrderListFilter/index.ts deleted file mode 100644 index 108193863..000000000 --- a/src/orders/components/OrderListFilter/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from "./OrderListFilter"; -export * from "./OrderListFilter"; diff --git a/src/orders/components/OrderListPage/OrderListPage.tsx b/src/orders/components/OrderListPage/OrderListPage.tsx index 7a72aedbb..525380a0d 100644 --- a/src/orders/components/OrderListPage/OrderListPage.tsx +++ b/src/orders/components/OrderListPage/OrderListPage.tsx @@ -14,14 +14,19 @@ import { SortPage } from "@saleor/types"; import { OrderListUrlSortField } from "@saleor/orders/urls"; +import FilterBar from "@saleor/components/FilterBar"; import { OrderList_orders_edges_node } from "../../types/OrderList"; import OrderList from "../OrderList"; -import OrderListFilter, { OrderFilterKeys } from "../OrderListFilter"; +import { + createFilterStructure, + OrderListFilterOpts, + OrderFilterKeys +} from "./filters"; export interface OrderListPageProps extends PageListProps, ListActions, - FilterPageProps, + FilterPageProps, SortPage { orders: OrderList_orders_edges_node[]; } @@ -29,13 +34,13 @@ export interface OrderListPageProps const OrderListPage: React.FC = ({ currencySymbol, currentTab, - filtersList, initialSearch, + filterOpts, tabs, onAdd, onAll, onSearchChange, - onFilterAdd, + onFilterChange, onTabChange, onTabDelete, onTabSave, @@ -43,6 +48,8 @@ const OrderListPage: React.FC = ({ }) => { const intl = useIntl(); + const filterStructure = createFilterStructure(intl, filterOpts); + return ( @@ -54,28 +61,25 @@ const OrderListPage: React.FC = ({ - diff --git a/src/orders/components/OrderListPage/filters.ts b/src/orders/components/OrderListPage/filters.ts new file mode 100644 index 000000000..9e05c75d9 --- /dev/null +++ b/src/orders/components/OrderListPage/filters.ts @@ -0,0 +1,71 @@ +import { defineMessages, IntlShape } from "react-intl"; + +import { FilterOpts, MinMax } from "@saleor/types"; +import { OrderStatusFilter } from "@saleor/types/globalTypes"; +import { + createDateField, + createOptionsField +} from "@saleor/utils/filters/fields"; +import { IFilter } from "@saleor/components/Filter"; +import { orderStatusMessages } from "@saleor/misc"; +import { commonMessages } from "@saleor/intl"; + +export enum OrderFilterKeys { + created = "created", + status = "status" +} + +export interface OrderListFilterOpts { + created: FilterOpts; + status: FilterOpts; +} + +const messages = defineMessages({ + placed: { + defaultMessage: "Placed", + description: "order" + } +}); + +export function createFilterStructure( + intl: IntlShape, + opts: OrderListFilterOpts +): IFilter { + return [ + { + ...createDateField( + OrderFilterKeys.created, + intl.formatMessage(messages.placed), + opts.created.value + ), + active: opts.created.active + }, + { + ...createOptionsField( + OrderFilterKeys.status, + intl.formatMessage(commonMessages.status), + opts.status.value, + true, + [ + { + label: intl.formatMessage(orderStatusMessages.cancelled), + value: OrderStatusFilter.CANCELED + }, + { + label: intl.formatMessage(orderStatusMessages.fulfilled), + value: OrderStatusFilter.FULFILLED + }, + { + label: intl.formatMessage(orderStatusMessages.partiallyFulfilled), + value: OrderStatusFilter.PARTIALLY_FULFILLED + }, + { + label: intl.formatMessage(orderStatusMessages.unfulfilled), + value: OrderStatusFilter.UNFULFILLED + } + ] + ), + active: opts.status.active + } + ]; +} diff --git a/src/orders/components/OrderListPage/index.ts b/src/orders/components/OrderListPage/index.ts index 69c5cc429..14183bccf 100644 --- a/src/orders/components/OrderListPage/index.ts +++ b/src/orders/components/OrderListPage/index.ts @@ -1,2 +1,3 @@ export { default } from "./OrderListPage"; export * from "./OrderListPage"; +export * from "./filters"; diff --git a/src/orders/urls.ts b/src/orders/urls.ts index ffeed351c..2dbfb6dfb 100644 --- a/src/orders/urls.ts +++ b/src/orders/urls.ts @@ -17,8 +17,8 @@ const orderSectionUrl = "/orders"; export const orderListPath = orderSectionUrl; export enum OrderListUrlFiltersEnum { - dateFrom = "dateFrom", - dateTo = "dateTo", + createdFrom = "createdFrom", + createdTo = "createdTo", email = "email", payment = "payment", query = "query" @@ -55,6 +55,9 @@ export const orderListUrl = (params?: OrderListUrlQueryParams): string => { export const orderDraftListPath = urlJoin(orderSectionUrl, "drafts"); export enum OrderDraftListUrlFiltersEnum { + createdFrom = "createdFrom", + createdTo = "createdTo", + customer = "customer", query = "query" } export type OrderDraftListUrlFilters = Filters; diff --git a/src/orders/views/OrderDraftList/OrderDraftList.tsx b/src/orders/views/OrderDraftList/OrderDraftList.tsx index 8c500c1c8..81e816d9a 100644 --- a/src/orders/views/OrderDraftList/OrderDraftList.tsx +++ b/src/orders/views/OrderDraftList/OrderDraftList.tsx @@ -21,6 +21,8 @@ import { ListViews } from "@saleor/types"; import createSortHandler from "@saleor/utils/handlers/sortHandler"; import { getSortParams } from "@saleor/utils/sort"; import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers"; +import useShop from "@saleor/hooks/useShop"; +import createFilterHandlers from "@saleor/utils/handlers/filterHandlers"; import OrderDraftListPage from "../../components/OrderDraftListPage"; import { TypedOrderDraftBulkCancelMutation, @@ -31,7 +33,6 @@ import { OrderDraftBulkCancel } from "../../types/OrderDraftBulkCancel"; import { OrderDraftCreate } from "../../types/OrderDraftCreate"; import { orderDraftListUrl, - OrderDraftListUrlFilters, OrderDraftListUrlQueryParams, orderUrl, OrderDraftListUrlDialog @@ -42,8 +43,10 @@ import { getActiveFilters, getFilterTabs, getFilterVariables, - saveFilterTab -} from "./filter"; + saveFilterTab, + getFilterQueryParam, + getFilterOpts +} from "./filters"; import { getSortQueryVariables } from "./sort"; interface OrderDraftListProps { @@ -61,6 +64,7 @@ export const OrderDraftList: React.FC = ({ params }) => { ListViews.DRAFT_LIST ); const intl = useIntl(); + const shop = useShop(); const handleCreateOrderCreateSuccess = (data: OrderDraftCreate) => { notify({ @@ -84,16 +88,17 @@ export const OrderDraftList: React.FC = ({ params }) => { : 0 : parseInt(params.activeTab, 0); - const changeFilterField = (filter: OrderDraftListUrlFilters) => { - reset(); - navigate( - orderDraftListUrl({ - ...getActiveFilters(params), - ...filter, - activeTab: undefined - }) - ); - }; + const [ + changeFilters, + resetFilters, + handleSearchChange + ] = createFilterHandlers({ + cleanupFn: reset, + createUrl: orderDraftListUrl, + getFilterQueryParam, + navigate, + params + }); const [openModal, closeModal] = createDialogActionHandlers< OrderDraftListUrlDialog, @@ -155,6 +160,7 @@ export const OrderDraftList: React.FC = ({ params }) => { }; const handleSort = createSortHandler(navigate, orderDraftListUrl, params); + const currencySymbol = maybe(() => shop.defaultCurrency, "USD"); return ( @@ -169,10 +175,13 @@ export const OrderDraftList: React.FC = ({ params }) => { return ( <> changeFilterField({ query })} - onAll={() => navigate(orderDraftListUrl())} + onSearchChange={handleSearchChange} + onFilterChange={changeFilters} + onAll={resetFilters} onTabChange={handleTabChange} onTabDelete={() => openModal("delete-search")} onTabSave={() => openModal("save-search")} diff --git a/src/orders/views/OrderDraftList/__snapshots__/filters.test.ts.snap b/src/orders/views/OrderDraftList/__snapshots__/filters.test.ts.snap new file mode 100644 index 000000000..b48bb1405 --- /dev/null +++ b/src/orders/views/OrderDraftList/__snapshots__/filters.test.ts.snap @@ -0,0 +1,11 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Filtering URL params should not be empty if active filters are present 1`] = ` +Object { + "createdFrom": "2019-12-09", + "createdTo": "2019-12-38", + "customer": "admin@example.com", +} +`; + +exports[`Filtering URL params should not be empty if active filters are present 2`] = `"createdFrom=2019-12-09&createdTo=2019-12-38&customer=admin%40example.com"`; diff --git a/src/orders/views/OrderDraftList/filter.ts b/src/orders/views/OrderDraftList/filter.ts deleted file mode 100644 index 76e72e29e..000000000 --- a/src/orders/views/OrderDraftList/filter.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { OrderDraftFilterInput } from "@saleor/types/globalTypes"; -import { - createFilterTabUtils, - createFilterUtils -} from "../../../utils/filters"; -import { - OrderDraftListUrlFilters, - OrderDraftListUrlFiltersEnum, - OrderDraftListUrlQueryParams -} from "../../urls"; - -export const ORDER_DRAFT_FILTERS_KEY = "orderDraftFilters"; - -export function getFilterVariables( - params: OrderDraftListUrlFilters -): OrderDraftFilterInput { - return { - search: params.query - }; -} - -export const { - deleteFilterTab, - getFilterTabs, - saveFilterTab -} = createFilterTabUtils(ORDER_DRAFT_FILTERS_KEY); - -export const { areFiltersApplied, getActiveFilters } = createFilterUtils< - OrderDraftListUrlQueryParams, - OrderDraftListUrlFilters ->(OrderDraftListUrlFiltersEnum); diff --git a/src/orders/views/OrderDraftList/filters.test.ts b/src/orders/views/OrderDraftList/filters.test.ts new file mode 100644 index 000000000..bc0bd6e4c --- /dev/null +++ b/src/orders/views/OrderDraftList/filters.test.ts @@ -0,0 +1,67 @@ +import { createIntl } from "react-intl"; +import { stringify as stringifyQs } from "qs"; + +import { OrderDraftListUrlFilters } from "@saleor/orders/urls"; +import { createFilterStructure } from "@saleor/orders/components/OrderDraftListPage"; +import { getFilterQueryParams } from "@saleor/utils/filters"; +import { date } from "@saleor/fixtures"; +import { getExistingKeys, setFilterOptsStatus } from "@test/filters"; +import { config } from "@test/intl"; +import { getFilterVariables, getFilterQueryParam } from "./filters"; + +describe("Filtering query params", () => { + it("should be empty object if no params given", () => { + const params: OrderDraftListUrlFilters = {}; + const filterVariables = getFilterVariables(params); + + expect(getExistingKeys(filterVariables)).toHaveLength(0); + }); + + it("should not be empty object if params given", () => { + const params: OrderDraftListUrlFilters = { + createdFrom: date.from, + createdTo: date.to, + customer: "admin@example.com" + }; + const filterVariables = getFilterVariables(params); + + expect(getExistingKeys(filterVariables)).toHaveLength(2); + }); +}); + +describe("Filtering URL params", () => { + const intl = createIntl(config); + + const filters = createFilterStructure(intl, { + created: { + active: false, + value: { + max: date.to, + min: date.from + } + }, + customer: { + active: false, + value: "admin@example.com" + } + }); + + it("should be empty if no active filters", () => { + const filterQueryParams = getFilterQueryParams( + filters, + getFilterQueryParam + ); + + expect(getExistingKeys(filterQueryParams)).toHaveLength(0); + }); + + it("should not be empty if active filters are present", () => { + const filterQueryParams = getFilterQueryParams( + setFilterOptsStatus(filters, true), + getFilterQueryParam + ); + + expect(filterQueryParams).toMatchSnapshot(); + expect(stringifyQs(filterQueryParams)).toMatchSnapshot(); + }); +}); diff --git a/src/orders/views/OrderDraftList/filters.ts b/src/orders/views/OrderDraftList/filters.ts new file mode 100644 index 000000000..a6814a4e0 --- /dev/null +++ b/src/orders/views/OrderDraftList/filters.ts @@ -0,0 +1,90 @@ +import { OrderDraftFilterInput } from "@saleor/types/globalTypes"; +import { maybe } from "@saleor/misc"; +import { IFilterElement } from "@saleor/components/Filter"; +import { + OrderDraftFilterKeys, + OrderDraftListFilterOpts +} from "@saleor/orders/components/OrderDraftListPage"; +import { + OrderDraftListUrlFilters, + OrderDraftListUrlFiltersEnum, + OrderDraftListUrlQueryParams +} from "../../urls"; +import { + createFilterTabUtils, + createFilterUtils, + getGteLteVariables, + getMinMaxQueryParam, + getSingleValueQueryParam +} from "../../../utils/filters"; + +export const ORDER_DRAFT_FILTERS_KEY = "orderDraftFilters"; + +export function getFilterOpts( + params: OrderDraftListUrlFilters +): OrderDraftListFilterOpts { + return { + created: { + active: maybe( + () => + [params.createdFrom, params.createdTo].some( + field => field !== undefined + ), + false + ), + value: { + max: maybe(() => params.createdTo), + min: maybe(() => params.createdFrom) + } + }, + customer: { + active: !!maybe(() => params.customer), + value: params.customer + } + }; +} + +export function getFilterVariables( + params: OrderDraftListUrlFilters +): OrderDraftFilterInput { + return { + created: getGteLteVariables({ + gte: params.createdFrom, + lte: params.createdTo + }), + customer: params.customer, + search: params.query + }; +} + +export function getFilterQueryParam( + filter: IFilterElement +): OrderDraftListUrlFilters { + const { name } = filter; + + switch (name) { + case OrderDraftFilterKeys.created: + return getMinMaxQueryParam( + filter, + OrderDraftListUrlFiltersEnum.createdFrom, + OrderDraftListUrlFiltersEnum.createdTo + ); + + case OrderDraftFilterKeys.customer: + return getSingleValueQueryParam( + filter, + OrderDraftListUrlFiltersEnum.customer + ); + } +} + +export const { + deleteFilterTab, + getFilterTabs, + saveFilterTab +} = createFilterTabUtils(ORDER_DRAFT_FILTERS_KEY); + +export const { areFiltersApplied, getActiveFilters } = createFilterUtils< + OrderDraftListUrlQueryParams, + OrderDraftListUrlFilters +>(OrderDraftListUrlFiltersEnum); diff --git a/src/orders/views/OrderList/OrderList.tsx b/src/orders/views/OrderList/OrderList.tsx index 40a078ca9..c17d82d6c 100644 --- a/src/orders/views/OrderList/OrderList.tsx +++ b/src/orders/views/OrderList/OrderList.tsx @@ -7,7 +7,6 @@ import SaveFilterTabDialog, { SaveFilterTabDialogFormData } from "@saleor/components/SaveFilterTabDialog"; import useBulkActions from "@saleor/hooks/useBulkActions"; -import useDateLocalize from "@saleor/hooks/useDateLocalize"; import useListSettings from "@saleor/hooks/useListSettings"; import useNavigator from "@saleor/hooks/useNavigator"; import useNotifier from "@saleor/hooks/useNotifier"; @@ -20,6 +19,7 @@ import { ListViews } from "@saleor/types"; import createSortHandler from "@saleor/utils/handlers/sortHandler"; import { getSortParams } from "@saleor/utils/sort"; import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers"; +import createFilterHandlers from "@saleor/utils/handlers/filterHandlers"; import OrderBulkCancelDialog from "../../components/OrderBulkCancelDialog"; import OrderListPage from "../../components/OrderListPage/OrderListPage"; import { @@ -31,20 +31,19 @@ import { OrderBulkCancel } from "../../types/OrderBulkCancel"; import { OrderDraftCreate } from "../../types/OrderDraftCreate"; import { orderListUrl, - OrderListUrlFilters, OrderListUrlQueryParams, orderUrl, OrderListUrlDialog } from "../../urls"; import { areFiltersApplied, - createFilter, - createFilterChips, deleteFilterTab, getActiveFilters, getFilterTabs, + getFilterOpts, getFilterVariables, - saveFilterTab + saveFilterTab, + getFilterQueryParam } from "./filters"; import { getSortQueryVariables } from "./sort"; @@ -53,7 +52,6 @@ interface OrderListProps { } export const OrderList: React.FC = ({ params }) => { - const formatDate = useDateLocalize(); const navigate = useNavigator(); const notify = useNotifier(); const paginate = usePaginator(); @@ -88,21 +86,17 @@ export const OrderList: React.FC = ({ params }) => { : 0 : parseInt(params.activeTab, 0); - const changeFilters = (filters: OrderListUrlFilters) => { - reset(); - navigate(orderListUrl(filters)); - }; - - const changeFilterField = (filter: OrderListUrlFilters) => { - reset(); - navigate( - orderListUrl({ - ...getActiveFilters(params), - ...filter, - activeTab: undefined - }) - ); - }; + const [ + changeFilters, + resetFilters, + handleSearchChange + ] = createFilterHandlers({ + cleanupFn: reset, + createUrl: orderListUrl, + getFilterQueryParam, + navigate, + params + }); const [openModal, closeModal] = createDialogActionHandlers< OrderListUrlDialog, @@ -183,16 +177,9 @@ export const OrderList: React.FC = ({ params }) => { data.orders.edges.map(edge => edge.node))} pageInfo={pageInfo} sort={getSortParams(params)} @@ -221,20 +208,14 @@ export const OrderList: React.FC = ({ params }) => { /> } - onSearchChange={query => changeFilterField({ query })} - onFilterAdd={data => - changeFilterField(createFilter(params, data)) - } + onSearchChange={handleSearchChange} + onFilterChange={changeFilters} onTabSave={() => openModal("save-search")} onTabDelete={() => openModal("delete-search")} onTabChange={handleTabChange} initialSearch={params.query || ""} tabs={getFilterTabs().map(tab => tab.name)} - onAll={() => - changeFilters({ - status: undefined - }) - } + onAll={resetFilters} /> { + it("should be empty object if no params given", () => { + const params: OrderListUrlFilters = {}; + const filterVariables = getFilterVariables(params); -describe("Create filter object", () => { - it("with date", () => { - const filter = createFilter( - {}, - { - name: OrderFilterKeys.dateEqual, - value: "2019-09-01" - } - ); - - expect(filter).toMatchSnapshot(); + expect(getExistingKeys(filterVariables)).toHaveLength(0); }); - it("with date range", () => { - const filter = createFilter( - {}, - { - name: OrderFilterKeys.dateRange, - value: ["2019-09-01", "2019-09-10"] - } - ); - - expect(filter).toMatchSnapshot(); - }); - - it("with date last week", () => { - const filter = createFilter( - {}, - { - name: OrderFilterKeys.dateLastWeek, - value: "2019-09-01" - } - ); - - expect(filter).toMatchSnapshot(); - }); - - it("with date last month", () => { - const filter = createFilter( - {}, - { - name: OrderFilterKeys.dateLastMonth, - value: "2019-09-01" - } - ); - - expect(filter).toMatchSnapshot(); - }); - - it("with date last year", () => { - const filter = createFilter( - {}, - { - name: OrderFilterKeys.dateLastYear, - value: "2019-09-01" - } - ); - - expect(filter).toMatchSnapshot(); - }); - - it("with fulfillment status", () => { - const filter = createFilter( - {}, - { - name: OrderFilterKeys.status, - value: OrderStatusFilter.PARTIALLY_FULFILLED - } - ); - - expect(filter).toMatchSnapshot(); - }); - - it("with multiple values", () => { - const filter = createFilter( - { - status: [OrderStatusFilter.FULFILLED] - }, - { - name: OrderFilterKeys.status, - value: OrderStatusFilter.PARTIALLY_FULFILLED - } - ); - - expect(filter).toMatchSnapshot(); - }); - - it("with multiple deduped values", () => { - const filter = createFilter( - { - status: [OrderStatusFilter.FULFILLED] - }, - { - name: OrderFilterKeys.status, - value: OrderStatusFilter.FULFILLED - } - ); - - expect(filter).toMatchSnapshot(); - }); -}); - -test("Crate filter chips", () => { - const chips = createFilterChips( - { - dateFrom: "2019-09-01", - dateTo: "2019-09-10", + it("should not be empty object if params given", () => { + const params: OrderListUrlFilters = { + createdFrom: date.from, + createdTo: date.to, email: "email@example.com", - status: [OrderStatus.FULFILLED, OrderStatus.PARTIALLY_FULFILLED] - }, - { - formatDate: date => date - }, - jest.fn(), - mockIntl as any - ); - - expect(chips).toMatchSnapshot(); -}); - -describe("Get filter variables", () => { - it("from single status value", () => { - const filter = getFilterVariables({ - dateFrom: "2019-09-01", - dateTo: "2019-09-10", - email: "email@example.com", - query: "24", - status: OrderStatus.FULFILLED.toString() - }); - - expect(filter).toMatchSnapshot(); - }); - - it("from multiple status value", () => { - const filter = getFilterVariables({ - dateFrom: "2019-09-01", - dateTo: "2019-09-10", - email: "email@example.com", - query: "24", status: [ - OrderStatus.FULFILLED.toString(), - OrderStatus.PARTIALLY_FULFILLED.toString() + OrderStatusFilter.FULFILLED, + OrderStatusFilter.PARTIALLY_FULFILLED ] - }); + }; + const filterVariables = getFilterVariables(params); - expect(filter).toMatchSnapshot(); + expect(getExistingKeys(filterVariables)).toHaveLength(3); + }); +}); + +describe("Filtering URL params", () => { + const intl = createIntl(config); + + const filters = createFilterStructure(intl, { + created: { + active: false, + value: { + max: date.to, + min: date.from + } + }, + status: { + active: false, + value: [ + OrderStatusFilter.FULFILLED, + OrderStatusFilter.PARTIALLY_FULFILLED + ] + } + }); + + it("should be empty if no active filters", () => { + const filterQueryParams = getFilterQueryParams( + filters, + getFilterQueryParam + ); + + expect(getExistingKeys(filterQueryParams)).toHaveLength(0); + }); + + it("should not be empty if active filters are present", () => { + const filterQueryParams = getFilterQueryParams( + setFilterOptsStatus(filters, true), + getFilterQueryParam + ); + + expect(filterQueryParams).toMatchSnapshot(); + expect(stringifyQs(filterQueryParams)).toMatchSnapshot(); }); }); diff --git a/src/orders/views/OrderList/filters.ts b/src/orders/views/OrderList/filters.ts index c9441161b..db3bfec82 100644 --- a/src/orders/views/OrderList/filters.ts +++ b/src/orders/views/OrderList/filters.ts @@ -1,22 +1,22 @@ -import { defineMessages, IntlShape } from "react-intl"; - -import { findInEnum } from "@saleor/misc"; -import { removeAtIndex } from "@saleor/utils/lists"; -import { FilterContentSubmitData } from "../../../components/Filter"; -import { Filter } from "../../../components/TableFilter"; +import { findInEnum, maybe, findValueInEnum } from "@saleor/misc"; +import { + OrderListFilterOpts, + OrderFilterKeys +} from "@saleor/orders/components/OrderListPage/filters"; +import { IFilterElement } from "../../../components/Filter"; import { OrderFilterInput, - OrderStatusFilter + OrderStatusFilter, + OrderStatus } from "../../../types/globalTypes"; import { - arrayOrUndefined, - arrayOrValue, createFilterTabUtils, createFilterUtils, dedupeFilter, - valueOrFirst + getGteLteVariables, + getMinMaxQueryParam, + getMultipleEnumValueQueryParam } from "../../../utils/filters"; -import { OrderFilterKeys } from "../../components/OrderListFilter"; import { OrderListUrlFilters, OrderListUrlFiltersEnum, @@ -26,215 +26,76 @@ import { export const ORDER_FILTERS_KEY = "orderFilters"; -const filterMessages = defineMessages({ - dateFrom: { - defaultMessage: "Date from {date}", - description: "filter by date" - }, - dateIs: { - defaultMessage: "Date is {date}", - description: "filter by date" - }, - dateTo: { - defaultMessage: "Date to {date}", - description: "filter by date" - }, - fulfilled: { - defaultMessage: "Fulfilled", - description: "order status" - }, - partiallyFulfilled: { - defaultMessage: "Partially Fulfilled", - description: "order status" - }, - readyToCapture: { - defaultMessage: "Ready to Capture", - description: "order status" - }, - unfulfilled: { - defaultMessage: "Unfulfilled", - description: "order status" - } -}); - -function getStatusLabel(status: string, intl: IntlShape): string { - switch (status) { - case OrderStatusFilter.FULFILLED.toString(): - return intl.formatMessage(filterMessages.fulfilled); - - case OrderStatusFilter.PARTIALLY_FULFILLED.toString(): - return intl.formatMessage(filterMessages.partiallyFulfilled); - - case OrderStatusFilter.UNFULFILLED.toString(): - return intl.formatMessage(filterMessages.unfulfilled); - - case OrderStatusFilter.READY_TO_CAPTURE.toString(): - return intl.formatMessage(filterMessages.readyToCapture); - } - - return ""; +export function getFilterOpts( + params: OrderListUrlFilters +): OrderListFilterOpts { + return { + created: { + active: maybe( + () => + [params.createdFrom, params.createdTo].some( + field => field !== undefined + ), + false + ), + value: { + max: maybe(() => params.createdTo, ""), + min: maybe(() => params.createdFrom, "") + } + }, + status: { + active: maybe(() => params.status !== undefined, false), + value: maybe( + () => + dedupeFilter( + params.status.map(status => + findValueInEnum(status, OrderStatusFilter) + ) + ), + [] + ) + } + }; } export function getFilterVariables( params: OrderListUrlFilters ): OrderFilterInput { return { - created: { - gte: params.dateFrom, - lte: params.dateTo - }, + created: getGteLteVariables({ + gte: params.createdFrom, + lte: params.createdTo + }), customer: params.email, search: params.query, - status: Array.isArray(params.status) - ? params.status.map(status => findInEnum(status, OrderStatusFilter)) - : params.status - ? [findInEnum(params.status, OrderStatusFilter)] - : undefined + status: maybe(() => + params.status.map(status => findInEnum(status, OrderStatusFilter)) + ) }; } -export function createFilter( - filter: OrderListUrlFilters, - data: FilterContentSubmitData +export function getFilterQueryParam( + filter: IFilterElement ): OrderListUrlFilters { - const { name: filterName, value } = data; - if (filterName === OrderFilterKeys.dateEqual) { - return { - dateFrom: valueOrFirst(value), - dateTo: valueOrFirst(value) - }; - } else if (filterName === OrderFilterKeys.dateRange) { - return { - dateFrom: value[0], - dateTo: value[1] - }; - } else if ( - [ - OrderFilterKeys.dateLastWeek, - OrderFilterKeys.dateLastMonth, - OrderFilterKeys.dateLastYear - ].includes(filterName) - ) { - return { - dateFrom: valueOrFirst(value), - dateTo: undefined - }; - } else if (filterName === OrderFilterKeys.status) { - return { - status: dedupeFilter( - filter.status - ? [...(filter.status as string[]), valueOrFirst(value)] - : arrayOrValue(value) - ) - }; + const { name } = filter; + + switch (name) { + case OrderFilterKeys.created: + return getMinMaxQueryParam( + filter, + OrderListUrlFiltersEnum.createdFrom, + OrderListUrlFiltersEnum.createdTo + ); + + case OrderFilterKeys.status: + return getMultipleEnumValueQueryParam( + filter, + OrderListUrlFiltersWithMultipleValuesEnum.status, + OrderStatus + ); } } -interface OrderListChipFormatData { - formatDate: (date: string) => string; -} -export function createFilterChips( - filters: OrderListUrlFilters, - formatData: OrderListChipFormatData, - onFilterDelete: (filters: OrderListUrlFilters) => void, - intl: IntlShape -): Filter[] { - let filterChips: Filter[] = []; - - if (!!filters.dateFrom || !!filters.dateTo) { - if (filters.dateFrom === filters.dateTo) { - filterChips = [ - ...filterChips, - { - label: intl.formatMessage(filterMessages.dateIs, { - date: formatData.formatDate(filters.dateFrom) - }), - onClick: () => - onFilterDelete({ - ...filters, - dateFrom: undefined, - dateTo: undefined - }) - } - ]; - } else { - if (!!filters.dateFrom) { - filterChips = [ - ...filterChips, - { - label: intl.formatMessage(filterMessages.dateFrom, { - date: formatData.formatDate(filters.dateFrom) - }), - onClick: () => - onFilterDelete({ - ...filters, - dateFrom: undefined - }) - } - ]; - } - - if (!!filters.dateTo) { - filterChips = [ - ...filterChips, - { - label: intl.formatMessage(filterMessages.dateTo, { - date: formatData.formatDate(filters.dateTo) - }), - onClick: () => - onFilterDelete({ - ...filters, - dateTo: undefined - }) - } - ]; - } - } - } - - if (!!filters.email) { - filterChips = [ - ...filterChips, - { - label: filters.email, - onClick: () => - onFilterDelete({ - ...filters, - email: undefined - }) - } - ]; - } - - if (!!filters.status) { - const statusFilterChips = Array.isArray(filters.status) - ? filters.status.map((status, statusIndex) => ({ - label: getStatusLabel(status, intl), - onClick: () => - onFilterDelete({ - ...filters, - status: arrayOrUndefined( - removeAtIndex(filters.status as string[], statusIndex) - ) - }) - })) - : [ - { - label: getStatusLabel(filters.status, intl), - onClick: () => - onFilterDelete({ - ...filters, - status: undefined - }) - } - ]; - - filterChips = [...filterChips, ...statusFilterChips]; - } - - return filterChips; -} - export const { deleteFilterTab, getFilterTabs, diff --git a/src/plugins/components/PluginsList/PluginsList.tsx b/src/plugins/components/PluginsList/PluginsList.tsx index 9fa315136..568e88910 100644 --- a/src/plugins/components/PluginsList/PluginsList.tsx +++ b/src/plugins/components/PluginsList/PluginsList.tsx @@ -1,4 +1,3 @@ -import Card from "@material-ui/core/Card"; import { makeStyles } from "@material-ui/core/styles"; import TableBody from "@material-ui/core/TableBody"; import TableCell from "@material-ui/core/TableCell"; @@ -65,107 +64,105 @@ const PluginList: React.FC = props => { onPreviousPage } = props; const classes = useStyles(props); - const intl = useIntl(); + return ( - - - - + + onSort(PluginListUrlSortField.name)} + className={classes.colName} + > + {intl.formatMessage({ + defaultMessage: "Name", + description: "plugin name" + })} + + onSort(PluginListUrlSortField.active)} + className={classes.colActive} + > + {intl.formatMessage({ + defaultMessage: "Active", + description: "plugin status" + })} + + + {intl.formatMessage({ + defaultMessage: "Action", + description: "user action bar" + })} + + + + + onSort(PluginListUrlSortField.name)} - className={classes.colName} - > - {intl.formatMessage({ - defaultMessage: "Name", - description: "plugin name" - })} - - onSort(PluginListUrlSortField.active)} - className={classes.colActive} - > - {intl.formatMessage({ - defaultMessage: "Active", - description: "plugin status" - })} - - - {intl.formatMessage({ - defaultMessage: "Action", - description: "user action bar" - })} - - - - - - - - - {renderCollection( - plugins, - plugin => ( - - - {maybe(() => plugin.name, )} - - - {maybe( - () => ( - - ), - - )} - - -
- -
-
-
- ), - () => ( - - - {intl.formatMessage({ - defaultMessage: "No plugins found" - })} - - - ) - )} -
-
-
+ onPreviousPage={onPreviousPage} + /> + + + + {renderCollection( + plugins, + plugin => ( + + + {maybe(() => plugin.name, )} + + + {maybe( + () => ( + + ), + + )} + + +
+ +
+
+
+ ), + () => ( + + + {intl.formatMessage({ + defaultMessage: "No plugins found" + })} + + + ) + )} +
+ ); }; PluginList.displayName = "PluginList"; diff --git a/src/plugins/components/PluginsListPage/PluginsListPage.tsx b/src/plugins/components/PluginsListPage/PluginsListPage.tsx index efcb3372b..da4ead724 100644 --- a/src/plugins/components/PluginsListPage/PluginsListPage.tsx +++ b/src/plugins/components/PluginsListPage/PluginsListPage.tsx @@ -1,35 +1,85 @@ import React from "react"; +import Card from "@material-ui/core/Card"; import { useIntl } from "react-intl"; import AppHeader from "@saleor/components/AppHeader"; import Container from "@saleor/components/Container"; import PageHeader from "@saleor/components/PageHeader"; import { sectionNames } from "@saleor/intl"; -import { PageListProps, SortPage } from "@saleor/types"; +import { + PageListProps, + SortPage, + FilterPageProps, + TabPageProps +} from "@saleor/types"; import { PluginListUrlSortField } from "@saleor/plugins/urls"; +import FilterBar from "@saleor/components/FilterBar"; import { Plugins_plugins_edges_node } from "../../types/Plugins"; import PluginsList from "../PluginsList/PluginsList"; +import { + createFilterStructure, + PluginFilterKeys, + PluginListFilterOpts +} from "./filters"; export interface PluginsListPageProps extends PageListProps, - SortPage { + FilterPageProps, + SortPage, + TabPageProps { plugins: Plugins_plugins_edges_node[]; onBack: () => void; } const PluginsListPage: React.FC = ({ + currencySymbol, + currentTab, + initialSearch, + filterOpts, + tabs, + onAdd, + onAll, onBack, + onSearchChange, + onFilterChange, + onTabChange, + onTabDelete, + onTabSave, ...listProps }) => { const intl = useIntl(); + const filterStructure = createFilterStructure(intl, filterOpts); + return ( {intl.formatMessage(sectionNames.configuration)} - + + + + ); }; diff --git a/src/plugins/components/PluginsListPage/filters.ts b/src/plugins/components/PluginsListPage/filters.ts new file mode 100644 index 000000000..d4d1a384b --- /dev/null +++ b/src/plugins/components/PluginsListPage/filters.ts @@ -0,0 +1,45 @@ +import { defineMessages, IntlShape } from "react-intl"; + +import { FilterOpts } from "@saleor/types"; +import { IFilter } from "@saleor/components/Filter"; +import { createBooleanField } from "@saleor/utils/filters/fields"; +import { commonMessages } from "@saleor/intl"; + +export enum PluginFilterKeys { + active = "active" +} + +export interface PluginListFilterOpts { + isActive: FilterOpts; +} + +const messages = defineMessages({ + active: { + defaultMessage: "Active", + description: "plugin" + }, + deactivated: { + defaultMessage: "Inactive", + description: "plugin" + } +}); + +export function createFilterStructure( + intl: IntlShape, + opts: PluginListFilterOpts +): IFilter { + return [ + { + ...createBooleanField( + PluginFilterKeys.active, + intl.formatMessage(commonMessages.status), + opts.isActive.value, + { + negative: intl.formatMessage(messages.deactivated), + positive: intl.formatMessage(messages.active) + } + ), + active: opts.isActive.active + } + ]; +} diff --git a/src/plugins/components/PluginsListPage/index.ts b/src/plugins/components/PluginsListPage/index.ts index 37ed6f973..a378b1ac7 100644 --- a/src/plugins/components/PluginsListPage/index.ts +++ b/src/plugins/components/PluginsListPage/index.ts @@ -1,2 +1,3 @@ export { default } from "./PluginsListPage"; export * from "./PluginsListPage"; +export * from "./filters"; diff --git a/src/plugins/queries.ts b/src/plugins/queries.ts index dc518f6a5..c3003fa81 100644 --- a/src/plugins/queries.ts +++ b/src/plugins/queries.ts @@ -35,6 +35,7 @@ const pluginsList = gql` $after: String $last: Int $before: String + $filter: PluginFilterInput $sort: PluginSortingInput ) { plugins( @@ -42,6 +43,7 @@ const pluginsList = gql` after: $after first: $first last: $last + filter: $filter sortBy: $sort ) { edges { diff --git a/src/plugins/types/Plugins.ts b/src/plugins/types/Plugins.ts index a8d888402..59b578762 100644 --- a/src/plugins/types/Plugins.ts +++ b/src/plugins/types/Plugins.ts @@ -2,7 +2,7 @@ /* eslint-disable */ // This file was automatically generated and should not be edited. -import { PluginSortingInput } from "./../../types/globalTypes"; +import { PluginFilterInput, PluginSortingInput } from "./../../types/globalTypes"; // ==================================================== // GraphQL query operation: Plugins @@ -44,5 +44,6 @@ export interface PluginsVariables { after?: string | null; last?: number | null; before?: string | null; + filter?: PluginFilterInput | null; sort?: PluginSortingInput | null; } diff --git a/src/plugins/urls.ts b/src/plugins/urls.ts index 11a6b15f8..ab0faca5f 100644 --- a/src/plugins/urls.ts +++ b/src/plugins/urls.ts @@ -1,17 +1,34 @@ import { stringify as stringifyQs } from "qs"; import urlJoin from "url-join"; -import { Dialog, Pagination, SingleAction, Sort } from "../types"; +import { + Dialog, + Pagination, + SingleAction, + Sort, + Filters, + ActiveTab, + TabActionDialog +} from "../types"; export const pluginSection = "/plugins/"; export const pluginListPath = pluginSection; +export enum PluginListUrlFiltersEnum { + active = "active", + query = "query" +} +export type PluginListUrlFilters = Filters; +export type PluginListUrlDialog = TabActionDialog; export enum PluginListUrlSortField { name = "name", active = "active" } export type PluginListUrlSort = Sort; -export type PluginListUrlQueryParams = Pagination & +export type PluginListUrlQueryParams = ActiveTab & + Dialog & + PluginListUrlFilters & + Pagination & PluginListUrlSort & SingleAction; export const pluginListUrl = (params?: PluginListUrlQueryParams) => diff --git a/src/plugins/views/PluginList/PluginList.tsx b/src/plugins/views/PluginList/PluginList.tsx index 60482660a..e46efa943 100644 --- a/src/plugins/views/PluginList/PluginList.tsx +++ b/src/plugins/views/PluginList/PluginList.tsx @@ -10,10 +10,32 @@ import React from "react"; import { getSortParams } from "@saleor/utils/sort"; import createSortHandler from "@saleor/utils/handlers/sortHandler"; +import createFilterHandlers from "@saleor/utils/handlers/filterHandlers"; +import useShop from "@saleor/hooks/useShop"; +import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers"; +import SaveFilterTabDialog, { + SaveFilterTabDialogFormData +} from "@saleor/components/SaveFilterTabDialog"; +import DeleteFilterTabDialog from "@saleor/components/DeleteFilterTabDialog"; import PluginsListPage from "../../components/PluginsListPage/PluginsListPage"; import { usePluginsListQuery } from "../../queries"; -import { PluginListUrlQueryParams, pluginListUrl, pluginUrl } from "../../urls"; +import { + PluginListUrlQueryParams, + pluginListUrl, + pluginUrl, + PluginListUrlDialog +} from "../../urls"; import { getSortQueryVariables } from "./sort"; +import { + getFilterQueryParam, + getFilterOpts, + getFilterTabs, + areFiltersApplied, + saveFilterTab, + getActiveFilters, + deleteFilterTab, + getFilterVariables +} from "./filters"; interface PluginsListProps { params: PluginListUrlQueryParams; @@ -22,6 +44,7 @@ interface PluginsListProps { export const PluginsList: React.FC = ({ params }) => { const navigate = useNavigator(); const paginate = usePaginator(); + const shop = useShop(); const { updateListSettings, settings } = useListSettings( ListViews.PLUGINS_LIST ); @@ -30,6 +53,7 @@ export const PluginsList: React.FC = ({ params }) => { const queryVariables = React.useMemo( () => ({ ...paginationState, + filter: getFilterVariables(params), sort: getSortQueryVariables(params) }), [params] @@ -39,6 +63,50 @@ export const PluginsList: React.FC = ({ params }) => { variables: queryVariables }); + const tabs = getFilterTabs(); + + const currentTab = + params.activeTab === undefined + ? areFiltersApplied(params) + ? tabs.length + 1 + : 0 + : parseInt(params.activeTab, 0); + + const [ + changeFilters, + resetFilters, + handleSearchChange + ] = createFilterHandlers({ + createUrl: pluginListUrl, + getFilterQueryParam, + navigate, + params + }); + + const [openModal, closeModal] = createDialogActionHandlers< + PluginListUrlDialog, + PluginListUrlQueryParams + >(navigate, pluginListUrl, params); + + const handleTabChange = (tab: number) => { + navigate( + pluginListUrl({ + activeTab: tab.toString(), + ...getFilterTabs()[tab - 1].data + }) + ); + }; + + const handleFilterTabDelete = () => { + deleteFilterTab(currentTab); + navigate(pluginListUrl()); + }; + + const handleFilterTabSave = (data: SaveFilterTabDialogFormData) => { + saveFilterTab(data.name, getActiveFilters(params)); + handleTabChange(tabs.length + 1); + }; + const { loadNextPage, loadPreviousPage, pageInfo } = paginate( maybe(() => data.plugins.pageInfo), paginationState, @@ -46,23 +114,48 @@ export const PluginsList: React.FC = ({ params }) => { ); const handleSort = createSortHandler(navigate, pluginListUrl, params); + const currencySymbol = maybe(() => shop.defaultCurrency, "USD"); return ( <> data.plugins.edges.map(edge => edge.node))} pageInfo={pageInfo} sort={getSortParams(params)} + tabs={getFilterTabs().map(tab => tab.name)} onAdd={() => navigate(configurationMenuUrl)} + onAll={resetFilters} onBack={() => navigate(configurationMenuUrl)} + onFilterChange={changeFilters} + onSearchChange={handleSearchChange} onNextPage={loadNextPage} onPreviousPage={loadPreviousPage} onSort={handleSort} + onTabSave={() => openModal("save-search")} + onTabDelete={() => openModal("delete-search")} + onTabChange={handleTabChange} onUpdateListSettings={updateListSettings} onRowClick={id => () => navigate(pluginUrl(id))} /> + + tabs[currentTab - 1].name, "...")} + /> ); }; diff --git a/src/plugins/views/PluginList/__snapshots__/filters.test.ts.snap b/src/plugins/views/PluginList/__snapshots__/filters.test.ts.snap new file mode 100644 index 000000000..79b6ca913 --- /dev/null +++ b/src/plugins/views/PluginList/__snapshots__/filters.test.ts.snap @@ -0,0 +1,9 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Filtering URL params should not be empty if active filters are present 1`] = ` +Object { + "active": "true", +} +`; + +exports[`Filtering URL params should not be empty if active filters are present 2`] = `"active=true"`; diff --git a/src/plugins/views/PluginList/filters.test.ts b/src/plugins/views/PluginList/filters.test.ts new file mode 100644 index 000000000..fe14aa8b5 --- /dev/null +++ b/src/plugins/views/PluginList/filters.test.ts @@ -0,0 +1,57 @@ +import { createIntl } from "react-intl"; +import { stringify as stringifyQs } from "qs"; + +import { PluginListUrlFilters } from "@saleor/plugins/urls"; +import { createFilterStructure } from "@saleor/plugins/components/PluginsListPage"; +import { getFilterQueryParams } from "@saleor/utils/filters"; +import { getExistingKeys, setFilterOptsStatus } from "@test/filters"; +import { config } from "@test/intl"; +import { getFilterVariables, getFilterQueryParam } from "./filters"; + +describe("Filtering query params", () => { + it("should be empty object if no params given", () => { + const params: PluginListUrlFilters = {}; + const filterVariables = getFilterVariables(params); + + expect(getExistingKeys(filterVariables)).toHaveLength(0); + }); + + it("should not be empty object if params given", () => { + const params: PluginListUrlFilters = { + active: true.toString() + }; + const filterVariables = getFilterVariables(params); + + expect(getExistingKeys(filterVariables)).toHaveLength(1); + }); +}); + +describe("Filtering URL params", () => { + const intl = createIntl(config); + + const filters = createFilterStructure(intl, { + isActive: { + active: false, + value: true + } + }); + + it("should be empty if no active filters", () => { + const filterQueryParams = getFilterQueryParams( + filters, + getFilterQueryParam + ); + + expect(getExistingKeys(filterQueryParams)).toHaveLength(0); + }); + + it("should not be empty if active filters are present", () => { + const filterQueryParams = getFilterQueryParams( + setFilterOptsStatus(filters, true), + getFilterQueryParam + ); + + expect(filterQueryParams).toMatchSnapshot(); + expect(stringifyQs(filterQueryParams)).toMatchSnapshot(); + }); +}); diff --git a/src/plugins/views/PluginList/filters.ts b/src/plugins/views/PluginList/filters.ts new file mode 100644 index 000000000..ae33062e3 --- /dev/null +++ b/src/plugins/views/PluginList/filters.ts @@ -0,0 +1,65 @@ +import { PluginFilterInput } from "@saleor/types/globalTypes"; +import { + PluginListFilterOpts, + PluginFilterKeys +} from "@saleor/plugins/components/PluginsListPage"; +import { maybe, parseBoolean } from "@saleor/misc"; +import { IFilterElement } from "@saleor/components/Filter"; +import { + PluginListUrlFilters, + PluginListUrlFiltersEnum, + PluginListUrlQueryParams +} from "../../urls"; +import { + createFilterTabUtils, + createFilterUtils, + getSingleValueQueryParam +} from "../../../utils/filters"; + +export const PLUGIN_FILTERS_KEY = "pluginFilters"; + +export function getFilterOpts( + params: PluginListUrlFilters +): PluginListFilterOpts { + return { + isActive: { + active: maybe(() => params.active !== undefined, false), + value: + params.active !== undefined ? parseBoolean(params.active, true) : true + } + }; +} + +export function getFilterVariables( + params: PluginListUrlFilters +): PluginFilterInput { + return { + active: + params.active !== undefined + ? parseBoolean(params.active, true) + : undefined, + search: params.query + }; +} + +export function getFilterQueryParam( + filter: IFilterElement +): PluginListUrlFilters { + const { name } = filter; + + switch (name) { + case PluginFilterKeys.active: + return getSingleValueQueryParam(filter, PluginListUrlFiltersEnum.active); + } +} + +export const { + deleteFilterTab, + getFilterTabs, + saveFilterTab +} = createFilterTabUtils(PLUGIN_FILTERS_KEY); + +export const { areFiltersApplied, getActiveFilters } = createFilterUtils< + PluginListUrlQueryParams, + PluginListUrlFilters +>(PluginListUrlFiltersEnum); diff --git a/src/productTypes/components/ProductTypeListPage/ProductTypeListPage.tsx b/src/productTypes/components/ProductTypeListPage/ProductTypeListPage.tsx index 0061de667..647ae7ada 100644 --- a/src/productTypes/components/ProductTypeListPage/ProductTypeListPage.tsx +++ b/src/productTypes/components/ProductTypeListPage/ProductTypeListPage.tsx @@ -6,23 +6,28 @@ import { FormattedMessage, useIntl } from "react-intl"; import AppHeader from "@saleor/components/AppHeader"; import Container from "@saleor/components/Container"; import PageHeader from "@saleor/components/PageHeader"; -import SearchBar from "@saleor/components/SearchBar"; +import FilterBar from "@saleor/components/FilterBar"; import { sectionNames } from "@saleor/intl"; import { ProductTypeListUrlSortField } from "@saleor/productTypes/urls"; import { ListActions, PageListProps, - SearchPageProps, + FilterPageProps, TabPageProps, SortPage } from "../../../types"; import { ProductTypeList_productTypes_edges_node } from "../../types/ProductTypeList"; import ProductTypeList from "../ProductTypeList"; +import { + createFilterStructure, + ProductTypeFilterKeys, + ProductTypeListFilterOpts +} from "./filters"; export interface ProductTypeListPageProps extends PageListProps, ListActions, - SearchPageProps, + FilterPageProps, SortPage, TabPageProps { productTypes: ProductTypeList_productTypes_edges_node[]; @@ -30,11 +35,14 @@ export interface ProductTypeListPageProps } const ProductTypeListPage: React.FC = ({ + currencySymbol, currentTab, + filterOpts, initialSearch, onAdd, onAll, onBack, + onFilterChange, onSearchChange, onTabChange, onTabDelete, @@ -44,6 +52,8 @@ const ProductTypeListPage: React.FC = ({ }) => { const intl = useIntl(); + const structure = createFilterStructure(intl, filterOpts); + return ( @@ -58,18 +68,21 @@ const ProductTypeListPage: React.FC = ({ - ; + type: FilterOpts; +} + +const messages = defineMessages({ + configurable: { + defaultMessage: "Configurable", + description: "product type" + }, + digital: { + defaultMessage: "Digital", + description: "product" + }, + shippable: { + defaultMessage: "Shippable", + description: "product" + }, + type: { + defaultMessage: "Type", + description: "product type is digital or physical" + } +}); + +export function createFilterStructure( + intl: IntlShape, + opts: ProductTypeListFilterOpts +): IFilter { + return [ + { + ...createOptionsField( + ProductTypeFilterKeys.configurable, + intl.formatMessage(messages.configurable), + [opts.configurable.value], + false, + [ + { + label: intl.formatMessage(commonMessages.yes), + value: ProductTypeConfigurable.CONFIGURABLE + }, + { + label: intl.formatMessage(commonMessages.no), + value: ProductTypeConfigurable.SIMPLE + } + ] + ), + active: opts.configurable.active + }, + { + ...createOptionsField( + ProductTypeFilterKeys.type, + intl.formatMessage(messages.type), + [opts.type.value], + false, + [ + { + label: intl.formatMessage(messages.digital), + value: ProductTypeEnum.DIGITAL + }, + { + label: intl.formatMessage(messages.shippable), + value: ProductTypeEnum.SHIPPABLE + } + ] + ), + active: opts.type.active + } + ]; +} diff --git a/src/productTypes/components/ProductTypeListPage/index.ts b/src/productTypes/components/ProductTypeListPage/index.ts index 55c8646cc..0e7b89ba0 100644 --- a/src/productTypes/components/ProductTypeListPage/index.ts +++ b/src/productTypes/components/ProductTypeListPage/index.ts @@ -1,2 +1,3 @@ export { default } from "./ProductTypeListPage"; export * from "./ProductTypeListPage"; +export * from "./filters"; diff --git a/src/productTypes/urls.ts b/src/productTypes/urls.ts index bf295d6ae..5905a7b04 100644 --- a/src/productTypes/urls.ts +++ b/src/productTypes/urls.ts @@ -16,6 +16,8 @@ const productTypeSection = "/product-types/"; export const productTypeListPath = productTypeSection; export enum ProductTypeListUrlFiltersEnum { + configurable = "configurable", + type = "type", query = "query" } export type ProductTypeListUrlFilters = Filters; diff --git a/src/productTypes/views/ProductTypeList/ProductTypeList.tsx b/src/productTypes/views/ProductTypeList/ProductTypeList.tsx index 92c73c7e2..1de7f7498 100644 --- a/src/productTypes/views/ProductTypeList/ProductTypeList.tsx +++ b/src/productTypes/views/ProductTypeList/ProductTypeList.tsx @@ -21,6 +21,8 @@ import { ListViews } from "@saleor/types"; import { getSortParams } from "@saleor/utils/sort"; import createSortHandler from "@saleor/utils/handlers/sortHandler"; import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers"; +import useShop from "@saleor/hooks/useShop"; +import createFilterHandlers from "@saleor/utils/handlers/filterHandlers"; import { configurationMenuUrl } from "../../../configuration"; import { maybe } from "../../../misc"; import ProductTypeListPage from "../../components/ProductTypeListPage"; @@ -31,7 +33,6 @@ import { productTypeAddUrl, productTypeListUrl, ProductTypeListUrlDialog, - ProductTypeListUrlFilters, ProductTypeListUrlQueryParams, productTypeUrl } from "../../urls"; @@ -41,8 +42,10 @@ import { getActiveFilters, getFilterTabs, getFilterVariables, - saveFilterTab -} from "./filter"; + saveFilterTab, + getFilterQueryParam, + getFilterOpts +} from "./filters"; import { getSortQueryVariables } from "./sort"; interface ProductTypeListProps { @@ -53,6 +56,7 @@ export const ProductTypeList: React.FC = ({ params }) => { const navigate = useNavigator(); const notify = useNotifier(); const paginate = usePaginator(); + const shop = useShop(); const { isSelected, listElements, reset, toggle, toggleAll } = useBulkActions( params.ids ); @@ -82,16 +86,17 @@ export const ProductTypeList: React.FC = ({ params }) => { : 0 : parseInt(params.activeTab, 0); - const changeFilterField = (filter: ProductTypeListUrlFilters) => { - reset(); - navigate( - productTypeListUrl({ - ...getActiveFilters(params), - ...filter, - activeTab: undefined - }) - ); - }; + const [ + changeFilters, + resetFilters, + handleSearchChange + ] = createFilterHandlers({ + cleanupFn: reset, + createUrl: productTypeListUrl, + getFilterQueryParam, + navigate, + params + }); const [openModal, closeModal] = createDialogActionHandlers< ProductTypeListUrlDialog, @@ -143,6 +148,7 @@ export const ProductTypeList: React.FC = ({ params }) => { }; const handleSort = createSortHandler(navigate, productTypeListUrl, params); + const currencySymbol = maybe(() => shop.defaultCurrency, "USD"); return ( = ({ params }) => { return ( <> changeFilterField({ query })} - onAll={() => navigate(productTypeListUrl())} + onSearchChange={handleSearchChange} + onFilterChange={changeFilters} + onAll={resetFilters} onTabChange={handleTabChange} onTabDelete={() => openModal("delete-search")} onTabSave={() => openModal("save-search")} diff --git a/src/productTypes/views/ProductTypeList/__snapshots__/filters.test.ts.snap b/src/productTypes/views/ProductTypeList/__snapshots__/filters.test.ts.snap new file mode 100644 index 000000000..305a60be9 --- /dev/null +++ b/src/productTypes/views/ProductTypeList/__snapshots__/filters.test.ts.snap @@ -0,0 +1,10 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Filtering URL params should not be empty if active filters are present 1`] = ` +Object { + "configurable": "CONFIGURABLE", + "type": "DIGITAL", +} +`; + +exports[`Filtering URL params should not be empty if active filters are present 2`] = `"configurable=CONFIGURABLE&type=DIGITAL"`; diff --git a/src/productTypes/views/ProductTypeList/filter.ts b/src/productTypes/views/ProductTypeList/filter.ts deleted file mode 100644 index fffcd0cd8..000000000 --- a/src/productTypes/views/ProductTypeList/filter.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { ProductTypeFilterInput } from "@saleor/types/globalTypes"; -import { - createFilterTabUtils, - createFilterUtils -} from "../../../utils/filters"; -import { - ProductTypeListUrlFilters, - ProductTypeListUrlFiltersEnum, - ProductTypeListUrlQueryParams -} from "../../urls"; - -export const PRODUCT_TYPE_FILTERS_KEY = "productTypeFilters"; - -export function getFilterVariables( - params: ProductTypeListUrlFilters -): ProductTypeFilterInput { - return { - search: params.query - }; -} - -export const { - deleteFilterTab, - getFilterTabs, - saveFilterTab -} = createFilterTabUtils(PRODUCT_TYPE_FILTERS_KEY); - -export const { areFiltersApplied, getActiveFilters } = createFilterUtils< - ProductTypeListUrlQueryParams, - ProductTypeListUrlFilters ->(ProductTypeListUrlFiltersEnum); diff --git a/src/productTypes/views/ProductTypeList/filters.test.ts b/src/productTypes/views/ProductTypeList/filters.test.ts new file mode 100644 index 000000000..9911460a0 --- /dev/null +++ b/src/productTypes/views/ProductTypeList/filters.test.ts @@ -0,0 +1,66 @@ +import { createIntl } from "react-intl"; +import { stringify as stringifyQs } from "qs"; + +import { ProductTypeListUrlFilters } from "@saleor/productTypes/urls"; +import { createFilterStructure } from "@saleor/productTypes/components/ProductTypeListPage"; +import { getFilterQueryParams } from "@saleor/utils/filters"; +import { getExistingKeys, setFilterOptsStatus } from "@test/filters"; +import { config } from "@test/intl"; +import { + ProductTypeEnum, + ProductTypeConfigurable +} from "@saleor/types/globalTypes"; +import { getFilterVariables, getFilterQueryParam } from "./filters"; + +describe("Filtering query params", () => { + it("should be empty object if no params given", () => { + const params: ProductTypeListUrlFilters = {}; + const filterVariables = getFilterVariables(params); + + expect(getExistingKeys(filterVariables)).toHaveLength(0); + }); + + it("should not be empty object if params given", () => { + const params: ProductTypeListUrlFilters = { + configurable: ProductTypeConfigurable.CONFIGURABLE, + type: ProductTypeEnum.DIGITAL + }; + const filterVariables = getFilterVariables(params); + + expect(getExistingKeys(filterVariables)).toHaveLength(2); + }); +}); + +describe("Filtering URL params", () => { + const intl = createIntl(config); + + const filters = createFilterStructure(intl, { + configurable: { + active: false, + value: ProductTypeConfigurable.CONFIGURABLE + }, + type: { + active: false, + value: ProductTypeEnum.DIGITAL + } + }); + + it("should be empty if no active filters", () => { + const filterQueryParams = getFilterQueryParams( + filters, + getFilterQueryParam + ); + + expect(getExistingKeys(filterQueryParams)).toHaveLength(0); + }); + + it("should not be empty if active filters are present", () => { + const filterQueryParams = getFilterQueryParams( + setFilterOptsStatus(filters, true), + getFilterQueryParam + ); + + expect(filterQueryParams).toMatchSnapshot(); + expect(stringifyQs(filterQueryParams)).toMatchSnapshot(); + }); +}); diff --git a/src/productTypes/views/ProductTypeList/filters.ts b/src/productTypes/views/ProductTypeList/filters.ts new file mode 100644 index 000000000..c21c4e426 --- /dev/null +++ b/src/productTypes/views/ProductTypeList/filters.ts @@ -0,0 +1,85 @@ +import { + ProductTypeFilterInput, + ProductTypeConfigurable, + ProductTypeEnum +} from "@saleor/types/globalTypes"; +import { IFilterElement } from "@saleor/components/Filter"; +import { maybe, findValueInEnum } from "@saleor/misc"; +import { + ProductTypeFilterKeys, + ProductTypeListFilterOpts +} from "@saleor/productTypes/components/ProductTypeListPage"; +import { + createFilterTabUtils, + createFilterUtils, + getSingleValueQueryParam +} from "../../../utils/filters"; +import { + ProductTypeListUrlFilters, + ProductTypeListUrlFiltersEnum, + ProductTypeListUrlQueryParams +} from "../../urls"; + +export const PRODUCT_TYPE_FILTERS_KEY = "productTypeFilters"; + +export function getFilterOpts( + params: ProductTypeListUrlFilters +): ProductTypeListFilterOpts { + return { + configurable: { + active: !!maybe(() => params.configurable), + value: maybe(() => + findValueInEnum(params.configurable, ProductTypeConfigurable) + ) + }, + type: { + active: !!maybe(() => params.type), + value: maybe(() => findValueInEnum(params.type, ProductTypeEnum)) + } + }; +} + +export function getFilterVariables( + params: ProductTypeListUrlFilters +): ProductTypeFilterInput { + return { + configurable: params.configurable + ? findValueInEnum(params.configurable, ProductTypeConfigurable) + : undefined, + productType: params.type + ? findValueInEnum(params.type, ProductTypeEnum) + : undefined, + search: params.query + }; +} + +export function getFilterQueryParam( + filter: IFilterElement +): ProductTypeListUrlFilters { + const { name } = filter; + + switch (name) { + case ProductTypeFilterKeys.configurable: + return getSingleValueQueryParam( + filter, + ProductTypeListUrlFiltersEnum.configurable + ); + + case ProductTypeFilterKeys.type: + return getSingleValueQueryParam( + filter, + ProductTypeListUrlFiltersEnum.type + ); + } +} + +export const { + deleteFilterTab, + getFilterTabs, + saveFilterTab +} = createFilterTabUtils(PRODUCT_TYPE_FILTERS_KEY); + +export const { areFiltersApplied, getActiveFilters } = createFilterUtils< + ProductTypeListUrlQueryParams, + ProductTypeListUrlFilters +>(ProductTypeListUrlFiltersEnum); diff --git a/src/products/components/ProductListFilter/ProductListFilter.tsx b/src/products/components/ProductListFilter/ProductListFilter.tsx deleted file mode 100644 index 52074f5ed..000000000 --- a/src/products/components/ProductListFilter/ProductListFilter.tsx +++ /dev/null @@ -1,153 +0,0 @@ -import React from "react"; -import { useIntl } from "react-intl"; - -import { FieldType, IFilter } from "@saleor/components/Filter"; -import FilterBar from "@saleor/components/FilterBar"; -import { FilterProps } from "@saleor/types"; -import { StockAvailability } from "@saleor/types/globalTypes"; - -type ProductListFilterProps = Omit< - FilterProps, - "allTabLabel" | "filterLabel" | "searchPlaceholder" ->; - -export enum ProductFilterKeys { - published = "published", - price = "price", - priceEqual = "priceEqual", - priceRange = "priceRange", - stock = "stock" -} - -const ProductListFilter: React.FC = props => { - const intl = useIntl(); - - const filterMenu: IFilter = [ - { - children: [], - data: { - additionalText: intl.formatMessage({ - defaultMessage: "is set as", - description: "product status is set as" - }), - fieldLabel: intl.formatMessage({ - defaultMessage: "Status", - description: "product status" - }), - options: [ - { - label: intl.formatMessage({ - defaultMessage: "Visible", - description: "product is visible" - }), - value: true - }, - { - label: intl.formatMessage({ - defaultMessage: "Hidden", - description: "product is hidden" - }), - value: false - } - ], - type: FieldType.select - }, - label: intl.formatMessage({ - defaultMessage: "Visibility", - description: "product visibility" - }), - value: ProductFilterKeys.published - }, - { - children: [], - data: { - fieldLabel: intl.formatMessage({ - defaultMessage: "Stock quantity" - }), - options: [ - { - label: intl.formatMessage({ - defaultMessage: "Available", - description: "product status" - }), - value: StockAvailability.IN_STOCK - }, - { - label: intl.formatMessage({ - defaultMessage: "Out Of Stock", - description: "product status" - }), - value: StockAvailability.OUT_OF_STOCK - } - ], - type: FieldType.select - }, - label: intl.formatMessage({ - defaultMessage: "Stock", - description: "product stock" - }), - value: ProductFilterKeys.stock - }, - { - children: [ - { - children: [], - data: { - additionalText: intl.formatMessage({ - defaultMessage: "equals", - description: "product price" - }), - fieldLabel: null, - type: FieldType.price - }, - label: intl.formatMessage({ - defaultMessage: "Specific Price" - }), - value: ProductFilterKeys.priceEqual - }, - { - children: [], - data: { - fieldLabel: intl.formatMessage({ - defaultMessage: "Range" - }), - type: FieldType.rangePrice - }, - label: intl.formatMessage({ - defaultMessage: "Range" - }), - value: ProductFilterKeys.priceRange - } - ], - data: { - fieldLabel: intl.formatMessage({ - defaultMessage: "Price" - }), - type: FieldType.range - }, - label: intl.formatMessage({ - defaultMessage: "Price" - }), - value: ProductFilterKeys.price - } - ]; - - return ( - - ); -}; -ProductListFilter.displayName = "ProductListFilter"; -export default ProductListFilter; diff --git a/src/products/components/ProductListFilter/index.ts b/src/products/components/ProductListFilter/index.ts deleted file mode 100644 index c251bdbef..000000000 --- a/src/products/components/ProductListFilter/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from "./ProductListFilter"; -export * from "./ProductListFilter"; diff --git a/src/products/components/ProductListPage/ProductListPage.tsx b/src/products/components/ProductListPage/ProductListPage.tsx index 6ebc13096..d0bb14c1d 100644 --- a/src/products/components/ProductListPage/ProductListPage.tsx +++ b/src/products/components/ProductListPage/ProductListPage.tsx @@ -23,14 +23,19 @@ import { PageListProps, SortPage } from "@saleor/types"; +import FilterBar from "@saleor/components/FilterBar"; import { ProductListUrlSortField } from "../../urls"; import ProductList from "../ProductList"; -import ProductListFilter, { ProductFilterKeys } from "../ProductListFilter"; +import { + createFilterStructure, + ProductFilterKeys, + ProductListFilterOpts +} from "./filters"; export interface ProductListPageProps extends PageListProps, ListActions, - FilterPageProps, + FilterPageProps, FetchMoreProps, SortPage { activeAttributeSortId: string; @@ -55,9 +60,9 @@ export const ProductListPage: React.FC = props => { currencySymbol, currentTab, defaultSettings, - filtersList, gridAttributes, availableInGridAttributes, + filterOpts, hasMore, initialSearch, loading, @@ -67,8 +72,8 @@ export const ProductListPage: React.FC = props => { onAdd, onAll, onFetchMore, + onFilterChange, onSearchChange, - onFilterAdd, onTabChange, onTabDelete, onTabSave, @@ -81,6 +86,8 @@ export const ProductListPage: React.FC = props => { const handleSave = (columns: ProductListColumns[]) => onUpdateListSettings("columns", columns); + const filterStructure = createFilterStructure(intl, filterOpts); + const columns: ColumnPickerChoice[] = [ { label: intl.formatMessage({ @@ -140,18 +147,25 @@ export const ProductListPage: React.FC = props => { - & { + choices: MultiAutocompleteChoiceType[]; + name: string; + slug: string; + } + >; + categories: FilterOpts & AutocompleteFilterOpts; + collections: FilterOpts & AutocompleteFilterOpts; + price: FilterOpts; + productType: FilterOpts & AutocompleteFilterOpts; + status: FilterOpts; + stockStatus: FilterOpts; +} + +export enum ProductStatus { + PUBLISHED = "published", + HIDDEN = "hidden" +} + +const messages = defineMessages({ + available: { + defaultMessage: "Available", + description: "product status" + }, + hidden: { + defaultMessage: "Hidden", + description: "product is hidden" + }, + outOfStock: { + defaultMessage: "Out Of Stock", + description: "product status" + }, + price: { + defaultMessage: "Price" + }, + quantity: { + defaultMessage: "Stock quantity", + description: "product" + }, + visibility: { + defaultMessage: "Visibility", + description: "product visibility" + }, + visible: { + defaultMessage: "Visible", + description: "product is visible" + } +}); + +export function createFilterStructure( + intl: IntlShape, + opts: ProductListFilterOpts +): IFilter { + return [ + { + ...createOptionsField( + ProductFilterKeys.status, + intl.formatMessage(messages.visibility), + [opts.status.value], + false, + [ + { + label: intl.formatMessage(messages.visible), + value: ProductStatus.PUBLISHED + }, + { + label: intl.formatMessage(messages.hidden), + value: ProductStatus.HIDDEN + } + ] + ), + active: opts.status.active + }, + { + ...createOptionsField( + ProductFilterKeys.stock, + intl.formatMessage(messages.quantity), + [opts.stockStatus.value], + false, + [ + { + label: intl.formatMessage(messages.available), + value: StockAvailability.IN_STOCK + }, + { + label: intl.formatMessage(messages.outOfStock), + value: StockAvailability.OUT_OF_STOCK + } + ] + ), + active: opts.stockStatus.active + }, + { + ...createPriceField( + ProductFilterKeys.price, + intl.formatMessage(messages.price), + opts.price.value + ), + active: opts.price.active + }, + { + ...createAutocompleteField( + ProductFilterKeys.categories, + intl.formatMessage(sectionNames.categories), + opts.categories.value, + opts.categories.displayValues, + true, + opts.categories.choices, + { + hasMore: opts.categories.hasMore, + initialSearch: "", + loading: opts.categories.loading, + onFetchMore: opts.categories.onFetchMore, + onSearchChange: opts.categories.onSearchChange + } + ), + active: opts.categories.active + }, + { + ...createAutocompleteField( + ProductFilterKeys.collections, + intl.formatMessage(sectionNames.collections), + opts.collections.value, + opts.collections.displayValues, + true, + opts.collections.choices, + { + hasMore: opts.collections.hasMore, + initialSearch: "", + loading: opts.collections.loading, + onFetchMore: opts.collections.onFetchMore, + onSearchChange: opts.collections.onSearchChange + } + ), + active: opts.collections.active + }, + { + ...createAutocompleteField( + ProductFilterKeys.productType, + intl.formatMessage(sectionNames.productTypes), + opts.productType.value, + opts.productType.displayValues, + true, + opts.productType.choices, + { + hasMore: opts.productType.hasMore, + initialSearch: "", + loading: opts.productType.loading, + onFetchMore: opts.productType.onFetchMore, + onSearchChange: opts.productType.onSearchChange + } + ), + active: opts.productType.active + }, + ...opts.attributes.map(attr => ({ + ...createOptionsField( + attr.slug as any, + attr.name, + attr.value, + true, + attr.choices + ), + active: attr.active, + group: ProductFilterKeys.attributes + })) + ]; +} diff --git a/src/products/components/ProductListPage/index.ts b/src/products/components/ProductListPage/index.ts index a5e54aa30..ceb0354d3 100644 --- a/src/products/components/ProductListPage/index.ts +++ b/src/products/components/ProductListPage/index.ts @@ -1,2 +1,3 @@ export { default } from "./ProductListPage"; export * from "./ProductListPage"; +export * from "./filters"; diff --git a/src/products/index.tsx b/src/products/index.tsx index fed57ca5a..2d04e98a0 100644 --- a/src/products/index.tsx +++ b/src/products/index.tsx @@ -5,6 +5,7 @@ import { Route, RouteComponentProps, Switch } from "react-router-dom"; import { sectionNames } from "@saleor/intl"; import { asSortParams } from "@saleor/utils/sort"; +import { getArrayQueryParam } from "@saleor/utils/urls"; import { WindowTitle } from "../components/WindowTitle"; import { productAddPath, @@ -29,7 +30,12 @@ import ProductVariantCreateComponent from "./views/ProductVariantCreate"; const ProductList: React.FC> = ({ location }) => { const qs = parseQs(location.search.substr(1)); const params: ProductListUrlQueryParams = asSortParams( - qs, + { + ...qs, + categories: getArrayQueryParam(qs.categories), + collections: getArrayQueryParam(qs.collections), + productTypes: getArrayQueryParam(qs.productTypes) + }, ProductListUrlSortField ); diff --git a/src/products/queries.ts b/src/products/queries.ts index c8e27af29..99236c111 100644 --- a/src/products/queries.ts +++ b/src/products/queries.ts @@ -1,5 +1,6 @@ import gql from "graphql-tag"; +import makeQuery from "@saleor/hooks/makeQuery"; import { pageInfoFragment, TypedQuery } from "../queries"; import { AvailableInGridAttributes, @@ -22,6 +23,10 @@ import { ProductVariantDetails, ProductVariantDetailsVariables } from "./types/ProductVariantDetails"; +import { + InitialProductFilterData, + InitialProductFilterDataVariables +} from "./types/InitialProductFilterData"; export const fragmentMoney = gql` fragment Money on Money { @@ -209,6 +214,57 @@ export const fragmentVariant = gql` } `; +const initialProductFilterDataQuery = gql` + query InitialProductFilterData( + $categories: [ID!] + $collections: [ID!] + $productTypes: [ID!] + ) { + attributes(first: 100, filter: { filterableInDashboard: true }) { + edges { + node { + id + name + slug + values { + id + name + slug + } + } + } + } + categories(first: 100, filter: { ids: $categories }) { + edges { + node { + id + name + } + } + } + collections(first: 100, filter: { ids: $collections }) { + edges { + node { + id + name + } + } + } + productTypes(first: 100, filter: { ids: $productTypes }) { + edges { + node { + id + name + } + } + } + } +`; +export const useInitialProductFilterDataQuery = makeQuery< + InitialProductFilterData, + InitialProductFilterDataVariables +>(initialProductFilterDataQuery); + const productListQuery = gql` ${productFragment} query ProductList( diff --git a/src/products/types/InitialProductFilterData.ts b/src/products/types/InitialProductFilterData.ts new file mode 100644 index 000000000..0c982e04b --- /dev/null +++ b/src/products/types/InitialProductFilterData.ts @@ -0,0 +1,93 @@ +/* tslint:disable */ +/* eslint-disable */ +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL query operation: InitialProductFilterData +// ==================================================== + +export interface InitialProductFilterData_attributes_edges_node_values { + __typename: "AttributeValue"; + id: string; + name: string | null; + slug: string | null; +} + +export interface InitialProductFilterData_attributes_edges_node { + __typename: "Attribute"; + id: string; + name: string | null; + slug: string | null; + values: (InitialProductFilterData_attributes_edges_node_values | null)[] | null; +} + +export interface InitialProductFilterData_attributes_edges { + __typename: "AttributeCountableEdge"; + node: InitialProductFilterData_attributes_edges_node; +} + +export interface InitialProductFilterData_attributes { + __typename: "AttributeCountableConnection"; + edges: InitialProductFilterData_attributes_edges[]; +} + +export interface InitialProductFilterData_categories_edges_node { + __typename: "Category"; + id: string; + name: string; +} + +export interface InitialProductFilterData_categories_edges { + __typename: "CategoryCountableEdge"; + node: InitialProductFilterData_categories_edges_node; +} + +export interface InitialProductFilterData_categories { + __typename: "CategoryCountableConnection"; + edges: InitialProductFilterData_categories_edges[]; +} + +export interface InitialProductFilterData_collections_edges_node { + __typename: "Collection"; + id: string; + name: string; +} + +export interface InitialProductFilterData_collections_edges { + __typename: "CollectionCountableEdge"; + node: InitialProductFilterData_collections_edges_node; +} + +export interface InitialProductFilterData_collections { + __typename: "CollectionCountableConnection"; + edges: InitialProductFilterData_collections_edges[]; +} + +export interface InitialProductFilterData_productTypes_edges_node { + __typename: "ProductType"; + id: string; + name: string; +} + +export interface InitialProductFilterData_productTypes_edges { + __typename: "ProductTypeCountableEdge"; + node: InitialProductFilterData_productTypes_edges_node; +} + +export interface InitialProductFilterData_productTypes { + __typename: "ProductTypeCountableConnection"; + edges: InitialProductFilterData_productTypes_edges[]; +} + +export interface InitialProductFilterData { + attributes: InitialProductFilterData_attributes | null; + categories: InitialProductFilterData_categories | null; + collections: InitialProductFilterData_collections | null; + productTypes: InitialProductFilterData_productTypes | null; +} + +export interface InitialProductFilterDataVariables { + categories?: string[] | null; + collections?: string[] | null; + productTypes?: string[] | null; +} diff --git a/src/products/urls.ts b/src/products/urls.ts index 003a91c28..9441c04a5 100644 --- a/src/products/urls.ts +++ b/src/products/urls.ts @@ -9,7 +9,9 @@ import { Filters, Pagination, Sort, - TabActionDialog + TabActionDialog, + FiltersWithMultipleValues, + FiltersAsDictWithMultipleValues } from "../types"; const productSection = "/products/"; @@ -24,13 +26,25 @@ export type ProductListUrlDialog = | "delete" | TabActionDialog; export enum ProductListUrlFiltersEnum { - isPublished = "isPublished", priceFrom = "priceFrom", priceTo = "priceTo", status = "status", + stockStatus = "stockStatus", query = "query" } -export type ProductListUrlFilters = Filters; +export enum ProductListUrlFiltersWithMultipleValues { + categories = "categories", + collections = "collections", + productTypes = "productTypes" +} +export enum ProductListUrlFiltersAsDictWithMultipleValues { + attributes = "attributes" +} +export type ProductListUrlFilters = Filters & + FiltersWithMultipleValues & + FiltersAsDictWithMultipleValues< + ProductListUrlFiltersAsDictWithMultipleValues + >; export enum ProductListUrlSortField { attribute = "attribute", name = "name", diff --git a/src/products/views/ProductList/ProductList.tsx b/src/products/views/ProductList/ProductList.tsx index 5b826e5e2..818a47395 100644 --- a/src/products/views/ProductList/ProductList.tsx +++ b/src/products/views/ProductList/ProductList.tsx @@ -10,10 +10,13 @@ import DeleteFilterTabDialog from "@saleor/components/DeleteFilterTabDialog"; import SaveFilterTabDialog, { SaveFilterTabDialogFormData } from "@saleor/components/SaveFilterTabDialog"; -import { defaultListSettings, ProductListColumns } from "@saleor/config"; +import { + defaultListSettings, + ProductListColumns, + DEFAULT_INITIAL_SEARCH_DATA +} from "@saleor/config"; import useBulkActions from "@saleor/hooks/useBulkActions"; import useListSettings from "@saleor/hooks/useListSettings"; -import useLocale from "@saleor/hooks/useLocale"; import useNavigator from "@saleor/hooks/useNavigator"; import useNotifier from "@saleor/hooks/useNotifier"; import usePaginator, { @@ -26,6 +29,10 @@ import { ProductListVariables } from "@saleor/products/types/ProductList"; import { ListViews } from "@saleor/types"; import { getSortUrlVariables } from "@saleor/utils/sort"; import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers"; +import createFilterHandlers from "@saleor/utils/handlers/filterHandlers"; +import useCategorySearch from "@saleor/searches/useCategorySearch"; +import useCollectionSearch from "@saleor/searches/useCollectionSearch"; +import useProductTypeSearch from "@saleor/searches/useProductTypeSearch"; import ProductListPage from "../../components/ProductListPage"; import { TypedProductBulkDeleteMutation, @@ -33,14 +40,14 @@ import { } from "../../mutations"; import { AvailableInGridAttributesQuery, - TypedProductListQuery + TypedProductListQuery, + useInitialProductFilterDataQuery } from "../../queries"; import { productBulkDelete } from "../../types/productBulkDelete"; import { productBulkPublish } from "../../types/productBulkPublish"; import { productAddUrl, productListUrl, - ProductListUrlFilters, ProductListUrlQueryParams, ProductListUrlSortField, productUrl, @@ -48,13 +55,13 @@ import { } from "../../urls"; import { areFiltersApplied, - createFilter, - createFilterChips, deleteFilterTab, getActiveFilters, getFilterTabs, getFilterVariables, - saveFilterTab + saveFilterTab, + getFilterOpts, + getFilterQueryParam } from "./filters"; import { getSortQueryVariables } from "./sort"; @@ -63,7 +70,6 @@ interface ProductListProps { } export const ProductList: React.FC = ({ params }) => { - const { locale } = useLocale(); const navigate = useNavigator(); const notify = useNotifier(); const paginate = usePaginator(); @@ -75,6 +81,31 @@ export const ProductList: React.FC = ({ params }) => { ListViews.PRODUCT_LIST ); const intl = useIntl(); + const { data: initialFilterData } = useInitialProductFilterDataQuery({ + variables: { + categories: params.categories, + collections: params.collections, + productTypes: params.productTypes + } + }); + const searchCategories = useCategorySearch({ + variables: { + ...DEFAULT_INITIAL_SEARCH_DATA, + first: 5 + } + }); + const searchCollections = useCollectionSearch({ + variables: { + ...DEFAULT_INITIAL_SEARCH_DATA, + first: 5 + } + }); + const searchProductTypes = useProductTypeSearch({ + variables: { + ...DEFAULT_INITIAL_SEARCH_DATA, + first: 5 + } + }); React.useEffect( () => @@ -103,21 +134,17 @@ export const ProductList: React.FC = ({ params }) => { ProductListUrlQueryParams >(navigate, productListUrl, params); - const changeFilters = (filters: ProductListUrlFilters) => { - reset(); - navigate(productListUrl(filters)); - }; - - const changeFilterField = (filter: ProductListUrlFilters) => { - reset(); - navigate( - productListUrl({ - ...getActiveFilters(params), - ...filter, - activeTab: undefined - }) - ); - }; + const [ + changeFilters, + resetFilters, + handleSearchChange + ] = createFilterHandlers({ + cleanupFn: reset, + createUrl: productListUrl, + getFilterQueryParam, + navigate, + params + }); const handleTabChange = (tab: number) => { reset(); @@ -162,6 +189,32 @@ export const ProductList: React.FC = ({ params }) => { [params, settings.rowNumber] ); + const filterOpts = getFilterOpts( + params, + maybe(() => initialFilterData.attributes.edges.map(edge => edge.node), []), + { + initial: maybe( + () => initialFilterData.categories.edges.map(edge => edge.node), + [] + ), + search: searchCategories + }, + { + initial: maybe( + () => initialFilterData.collections.edges.map(edge => edge.node), + [] + ), + search: searchCollections + }, + { + initial: maybe( + () => initialFilterData.productTypes.edges.map(edge => edge.node), + [] + ), + search: searchProductTypes + } + ); + return ( = ({ params }) => { defaultSettings={ defaultListSettings[ListViews.PRODUCT_LIST] } + filterOpts={filterOpts} gridAttributes={maybe( () => attributes.data.grid.edges.map(edge => edge.node), @@ -241,15 +295,6 @@ export const ProductList: React.FC = ({ params }) => { .hasNextPage, false )} - filtersList={createFilterChips( - params, - { - currencySymbol, - locale - }, - changeFilterField, - intl - )} onAdd={() => navigate(productAddUrl)} disabled={loading} products={maybe(() => @@ -288,11 +333,7 @@ export const ProductList: React.FC = ({ params }) => { onUpdateListSettings={updateListSettings} pageInfo={pageInfo} onRowClick={id => () => navigate(productUrl(id))} - onAll={() => - changeFilters({ - status: undefined - }) - } + onAll={resetFilters} toolbar={ <> - = ({ })} tabs={tabs} onAll={onAll} + onFilterChange={onFilterChange} onSearchChange={onSearchChange} onTabChange={onTabChange} onTabDelete={onTabDelete} diff --git a/src/services/components/ServiceListPage/filters.ts b/src/services/components/ServiceListPage/filters.ts new file mode 100644 index 000000000..cf864cf54 --- /dev/null +++ b/src/services/components/ServiceListPage/filters.ts @@ -0,0 +1,47 @@ +import { defineMessages, IntlShape } from "react-intl"; + +import { FilterOpts } from "@saleor/types"; +import { IFilter } from "@saleor/components/Filter"; +import { createBooleanField } from "@saleor/utils/filters/fields"; +import { commonMessages } from "@saleor/intl"; + +export enum ServiceFilterKeys { + active = "active" +} + +export interface ServiceListFilterOpts { + isActive: FilterOpts; +} + +const messages = defineMessages({ + active: { + defaultMessage: "Active", + description: "service account" + }, + deactivated: { + defaultMessage: "Inactive", + description: "service account" + } +}); + +export function createFilterStructure( + intl: IntlShape, + opts: ServiceListFilterOpts +): IFilter { + return [ + { + ...createBooleanField( + ServiceFilterKeys.active, + intl.formatMessage(commonMessages.status), + opts.isActive.value, + { + negative: intl.formatMessage(messages.deactivated), + positive: intl.formatMessage(messages.active) + } + ), + active: opts.isActive.active + } + ]; +} + +export default messages; diff --git a/src/services/components/ServiceListPage/index.ts b/src/services/components/ServiceListPage/index.ts index 0cbe92960..44625afe2 100644 --- a/src/services/components/ServiceListPage/index.ts +++ b/src/services/components/ServiceListPage/index.ts @@ -1,2 +1,3 @@ export { default } from "./ServiceListPage"; export * from "./ServiceListPage"; +export * from "./filters"; diff --git a/src/services/urls.ts b/src/services/urls.ts index d14e8e2f0..4f5a29d1d 100644 --- a/src/services/urls.ts +++ b/src/services/urls.ts @@ -15,6 +15,7 @@ export const serviceSection = "/services/"; export const serviceListPath = serviceSection; export enum ServiceListUrlFiltersEnum { + active = "active", query = "query" } export type ServiceListUrlFilters = Filters; diff --git a/src/services/views/ServiceList/ServiceList.tsx b/src/services/views/ServiceList/ServiceList.tsx index fe29a1826..6c06fde1b 100644 --- a/src/services/views/ServiceList/ServiceList.tsx +++ b/src/services/views/ServiceList/ServiceList.tsx @@ -20,6 +20,8 @@ import { ListViews } from "@saleor/types"; import { getSortParams } from "@saleor/utils/sort"; import createSortHandler from "@saleor/utils/handlers/sortHandler"; import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers"; +import useShop from "@saleor/hooks/useShop"; +import createFilterHandlers from "@saleor/utils/handlers/filterHandlers"; import ServiceDeleteDialog from "../../components/ServiceDeleteDialog"; import ServiceListPage from "../../components/ServiceListPage"; import { useServiceListQuery } from "../../queries"; @@ -27,7 +29,6 @@ import { serviceAddUrl, serviceListUrl, ServiceListUrlDialog, - ServiceListUrlFilters, ServiceListUrlQueryParams, serviceUrl } from "../../urls"; @@ -36,9 +37,11 @@ import { deleteFilterTab, getActiveFilters, getFilterTabs, + getFilterQueryParam, getFilterVariables, - saveFilterTab -} from "./filter"; + saveFilterTab, + getFilterOpts +} from "./filters"; import { getSortQueryVariables } from "./sort"; interface ServiceListProps { @@ -49,6 +52,7 @@ export const ServiceList: React.FC = ({ params }) => { const navigate = useNavigator(); const notify = useNotifier(); const paginate = usePaginator(); + const shop = useShop(); const { updateListSettings, settings } = useListSettings( ListViews.STAFF_MEMBERS_LIST ); @@ -77,14 +81,16 @@ export const ServiceList: React.FC = ({ params }) => { : 0 : parseInt(params.activeTab, 0); - const changeFilterField = (filter: ServiceListUrlFilters) => - navigate( - serviceListUrl({ - ...getActiveFilters(params), - ...filter, - activeTab: undefined - }) - ); + const [ + changeFilters, + resetFilters, + handleSearchChange + ] = createFilterHandlers({ + createUrl: serviceListUrl, + getFilterQueryParam, + navigate, + params + }); const [openModal, closeModal] = createDialogActionHandlers< ServiceListUrlDialog, @@ -129,6 +135,7 @@ export const ServiceList: React.FC = ({ params }) => { }; const handleSort = createSortHandler(navigate, serviceListUrl, params); + const currencySymbol = maybe(() => shop.defaultCurrency, "USD"); return ( @@ -143,10 +150,13 @@ export const ServiceList: React.FC = ({ params }) => { return ( <> changeFilterField({ query })} - onAll={() => navigate(serviceListUrl())} + onSearchChange={handleSearchChange} + onFilterChange={changeFilters} + onAll={resetFilters} onTabChange={handleTabChange} onTabDelete={() => openModal("delete-search")} onTabSave={() => openModal("save-search")} diff --git a/src/services/views/ServiceList/__snapshots__/filters.test.ts.snap b/src/services/views/ServiceList/__snapshots__/filters.test.ts.snap new file mode 100644 index 000000000..c63a81372 --- /dev/null +++ b/src/services/views/ServiceList/__snapshots__/filters.test.ts.snap @@ -0,0 +1,9 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Filtering URL params should not be empty if active filters are present 1`] = ` +Object { + "active": "false", +} +`; + +exports[`Filtering URL params should not be empty if active filters are present 2`] = `"active=false"`; diff --git a/src/services/views/ServiceList/filter.ts b/src/services/views/ServiceList/filter.ts deleted file mode 100644 index a1d2b7724..000000000 --- a/src/services/views/ServiceList/filter.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { ServiceAccountFilterInput } from "@saleor/types/globalTypes"; -import { - createFilterTabUtils, - createFilterUtils -} from "../../../utils/filters"; -import { - ServiceListUrlFilters, - ServiceListUrlFiltersEnum, - ServiceListUrlQueryParams -} from "../../urls"; - -export const STAFF_FILTERS_KEY = "staffFilters"; - -export function getFilterVariables( - params: ServiceListUrlFilters -): ServiceAccountFilterInput { - return { - search: params.query - }; -} - -export const { - deleteFilterTab, - getFilterTabs, - saveFilterTab -} = createFilterTabUtils(STAFF_FILTERS_KEY); - -export const { areFiltersApplied, getActiveFilters } = createFilterUtils< - ServiceListUrlQueryParams, - ServiceListUrlFilters ->(ServiceListUrlFiltersEnum); diff --git a/src/services/views/ServiceList/filters.test.ts b/src/services/views/ServiceList/filters.test.ts new file mode 100644 index 000000000..e52fe2de1 --- /dev/null +++ b/src/services/views/ServiceList/filters.test.ts @@ -0,0 +1,57 @@ +import { createIntl } from "react-intl"; +import { stringify as stringifyQs } from "qs"; + +import { ServiceListUrlFilters } from "@saleor/services/urls"; +import { createFilterStructure } from "@saleor/services/components/ServiceListPage"; +import { getFilterQueryParams } from "@saleor/utils/filters"; +import { getExistingKeys, setFilterOptsStatus } from "@test/filters"; +import { config } from "@test/intl"; +import { getFilterVariables, getFilterQueryParam } from "./filters"; + +describe("Filtering query params", () => { + it("should be empty object if no params given", () => { + const params: ServiceListUrlFilters = {}; + const filterVariables = getFilterVariables(params); + + expect(getExistingKeys(filterVariables)).toHaveLength(0); + }); + + it("should not be empty object if params given", () => { + const params: ServiceListUrlFilters = { + active: false.toString() + }; + const filterVariables = getFilterVariables(params); + + expect(getExistingKeys(filterVariables)).toHaveLength(1); + }); +}); + +describe("Filtering URL params", () => { + const intl = createIntl(config); + + const filters = createFilterStructure(intl, { + isActive: { + active: false, + value: false + } + }); + + it("should be empty if no active filters", () => { + const filterQueryParams = getFilterQueryParams( + filters, + getFilterQueryParam + ); + + expect(getExistingKeys(filterQueryParams)).toHaveLength(0); + }); + + it("should not be empty if active filters are present", () => { + const filterQueryParams = getFilterQueryParams( + setFilterOptsStatus(filters, true), + getFilterQueryParam + ); + + expect(filterQueryParams).toMatchSnapshot(); + expect(stringifyQs(filterQueryParams)).toMatchSnapshot(); + }); +}); diff --git a/src/services/views/ServiceList/filters.ts b/src/services/views/ServiceList/filters.ts new file mode 100644 index 000000000..f8eaddd18 --- /dev/null +++ b/src/services/views/ServiceList/filters.ts @@ -0,0 +1,65 @@ +import { ServiceAccountFilterInput } from "@saleor/types/globalTypes"; +import { + ServiceListFilterOpts, + ServiceFilterKeys +} from "@saleor/services/components/ServiceListPage/filters"; +import { maybe, parseBoolean } from "@saleor/misc"; +import { IFilterElement } from "@saleor/components/Filter"; +import { + ServiceListUrlFilters, + ServiceListUrlFiltersEnum, + ServiceListUrlQueryParams +} from "../../urls"; +import { + createFilterTabUtils, + createFilterUtils, + getSingleValueQueryParam +} from "../../../utils/filters"; + +export const STAFF_FILTERS_KEY = "staffFilters"; + +export function getFilterOpts( + params: ServiceListUrlFilters +): ServiceListFilterOpts { + return { + isActive: { + active: maybe(() => params.active !== undefined, false), + value: + params.active !== undefined ? parseBoolean(params.active, true) : true + } + }; +} + +export function getFilterVariables( + params: ServiceListUrlFilters +): ServiceAccountFilterInput { + return { + isActive: + params.active !== undefined + ? parseBoolean(params.active, true) + : undefined, + search: params.query + }; +} + +export function getFilterQueryParam( + filter: IFilterElement +): ServiceListUrlFilters { + const { name } = filter; + + switch (name) { + case ServiceFilterKeys.active: + return getSingleValueQueryParam(filter, ServiceListUrlFiltersEnum.active); + } +} + +export const { + deleteFilterTab, + getFilterTabs, + saveFilterTab +} = createFilterTabUtils(STAFF_FILTERS_KEY); + +export const { areFiltersApplied, getActiveFilters } = createFilterUtils< + ServiceListUrlQueryParams, + ServiceListUrlFilters +>(ServiceListUrlFiltersEnum); diff --git a/src/staff/components/StaffListPage/StaffListPage.tsx b/src/staff/components/StaffListPage/StaffListPage.tsx index 6cd2d1211..b4faf0728 100644 --- a/src/staff/components/StaffListPage/StaffListPage.tsx +++ b/src/staff/components/StaffListPage/StaffListPage.tsx @@ -6,21 +6,26 @@ import { FormattedMessage, useIntl } from "react-intl"; import AppHeader from "@saleor/components/AppHeader"; import { Container } from "@saleor/components/Container"; import PageHeader from "@saleor/components/PageHeader"; -import SearchBar from "@saleor/components/SearchBar"; +import FilterBar from "@saleor/components/FilterBar"; import { sectionNames } from "@saleor/intl"; import { ListProps, - SearchPageProps, + FilterPageProps, TabPageProps, SortPage } from "@saleor/types"; import { StaffListUrlSortField } from "@saleor/staff/urls"; import { StaffList_staffUsers_edges_node } from "../../types/StaffList"; import StaffList from "../StaffList/StaffList"; +import { + createFilterStructure, + StaffFilterKeys, + StaffListFilterOpts +} from "./filters"; export interface StaffListPageProps extends ListProps, - SearchPageProps, + FilterPageProps, SortPage, TabPageProps { staffMembers: StaffList_staffUsers_edges_node[]; @@ -29,11 +34,14 @@ export interface StaffListPageProps } const StaffListPage: React.FC = ({ + currencySymbol, currentTab, + filterOpts, initialSearch, onAdd, onAll, onBack, + onFilterChange, onSearchChange, onTabChange, onTabDelete, @@ -43,6 +51,8 @@ const StaffListPage: React.FC = ({ }) => { const intl = useIntl(); + const structure = createFilterStructure(intl, filterOpts); + return ( @@ -57,18 +67,21 @@ const StaffListPage: React.FC = ({ - ; +} + +const messages = defineMessages({ + active: { + defaultMessage: "Active", + description: "staff member's account" + }, + deactivated: { + defaultMessage: "Deactivated", + description: "staff member's account" + }, + status: { + defaultMessage: "Status", + description: "staff member's account" + } +}); + +export function createFilterStructure( + intl: IntlShape, + opts: StaffListFilterOpts +): IFilter { + return [ + { + ...createOptionsField( + StaffFilterKeys.status, + intl.formatMessage(messages.status), + [opts.status.value], + false, + [ + { + label: intl.formatMessage(messages.active), + value: StaffMemberStatus.ACTIVE + }, + { + label: intl.formatMessage(messages.deactivated), + value: StaffMemberStatus.DEACTIVATED + } + ] + ), + active: opts.status.active + } + ]; +} diff --git a/src/staff/components/StaffListPage/index.ts b/src/staff/components/StaffListPage/index.ts index acbe4f3df..de24e5b17 100644 --- a/src/staff/components/StaffListPage/index.ts +++ b/src/staff/components/StaffListPage/index.ts @@ -1,2 +1,3 @@ export { default } from "./StaffListPage"; export * from "./StaffListPage"; +export * from "./filters"; diff --git a/src/staff/urls.ts b/src/staff/urls.ts index 970299df9..35ce6c2a4 100644 --- a/src/staff/urls.ts +++ b/src/staff/urls.ts @@ -15,6 +15,7 @@ const staffSection = "/staff/"; export const staffListPath = staffSection; export enum StaffListUrlFiltersEnum { + status = "status", query = "query" } export type StaffListUrlFilters = Filters; diff --git a/src/staff/views/StaffList/StaffList.tsx b/src/staff/views/StaffList/StaffList.tsx index c4ccefadd..393bba370 100644 --- a/src/staff/views/StaffList/StaffList.tsx +++ b/src/staff/views/StaffList/StaffList.tsx @@ -23,6 +23,7 @@ import { ListViews } from "@saleor/types"; import { getSortParams } from "@saleor/utils/sort"; import createSortHandler from "@saleor/utils/handlers/sortHandler"; import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers"; +import createFilterHandlers from "@saleor/utils/handlers/filterHandlers"; import StaffAddMemberDialog, { FormData as AddStaffMemberForm } from "../../components/StaffAddMemberDialog"; @@ -33,7 +34,6 @@ import { StaffMemberAdd } from "../../types/StaffMemberAdd"; import { staffListUrl, StaffListUrlDialog, - StaffListUrlFilters, StaffListUrlQueryParams, staffMemberDetailsUrl } from "../../urls"; @@ -43,8 +43,10 @@ import { getActiveFilters, getFilterTabs, getFilterVariables, - saveFilterTab -} from "./filter"; + saveFilterTab, + getFilterQueryParam, + getFilterOpts +} from "./filters"; import { getSortQueryVariables } from "./sort"; interface StaffListProps { @@ -62,6 +64,7 @@ export const StaffList: React.FC = ({ params }) => { const shop = useShop(); const paginationState = createPaginationState(settings.rowNumber, params); + const currencySymbol = maybe(() => shop.defaultCurrency, "USD"); const queryVariables = React.useMemo( () => ({ ...paginationState, @@ -84,14 +87,16 @@ export const StaffList: React.FC = ({ params }) => { : 0 : parseInt(params.activeTab, 0); - const changeFilterField = (filter: StaffListUrlFilters) => - navigate( - staffListUrl({ - ...getActiveFilters(params), - ...filter, - activeTab: undefined - }) - ); + const [ + changeFilters, + resetFilters, + handleSearchChange + ] = createFilterHandlers({ + createUrl: staffListUrl, + getFilterQueryParam, + navigate, + params + }); const [openModal, closeModal] = createDialogActionHandlers< StaffListUrlDialog, @@ -159,10 +164,13 @@ export const StaffList: React.FC = ({ params }) => { return ( <> changeFilterField({ query })} - onAll={() => navigate(staffListUrl())} + onSearchChange={handleSearchChange} + onFilterChange={changeFilters} + onAll={resetFilters} onTabChange={handleTabChange} onTabDelete={() => openModal("delete-search")} onTabSave={() => openModal("save-search")} diff --git a/src/staff/views/StaffList/__snapshots__/filters.test.ts.snap b/src/staff/views/StaffList/__snapshots__/filters.test.ts.snap new file mode 100644 index 000000000..124fbeb79 --- /dev/null +++ b/src/staff/views/StaffList/__snapshots__/filters.test.ts.snap @@ -0,0 +1,9 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Filtering URL params should not be empty if active filters are present 1`] = ` +Object { + "status": "ACTIVE", +} +`; + +exports[`Filtering URL params should not be empty if active filters are present 2`] = `"status=ACTIVE"`; diff --git a/src/staff/views/StaffList/filter.ts b/src/staff/views/StaffList/filter.ts deleted file mode 100644 index 9223a0372..000000000 --- a/src/staff/views/StaffList/filter.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { StaffUserInput } from "@saleor/types/globalTypes"; -import { - createFilterTabUtils, - createFilterUtils -} from "../../../utils/filters"; -import { - StaffListUrlFilters, - StaffListUrlFiltersEnum, - StaffListUrlQueryParams -} from "../../urls"; - -export const STAFF_FILTERS_KEY = "staffFilters"; - -export function getFilterVariables( - params: StaffListUrlFilters -): StaffUserInput { - return { - search: params.query - }; -} - -export const { - deleteFilterTab, - getFilterTabs, - saveFilterTab -} = createFilterTabUtils(STAFF_FILTERS_KEY); - -export const { areFiltersApplied, getActiveFilters } = createFilterUtils< - StaffListUrlQueryParams, - StaffListUrlFilters ->(StaffListUrlFiltersEnum); diff --git a/src/staff/views/StaffList/filters.test.ts b/src/staff/views/StaffList/filters.test.ts new file mode 100644 index 000000000..32a812b2d --- /dev/null +++ b/src/staff/views/StaffList/filters.test.ts @@ -0,0 +1,58 @@ +import { createIntl } from "react-intl"; +import { stringify as stringifyQs } from "qs"; + +import { StaffListUrlFilters } from "@saleor/staff/urls"; +import { createFilterStructure } from "@saleor/staff/components/StaffListPage"; +import { getFilterQueryParams } from "@saleor/utils/filters"; +import { getExistingKeys, setFilterOptsStatus } from "@test/filters"; +import { config } from "@test/intl"; +import { StaffMemberStatus } from "@saleor/types/globalTypes"; +import { getFilterVariables, getFilterQueryParam } from "./filters"; + +describe("Filtering query params", () => { + it("should be empty object if no params given", () => { + const params: StaffListUrlFilters = {}; + const filterVariables = getFilterVariables(params); + + expect(getExistingKeys(filterVariables)).toHaveLength(0); + }); + + it("should not be empty object if params given", () => { + const params: StaffListUrlFilters = { + status: StaffMemberStatus.ACTIVE + }; + const filterVariables = getFilterVariables(params); + + expect(getExistingKeys(filterVariables)).toHaveLength(1); + }); +}); + +describe("Filtering URL params", () => { + const intl = createIntl(config); + + const filters = createFilterStructure(intl, { + status: { + active: false, + value: StaffMemberStatus.ACTIVE + } + }); + + it("should be empty if no active filters", () => { + const filterQueryParams = getFilterQueryParams( + filters, + getFilterQueryParam + ); + + expect(getExistingKeys(filterQueryParams)).toHaveLength(0); + }); + + it("should not be empty if active filters are present", () => { + const filterQueryParams = getFilterQueryParams( + setFilterOptsStatus(filters, true), + getFilterQueryParam + ); + + expect(filterQueryParams).toMatchSnapshot(); + expect(stringifyQs(filterQueryParams)).toMatchSnapshot(); + }); +}); diff --git a/src/staff/views/StaffList/filters.ts b/src/staff/views/StaffList/filters.ts new file mode 100644 index 000000000..cfb46d078 --- /dev/null +++ b/src/staff/views/StaffList/filters.ts @@ -0,0 +1,67 @@ +import { StaffUserInput, StaffMemberStatus } from "@saleor/types/globalTypes"; +import { maybe, findValueInEnum } from "@saleor/misc"; +import { IFilterElement } from "@saleor/components/Filter"; +import { + StaffListFilterOpts, + StaffFilterKeys +} from "@saleor/staff/components/StaffListPage"; +import { + createFilterTabUtils, + createFilterUtils, + getSingleEnumValueQueryParam +} from "../../../utils/filters"; +import { + StaffListUrlFilters, + StaffListUrlFiltersEnum, + StaffListUrlQueryParams +} from "../../urls"; + +export const STAFF_FILTERS_KEY = "staffFilters"; + +export function getFilterOpts( + params: StaffListUrlFilters +): StaffListFilterOpts { + return { + status: { + active: maybe(() => params.status !== undefined, false), + value: maybe(() => findValueInEnum(params.status, StaffMemberStatus)) + } + }; +} + +export function getFilterVariables( + params: StaffListUrlFilters +): StaffUserInput { + return { + search: params.query, + status: params.status + ? findValueInEnum(params.status, StaffMemberStatus) + : null + }; +} + +export function getFilterQueryParam( + filter: IFilterElement +): StaffListUrlFilters { + const { name } = filter; + + switch (name) { + case StaffFilterKeys.status: + return getSingleEnumValueQueryParam( + filter, + StaffListUrlFiltersEnum.status, + StaffMemberStatus + ); + } +} + +export const { + deleteFilterTab, + getFilterTabs, + saveFilterTab +} = createFilterTabUtils(STAFF_FILTERS_KEY); + +export const { areFiltersApplied, getActiveFilters } = createFilterUtils< + StaffListUrlQueryParams, + StaffListUrlFilters +>(StaffListUrlFiltersEnum); diff --git a/src/storybook/__snapshots__/Stories.test.ts.snap b/src/storybook/__snapshots__/Stories.test.ts.snap index e977ef717..2d8f82911 100644 --- a/src/storybook/__snapshots__/Stories.test.ts.snap +++ b/src/storybook/__snapshots__/Stories.test.ts.snap @@ -1388,64 +1388,912 @@ exports[`Storyshots Generics / Filter default 1`] = ` style="padding:24px" >
-
-
-
+
+
+ +
+
+
+
+
+
+
+ + + +
+
+
+ + +
+
+ + and + +
+
+ + +
+
+
+
+
+ +
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+
+ +
+
+
+ +
+
+ +`; + +exports[`Storyshots Generics / Filter interactive 1`] = ` +
+
+
+
+
+
+ Filters +
+
+ + +
+
+
+
+ +
+
+
+
+
+
+
+ + + +
+
+
+ + +
+
+ + and + +
+
+ + +
+
+
+
+
+ +
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+
+ +
+
+
+
@@ -11587,17 +12435,30 @@ exports[`Storyshots Views / Attributes / Attribute list default 1`] = `
+
+ +
-
@@ -12566,17 +13424,30 @@ exports[`Storyshots Views / Attributes / Attribute list loading 1`] = `
+
+ +
-
@@ -12961,17 +13829,30 @@ exports[`Storyshots Views / Attributes / Attribute list no data 1`] = `
+
+ +
-
@@ -14204,17 +15082,17 @@ exports[`Storyshots Views / Categories / Category list default 1`] = `
-
@@ -14753,17 +15628,17 @@ exports[`Storyshots Views / Categories / Category list empty 1`] = `
-
@@ -15074,17 +15946,17 @@ exports[`Storyshots Views / Categories / Category list loading 1`] = `
-
@@ -26351,17 +27220,30 @@ exports[`Storyshots Views / Collections / Collection list default 1`] = `
+
+ +
-
@@ -26937,17 +27816,30 @@ exports[`Storyshots Views / Collections / Collection list loading 1`] = `
+
+ +
-
@@ -27318,17 +28207,30 @@ exports[`Storyshots Views / Collections / Collection list no data 1`] = `
+
+ +
-
@@ -37327,17 +38226,30 @@ exports[`Storyshots Views / Customers / Customer list default 1`] = `
+
+ +
-
@@ -38470,17 +39379,30 @@ exports[`Storyshots Views / Customers / Customer list loading 1`] = `
+
+ +
-
@@ -38842,17 +39761,30 @@ exports[`Storyshots Views / Customers / Customer list no data 1`] = `
+
+ +
-
@@ -44350,17 +45279,30 @@ exports[`Storyshots Views / Discounts / Sale list default 1`] = `
+
+ +
-
@@ -44916,17 +45855,30 @@ exports[`Storyshots Views / Discounts / Sale list loading 1`] = `
+
+ +
-
@@ -45312,17 +46261,30 @@ exports[`Storyshots Views / Discounts / Sale list no data 1`] = `
+
+ +
-
@@ -50532,17 +51491,30 @@ exports[`Storyshots Views / Discounts / Voucher list default 1`] = `
+
+ +
-
@@ -51011,17 +51980,30 @@ exports[`Storyshots Views / Discounts / Voucher list loading 1`] = `
+
+ +
-
@@ -51453,17 +52432,30 @@ exports[`Storyshots Views / Discounts / Voucher list no data 1`] = `
+
+ +
-
@@ -55949,17 +56938,30 @@ exports[`Storyshots Views / Orders / Draft order list default 1`] = `
+
+ +
-
@@ -57191,17 +58190,30 @@ exports[`Storyshots Views / Orders / Draft order list loading 1`] = `
+
+ +
-
@@ -57592,17 +58601,30 @@ exports[`Storyshots Views / Orders / Draft order list when no data 1`] = `
+
+ +
-
@@ -73101,7 +74120,7 @@ exports[`Storyshots Views / Orders / Order list default 1`] = `
-
-
@@ -74754,7 +75759,7 @@ exports[`Storyshots Views / Orders / Order list loading 1`] = `
-
+ -
@@ -75225,7 +76227,7 @@ exports[`Storyshots Views / Orders / Order list when no data 1`] = `
-
-
@@ -75535,2054 +76523,6 @@ exports[`Storyshots Views / Orders / Order list when no data 1`] = `
`; -exports[`Storyshots Views / Orders / Order list with custom filters 1`] = ` -
-
-
-
- Orders -
-
-
- -
-
-
-
-
-
-
- - -
-
-
-
-
- -
-
-
- - -
-
-
-
-
-
-
- Property X is 0 -
- -
-
-
- Property Y is 1 -
- -
-
-
- Property Z is 2 -
- -
-
-
- Property X is 3 -
- -
-
-
- Property Y is 4 -
- -
-
-
- Property Z is 5 -
- -
-
-
- Property X is 6 -
- -
-
-
- Property Y is 7 -
- -
-
-
- Property Z is 8 -
- -
-
-
- Property X is 9 -
- -
-
-
- Property Y is 10 -
- -
-
-
- Property Z is 11 -
- -
-
-
- Property X is 12 -
- -
-
-
- Property Y is 13 -
- -
-
-
- Property Z is 14 -
- -
-
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - -
-
- No. of Order -
- -
-
-
-
- Date -
-
-
-
-
- Customer -
-
-
-
-
- Payment -
-
-
-
-
- Fulfillment status -
-
-
-
-
- Total -
-
-
-
-
-
-
-`; - exports[`Storyshots Views / Pages / Page details default 1`] = `
+
+
+
+ + +
+
+
+
+
+ +
+
+
+ + +
+
+
@@ -82528,6 +81552,90 @@ exports[`Storyshots Views / Plugins / Plugin list loading 1`] = `
+
+
+
+ + +
+
+
+
+
+ +
+
+
+ + +
+
+
@@ -82783,6 +81891,90 @@ exports[`Storyshots Views / Plugins / Plugin list no data 1`] = `
+
+
+
+ + +
+
+
+
+
+ +
+
+
+ + +
+
+
@@ -85942,17 +85134,30 @@ exports[`Storyshots Views / Product types / Product types list default 1`] = `
+
+ +
-
@@ -86450,17 +85652,30 @@ exports[`Storyshots Views / Product types / Product types list loading 1`] = `
+
+ +
-
@@ -86785,17 +85997,30 @@ exports[`Storyshots Views / Product types / Product types list no data 1`] = `
+
+ +
-
@@ -104524,7 +103746,7 @@ exports[`Storyshots Views / Products / Product list default 1`] = `
-
-
@@ -106239,7 +105447,7 @@ exports[`Storyshots Views / Products / Product list loading 1`] = `
-
+
-
@@ -106741,7 +105946,7 @@ exports[`Storyshots Views / Products / Product list no data 1`] = `
-
-
@@ -107046,2116 +106237,6 @@ exports[`Storyshots Views / Products / Product list no data 1`] = `
`; -exports[`Storyshots Views / Products / Product list with custom filters 1`] = ` -
-
-
-
- Products -
-
-
-
- -
- -
-
-
-
-
-
-
- - -
-
-
-
-
- -
-
-
- - -
-
-
-
-
-
-
- Property X is 0 -
- -
-
-
- Property Y is 1 -
- -
-
-
- Property Z is 2 -
- -
-
-
- Property X is 3 -
- -
-
-
- Property Y is 4 -
- -
-
-
- Property Z is 5 -
- -
-
-
- Property X is 6 -
- -
-
-
- Property Y is 7 -
- -
-
-
- Property Z is 8 -
- -
-
-
- Property X is 9 -
- -
-
-
- Property Y is 10 -
- -
-
-
- Property Z is 11 -
- -
-
-
- Property X is 12 -
- -
-
-
- Property Y is 13 -
- -
-
-
- Property Z is 14 -
- -
-
-
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - -
-
- - Name - -
- -
-
-
-
- Type -
-
-
-
-
- Published -
-
-
-
-
- Price -
-
-
-
-
-
-
-
-`; - exports[`Storyshots Views / Products / Product variant details attribute errors 1`] = `
+
+ +
-
@@ -115596,17 +112687,30 @@ exports[`Storyshots Views / Services / Service list loading 1`] = `
+
+ +
-
@@ -115951,17 +113052,30 @@ exports[`Storyshots Views / Services / Service list no data 1`] = `
+
+ +
-
@@ -124770,17 +121881,30 @@ exports[`Storyshots Views / Staff / Staff members default 1`] = `
+
+ +
-
@@ -125439,17 +122560,30 @@ exports[`Storyshots Views / Staff / Staff members when loading 1`] = `
+
+ +
-
@@ -127068,17 +124199,17 @@ exports[`Storyshots Views / Translations / Entity list default 1`] = `
-
@@ -130952,17 +128080,30 @@ exports[`Storyshots Views / Webhooks / Webhook list default 1`] = `
+
+ +
-
@@ -131375,17 +128513,30 @@ exports[`Storyshots Views / Webhooks / Webhook list loading 1`] = `
+
+ +
-
@@ -131747,17 +128895,30 @@ exports[`Storyshots Views / Webhooks / Webhook list no data 1`] = `
+
+ +
-
diff --git a/src/storybook/mock.tsx b/src/storybook/mock.tsx index 0e6b83f0f..7b7d3e839 100644 --- a/src/storybook/mock.tsx +++ b/src/storybook/mock.tsx @@ -1,91 +1,99 @@ import React from "react"; import { SingleAutocompleteChoiceType } from "@saleor/components/SingleAutocompleteSelectField/SingleAutocompleteSelectFieldContent"; +import { FetchMoreProps } from "@saleor/types"; interface ChoiceProviderProps { - children: (props: { - choices: SingleAutocompleteChoiceType[]; - hasMore: boolean; - loading: boolean; - fetchChoices: (value: string) => void; - fetchMore: () => void; - }) => React.ReactElement; + children: ( + props: FetchMoreProps & { + choices: SingleAutocompleteChoiceType[]; + fetchChoices: (value: string) => void; + } + ) => React.ReactElement; choices: SingleAutocompleteChoiceType[]; } -interface ChoiceProviderState { - choices: SingleAutocompleteChoiceType[]; - filteredChoices: SingleAutocompleteChoiceType[]; - first: number; - loading: boolean; - timeout: any; -} const step = 5; +const loadingTime = 400; -export class ChoiceProvider extends React.Component< - ChoiceProviderProps, - ChoiceProviderState -> { - state = { - choices: [], - filteredChoices: [], - first: step, - loading: false, - timeout: null - }; - - handleChange = (inputValue: string) => { - if (!!this.state.timeout) { - clearTimeout(this.state.timeout); - } - const timeout = setTimeout(() => this.fetchChoices(inputValue), 500); - this.setState({ - loading: true, - timeout - }); - }; - - handleFetchMore = () => { - if (!!this.state.timeout) { - clearTimeout(this.state.timeout); - } - const timeout = setTimeout(this.fetchMore, 500); - this.setState({ - 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 - }); - }; - - render() { - return this.props.children({ - choices: this.state.filteredChoices, - fetchChoices: this.handleChange, - fetchMore: this.handleFetchMore, - hasMore: this.state.choices.length > this.state.filteredChoices.length, - loading: this.state.loading - }); - } +export interface UseMockChoiceProviderOpts extends FetchMoreProps { + fetchChoices: (value: string) => void; } +export type UseMockChoiceProvider = [ + SingleAutocompleteChoiceType[], + UseMockChoiceProviderOpts +]; +export function useMockChoiceProvider( + choices: SingleAutocompleteChoiceType[] +): UseMockChoiceProvider { + const [filteredChoices, setFilteredChoices] = React.useState( + choices.slice(0, step) + ); + const [loading, setLoading] = React.useState(false); + const [first, setFirst] = React.useState(step); + const timeout = React.useRef(null); + + React.useEffect( + () => () => { + if (timeout.current) { + clearTimeout(timeout.current); + } + }, + [] + ); + + const handleChange = (value: string) => { + if (!!timeout.current) { + clearTimeout(timeout.current); + } + timeout.current = setTimeout(() => fetchChoices(value), loadingTime); + }; + + const fetchChoices = (value: string) => { + const filteredChoices = choices.filter( + suggestion => + !value || + suggestion.label.toLowerCase().indexOf(value.toLowerCase()) !== -1 + ); + + setLoading(true); + + timeout.current = setTimeout(() => { + setFilteredChoices(filteredChoices); + setLoading(false); + setFirst(step); + }, loadingTime); + }; + + const handleFetchMore = () => { + setLoading(true); + + timeout.current = setTimeout(() => { + setFilteredChoices(choices.slice(0, first + step)); + setLoading(false); + setFirst(first + step); + }, loadingTime); + }; + + return [ + filteredChoices, + { + fetchChoices: handleChange, + hasMore: choices.length > filteredChoices.length, + loading, + onFetchMore: handleFetchMore + } + ]; +} + +export const ChoiceProvider: React.FC = ({ + children, + choices +}) => { + const [filteredChoices, opts] = useMockChoiceProvider(choices); + + return children({ + choices: filteredChoices, + ...opts + }); +}; diff --git a/src/storybook/stories/attributes/AttributeListPage.tsx b/src/storybook/stories/attributes/AttributeListPage.tsx index 020aa6e9e..61bfc3038 100644 --- a/src/storybook/stories/attributes/AttributeListPage.tsx +++ b/src/storybook/stories/attributes/AttributeListPage.tsx @@ -10,7 +10,8 @@ import { pageListProps, searchPageProps, tabPageProps, - sortPageProps + sortPageProps, + filterPageProps } from "@saleor/fixtures"; import { AttributeListUrlSortField } from "@saleor/attributes/urls"; import Decorator from "../../Decorator"; @@ -20,7 +21,34 @@ const props: AttributeListPageProps = { ...listActionsProps, ...tabPageProps, ...searchPageProps, + ...filterPageProps, attributes, + filterOpts: { + availableInGrid: { + active: false, + value: false + }, + filterableInDashboard: { + active: false, + value: false + }, + filterableInStorefront: { + active: false, + value: false + }, + isVariantOnly: { + active: false, + value: false + }, + valueRequired: { + active: false, + value: false + }, + visibleInStorefront: { + active: false, + value: false + } + }, onBack: () => undefined, onSort: () => undefined, sort: { diff --git a/src/storybook/stories/collections/CollectionListPage.tsx b/src/storybook/stories/collections/CollectionListPage.tsx index 80b741a6f..b5a0c0348 100644 --- a/src/storybook/stories/collections/CollectionListPage.tsx +++ b/src/storybook/stories/collections/CollectionListPage.tsx @@ -2,6 +2,7 @@ import { storiesOf } from "@storybook/react"; import React from "react"; import { CollectionListUrlSortField } from "@saleor/collections/urls"; +import { CollectionPublished } from "@saleor/types/globalTypes"; import CollectionListPage, { CollectionListPageProps } from "../../../collections/components/CollectionListPage"; @@ -11,7 +12,8 @@ import { pageListProps, searchPageProps, tabPageProps, - sortPageProps + sortPageProps, + filterPageProps } from "../../../fixtures"; import Decorator from "../../Decorator"; @@ -20,6 +22,13 @@ const props: CollectionListPageProps = { ...pageListProps.default, ...searchPageProps, ...sortPageProps, + ...filterPageProps, + filterOpts: { + status: { + active: false, + value: CollectionPublished.PUBLISHED + } + }, sort: { ...sortPageProps.sort, sort: CollectionListUrlSortField.name diff --git a/src/storybook/stories/components/Filter.tsx b/src/storybook/stories/components/Filter.tsx index 96cbf23de..5875ebf68 100644 --- a/src/storybook/stories/components/Filter.tsx +++ b/src/storybook/stories/components/Filter.tsx @@ -1,124 +1,92 @@ import { storiesOf } from "@storybook/react"; import React from "react"; +import { FilterContent, FilterContentProps } from "@saleor/components/Filter"; import { - FieldType, - FilterContent, - FilterContentProps -} from "@saleor/components/Filter"; -import CardDecorator from "../../CardDecorator"; + createPriceField, + createDateField, + createOptionsField +} from "@saleor/utils/filters/fields"; +import useFilter from "@saleor/components/Filter/useFilter"; import Decorator from "../../Decorator"; const props: FilterContentProps = { currencySymbol: "USD", filters: [ + createPriceField("price", "Price", { + max: "100.00", + min: "20.00" + }), { - children: [], - data: { - fieldLabel: "Category Name", - type: FieldType.text - }, - label: "Category", - value: "category" + ...createDateField("createdAt", "Created At", { + max: "2019-10-23", + min: "2019-09-09" + }), + active: true }, { - children: [], - data: { - fieldLabel: "Product Type Name", - type: FieldType.text - }, - label: "Product Type", - value: "product-type" + ...createOptionsField("status", "Status", ["val1"], false, [ + { + label: "Value 1", + value: "val1" + }, + { + label: "Value 2", + value: "val2" + }, + { + label: "Value 3", + value: "val3" + } + ]), + active: true }, { - children: [], - data: { - fieldLabel: "Status", - options: [ + ...createOptionsField( + "multiplOptions", + "Multiple Options", + ["val1", "val2"], + true, + [ { - label: "Published", - value: true + label: "Value 1", + value: "val1" }, { - label: "Hidden", - value: false + label: "Value 2", + value: "val2" + }, + { + label: "Value 3", + value: "val3" } - ], - type: FieldType.select - }, - label: "Published", - value: "published" - }, - { - children: [], - data: { - fieldLabel: "Stock", - type: FieldType.range - }, - label: "Stock", - value: "stock" - }, - { - children: [ - { - children: [], - data: { - fieldLabel: "Equal to", - type: FieldType.date - }, - label: "Equal to", - value: "date-equal" - }, - { - children: [], - data: { - fieldLabel: "Range", - type: FieldType.rangeDate - }, - label: "Range", - value: "date-range" - } - ], - data: { - fieldLabel: "Date", - type: FieldType.select - }, - label: "Date", - value: "date" - }, - { - children: [ - { - children: [], - data: { - fieldLabel: "Exactly", - type: FieldType.price - }, - label: "Exactly", - value: "price-exactly" - }, - { - children: [], - data: { - fieldLabel: "Range", - type: FieldType.rangePrice - }, - label: "Range", - value: "price-range" - } - ], - data: { - fieldLabel: "Price", - type: FieldType.select - }, - label: "Price", - value: "price" + ] + ), + active: false } ], + onClear: () => undefined, + onFilterPropertyChange: () => undefined, onSubmit: () => undefined }; +const InteractiveStory: React.FC = () => { + const [data, dispatchFilterActions, clear] = useFilter(props.filters); + + return ( + + ); +}; + storiesOf("Generics / Filter", module) - .addDecorator(CardDecorator) + .addDecorator(storyFn => ( +
{storyFn()}
+ )) .addDecorator(Decorator) - .add("default", () => ); + .add("default", () => ) + .add("interactive", () => ); diff --git a/src/storybook/stories/customers/CustomerListPage.tsx b/src/storybook/stories/customers/CustomerListPage.tsx index 2ec0d992a..b6d1c2709 100644 --- a/src/storybook/stories/customers/CustomerListPage.tsx +++ b/src/storybook/stories/customers/CustomerListPage.tsx @@ -11,17 +11,42 @@ import { pageListProps, searchPageProps, tabPageProps, - sortPageProps + sortPageProps, + filterPageProps } from "../../../fixtures"; import Decorator from "../../Decorator"; const props: CustomerListPageProps = { + ...filterPageProps, ...listActionsProps, ...pageListProps.default, ...searchPageProps, ...sortPageProps, ...tabPageProps, customers: customerList, + filterOpts: { + joined: { + active: false, + value: { + max: undefined, + min: undefined + } + }, + moneySpent: { + active: false, + value: { + max: undefined, + min: undefined + } + }, + numberOfOrders: { + active: false, + value: { + max: undefined, + min: undefined + } + } + }, sort: { ...sortPageProps.sort, sort: CustomerListUrlSortField.name diff --git a/src/storybook/stories/discounts/SaleListPage.tsx b/src/storybook/stories/discounts/SaleListPage.tsx index 07eb24509..7dfd58feb 100644 --- a/src/storybook/stories/discounts/SaleListPage.tsx +++ b/src/storybook/stories/discounts/SaleListPage.tsx @@ -2,6 +2,10 @@ import { storiesOf } from "@storybook/react"; import React from "react"; import { SaleListUrlSortField } from "@saleor/discounts/urls"; +import { + DiscountValueTypeEnum, + DiscountStatusEnum +} from "@saleor/types/globalTypes"; import SaleListPage, { SaleListPageProps } from "../../../discounts/components/SaleListPage"; @@ -9,19 +13,36 @@ import { saleList } from "../../../discounts/fixtures"; import { listActionsProps, pageListProps, - searchPageProps, tabPageProps, - sortPageProps + sortPageProps, + filterPageProps } from "../../../fixtures"; import Decorator from "../../Decorator"; const props: SaleListPageProps = { ...listActionsProps, ...pageListProps.default, - ...searchPageProps, + ...filterPageProps, ...sortPageProps, ...tabPageProps, defaultCurrency: "USD", + filterOpts: { + saleType: { + active: false, + value: DiscountValueTypeEnum.FIXED + }, + started: { + active: false, + value: { + max: undefined, + min: undefined + } + }, + status: { + active: false, + value: [DiscountStatusEnum.ACTIVE] + } + }, sales: saleList, sort: { ...sortPageProps.sort, diff --git a/src/storybook/stories/discounts/VoucherListPage.tsx b/src/storybook/stories/discounts/VoucherListPage.tsx index ffa4dff49..89b232735 100644 --- a/src/storybook/stories/discounts/VoucherListPage.tsx +++ b/src/storybook/stories/discounts/VoucherListPage.tsx @@ -2,6 +2,10 @@ import { storiesOf } from "@storybook/react"; import React from "react"; import { VoucherListUrlSortField } from "@saleor/discounts/urls"; +import { + VoucherDiscountType, + DiscountStatusEnum +} from "@saleor/types/globalTypes"; import VoucherListPage, { VoucherListPageProps } from "../../../discounts/components/VoucherListPage"; @@ -11,7 +15,8 @@ import { pageListProps, searchPageProps, tabPageProps, - sortPageProps + sortPageProps, + filterPageProps } from "../../../fixtures"; import Decorator from "../../Decorator"; @@ -21,7 +26,32 @@ const props: VoucherListPageProps = { ...searchPageProps, ...sortPageProps, ...tabPageProps, + ...filterPageProps, defaultCurrency: "USD", + filterOpts: { + saleType: { + active: false, + value: [VoucherDiscountType.FIXED, VoucherDiscountType.PERCENTAGE] + }, + started: { + active: false, + value: { + max: undefined, + min: undefined + } + }, + status: { + active: false, + value: [DiscountStatusEnum.ACTIVE] + }, + timesUsed: { + active: false, + value: { + max: undefined, + min: undefined + } + } + }, sort: { ...sortPageProps.sort, sort: VoucherListUrlSortField.code diff --git a/src/storybook/stories/orders/OrderDraftListPage.tsx b/src/storybook/stories/orders/OrderDraftListPage.tsx index 429a40b36..85d8ffec3 100644 --- a/src/storybook/stories/orders/OrderDraftListPage.tsx +++ b/src/storybook/stories/orders/OrderDraftListPage.tsx @@ -7,7 +7,8 @@ import { pageListProps, searchPageProps, tabPageProps, - sortPageProps + sortPageProps, + filterPageProps } from "../../../fixtures"; import OrderDraftListPage, { OrderDraftListPageProps @@ -21,6 +22,20 @@ const props: OrderDraftListPageProps = { ...searchPageProps, ...sortPageProps, ...tabPageProps, + ...filterPageProps, + filterOpts: { + created: { + active: false, + value: { + max: undefined, + min: undefined + } + }, + customer: { + active: false, + value: undefined + } + }, onAdd: () => undefined, orders, sort: { diff --git a/src/storybook/stories/orders/OrderListPage.tsx b/src/storybook/stories/orders/OrderListPage.tsx index c3095aee4..5fb565871 100644 --- a/src/storybook/stories/orders/OrderListPage.tsx +++ b/src/storybook/stories/orders/OrderListPage.tsx @@ -5,9 +5,9 @@ import OrderListPage, { OrderListPageProps } from "@saleor/orders/components/OrderListPage"; import { OrderListUrlSortField } from "@saleor/orders/urls"; +import { OrderStatusFilter } from "@saleor/types/globalTypes"; import { filterPageProps, - filters, listActionsProps, pageListProps, sortPageProps @@ -20,6 +20,19 @@ const props: OrderListPageProps = { ...pageListProps.default, ...filterPageProps, ...sortPageProps, + filterOpts: { + created: { + active: false, + value: { + max: "400", + min: "50" + } + }, + status: { + active: false, + value: [OrderStatusFilter.CANCELED, OrderStatusFilter.FULFILLED] + } + }, orders, sort: { ...sortPageProps.sort, @@ -30,9 +43,6 @@ const props: OrderListPageProps = { storiesOf("Views / Orders / Order list", module) .addDecorator(Decorator) .add("default", () => ) - .add("with custom filters", () => ( - - )) .add("loading", () => ( undefined, plugins: pluginList, sort: { diff --git a/src/storybook/stories/productTypes/ProductTypeListPage.tsx b/src/storybook/stories/productTypes/ProductTypeListPage.tsx index 3a339f83b..e4c3fa604 100644 --- a/src/storybook/stories/productTypes/ProductTypeListPage.tsx +++ b/src/storybook/stories/productTypes/ProductTypeListPage.tsx @@ -2,12 +2,17 @@ import { storiesOf } from "@storybook/react"; import React from "react"; import { ProductTypeListUrlSortField } from "@saleor/productTypes/urls"; +import { + ProductTypeConfigurable, + ProductTypeEnum +} from "@saleor/types/globalTypes"; import { listActionsProps, pageListProps, searchPageProps, tabPageProps, - sortPageProps + sortPageProps, + filterPageProps } from "../../../fixtures"; import ProductTypeListPage, { ProductTypeListPageProps @@ -20,6 +25,17 @@ const props: ProductTypeListPageProps = { ...pageListProps.default, ...searchPageProps, ...sortPageProps, + ...filterPageProps, + filterOpts: { + configurable: { + active: false, + value: ProductTypeConfigurable.CONFIGURABLE + }, + type: { + active: false, + value: ProductTypeEnum.SHIPPABLE + } + }, sort: { ...sortPageProps.sort, sort: ProductTypeListUrlSortField.name diff --git a/src/storybook/stories/products/ProductListPage.tsx b/src/storybook/stories/products/ProductListPage.tsx index e481c0e9f..483084f10 100644 --- a/src/storybook/stories/products/ProductListPage.tsx +++ b/src/storybook/stories/products/ProductListPage.tsx @@ -7,10 +7,10 @@ import { products as productListFixture } from "@saleor/products/fixtures"; import { ProductListUrlSortField } from "@saleor/products/urls"; import { attributes } from "@saleor/productTypes/fixtures"; import { ListViews } from "@saleor/types"; +import { productListFilterOpts } from "@saleor/products/views/ProductList/fixtures"; import { fetchMoreProps, filterPageProps, - filters, listActionsProps, pageListProps, sortPageProps @@ -37,6 +37,7 @@ const props: ProductListPageProps = { activeAttributeSortId: undefined, availableInGridAttributes: attributes, defaultSettings: defaultListSettings[ListViews.PRODUCT_LIST], + filterOpts: productListFilterOpts, gridAttributes: attributes, products, settings: { @@ -49,14 +50,10 @@ const props: ProductListPageProps = { storiesOf("Views / Products / Product list", module) .addDecorator(Decorator) .add("default", () => ) - .add("with custom filters", () => ( - - )) .add("loading", () => ( diff --git a/src/storybook/stories/staff/StaffListPage.tsx b/src/storybook/stories/staff/StaffListPage.tsx index 1c3d8c1cc..2ec0552c1 100644 --- a/src/storybook/stories/staff/StaffListPage.tsx +++ b/src/storybook/stories/staff/StaffListPage.tsx @@ -2,11 +2,13 @@ import { storiesOf } from "@storybook/react"; import React from "react"; import { StaffListUrlSortField } from "@saleor/staff/urls"; +import { StaffMemberStatus } from "@saleor/types/globalTypes"; import { pageListProps, searchPageProps, tabPageProps, - sortPageProps + sortPageProps, + filterPageProps } from "../../../fixtures"; import StaffListPage, { StaffListPageProps @@ -19,6 +21,13 @@ const props: StaffListPageProps = { ...searchPageProps, ...sortPageProps, ...tabPageProps, + ...filterPageProps, + filterOpts: { + status: { + active: false, + value: StaffMemberStatus.ACTIVE + } + }, onAdd: undefined, onBack: () => undefined, sort: { diff --git a/src/theme.ts b/src/theme.ts index 24530be60..164e9f620 100644 --- a/src/theme.ts +++ b/src/theme.ts @@ -330,6 +330,7 @@ export default (colors: IThemeColors): Theme => } } }, + backgroundColor: colors.background.paper, borderColor: colors.input.border, top: 0 } diff --git a/src/translations/components/TranslationsEntitiesListPage/TranslationsEntitiesListPage.tsx b/src/translations/components/TranslationsEntitiesListPage/TranslationsEntitiesListPage.tsx index f5c40b3c2..e9352f64f 100644 --- a/src/translations/components/TranslationsEntitiesListPage/TranslationsEntitiesListPage.tsx +++ b/src/translations/components/TranslationsEntitiesListPage/TranslationsEntitiesListPage.tsx @@ -1,12 +1,12 @@ import Card from "@material-ui/core/Card"; import React from "react"; import { IntlShape, useIntl } from "react-intl"; +import makeStyles from "@material-ui/core/styles/makeStyles"; import AppHeader from "@saleor/components/AppHeader"; import Container from "@saleor/components/Container"; -import FilterSearch from "@saleor/components/Filter/FilterSearch"; +import SearchInput from "@saleor/components/SearchBar/SearchInput"; import PageHeader from "@saleor/components/PageHeader"; -// tslint:disable no-submodule-imports import { ShopInfo_shop_languages } from "@saleor/components/Shop/types/ShopInfo"; import FilterTabs, { FilterTab } from "@saleor/components/TableFilter"; import { maybe } from "@saleor/misc"; @@ -31,6 +31,24 @@ export interface TranslationsEntitiesFilters { onProductTypesTabClick: () => void; } +const useStyles = makeStyles( + theme => ({ + root: { + display: "flex", + flexWrap: "wrap", + padding: theme.spacing(1, 3) + }, + tabActions: { + borderBottom: `1px solid ${theme.palette.divider}`, + padding: theme.spacing(1, 3, 2), + textAlign: "right" + } + }), + { + name: "FilterActions" + } +); + export type TranslationsEntitiesListFilterTab = keyof typeof TranslatableEntities; function getSearchPlaceholder( @@ -88,9 +106,10 @@ const tabs: TranslationsEntitiesListFilterTab[] = [ "productTypes" ]; -const TranslationsEntitiesListPage: React.FC< - TranslationsEntitiesListPageProps -> = ({ filters, language, onBack, children, ...searchProps }) => { +const TranslationsEntitiesListPage: React.FC = props => { + const { filters, language, onBack, children, ...searchProps } = props; + + const classes = useStyles(props); const intl = useIntl(); const currentTab = tabs.indexOf(filters.current); @@ -157,11 +176,12 @@ const TranslationsEntitiesListPage: React.FC< onClick={filters.onProductTypesTabClick} /> - +
+ +
{children} diff --git a/src/types.ts b/src/types.ts index 4f091c732..9aea9641d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,9 +1,9 @@ import { MutationResult } from "react-apollo"; import { User_permissions } from "./auth/types/User"; -import { FilterContentSubmitData } from "./components/Filter"; -import { Filter } from "./components/TableFilter"; import { ConfirmButtonTransitionState } from "./components/ConfirmButton"; +import { IFilter } from "./components/Filter"; +import { MultiAutocompleteChoiceType } from "./components/MultiAutocompleteSelectField"; export interface UserError { field: string; @@ -83,22 +83,16 @@ export interface SearchPageProps { initialSearch: string; onSearchChange: (value: string) => void; } -export interface FilterPageProps - extends SearchPageProps, +export interface FilterPageProps + extends FilterProps, + SearchPageProps, TabPageProps { - currencySymbol: string; - filtersList: Filter[]; - onFilterAdd: (filter: FilterContentSubmitData) => void; + filterOpts: TOpts; } -export interface SearchProps { - searchPlaceholder: string; -} -export interface FilterProps - extends FilterPageProps, - SearchProps { - allTabLabel: string; - filterLabel: string; +export interface FilterProps { + currencySymbol: string; + onFilterChange: (filter: IFilter) => void; } export interface TabPageProps { @@ -139,8 +133,14 @@ export type Filters = Partial< Record >; export type FiltersWithMultipleValues = Partial< - Record + Record >; +export type FiltersAsDictWithMultipleValues = Partial< + Record> +>; +export type Search = Partial<{ + query: string; +}>; export type SingleAction = Partial<{ id: string; }>; @@ -173,3 +173,17 @@ export interface UserPermissionProps { export interface MutationResultAdditionalProps { status: ConfirmButtonTransitionState; } + +export type MinMax = Record<"min" | "max", string>; + +export interface FilterOpts { + active: boolean; + value: T; +} + +export interface AutocompleteFilterOpts + extends FetchMoreProps, + SearchPageProps { + choices: MultiAutocompleteChoiceType[]; + displayValues: MultiAutocompleteChoiceType[]; +} diff --git a/src/types/globalTypes.ts b/src/types/globalTypes.ts index 136badcd5..bc8d960d2 100644 --- a/src/types/globalTypes.ts +++ b/src/types/globalTypes.ts @@ -986,6 +986,11 @@ export interface PageTranslationInput { contentJson?: any | null; } +export interface PluginFilterInput { + active?: boolean | null; + search?: string | null; +} + export interface PluginSortingInput { direction: OrderDirection; field: PluginSortField; diff --git a/src/utils/filters/fields.ts b/src/utils/filters/fields.ts new file mode 100644 index 000000000..a46473bbe --- /dev/null +++ b/src/utils/filters/fields.ts @@ -0,0 +1,129 @@ +import { IFilterElement, FieldType } from "@saleor/components/Filter"; +import { MultiAutocompleteChoiceType } from "@saleor/components/MultiAutocompleteSelectField"; +import { MinMax, FetchMoreProps, SearchPageProps } from "@saleor/types"; + +export function createPriceField( + name: T, + label: string, + defaultValue: MinMax +): IFilterElement { + return { + active: false, + label, + multiple: defaultValue.min !== defaultValue.max, + name, + type: FieldType.price, + value: [defaultValue.min, defaultValue.max] + }; +} + +export function createDateField( + name: T, + label: string, + defaultValue: MinMax +): IFilterElement { + return { + active: false, + label, + multiple: defaultValue.min !== defaultValue.max, + name, + type: FieldType.date, + value: [defaultValue.min, defaultValue.max] + }; +} + +export function createNumberField( + name: T, + label: string, + defaultValue: MinMax +): IFilterElement { + return { + active: false, + label, + multiple: true, + name, + type: FieldType.number, + value: [defaultValue.min, defaultValue.max] + }; +} + +export function createOptionsField( + name: T, + label: string, + defaultValue: string[], + multiple: boolean, + options: MultiAutocompleteChoiceType[] +): IFilterElement { + return { + active: false, + label, + multiple, + name, + options, + type: FieldType.options, + value: defaultValue + }; +} + +export function createAutocompleteField( + name: T, + label: string, + defaultValue: string[], + displayValues: MultiAutocompleteChoiceType[], + multiple: boolean, + options: MultiAutocompleteChoiceType[], + opts: FetchMoreProps & SearchPageProps +): IFilterElement { + return { + ...opts, + active: false, + displayValues, + label, + multiple, + name, + options, + type: FieldType.autocomplete, + value: defaultValue + }; +} + +export function createTextField( + name: T, + label: string, + defaultValue: string +): IFilterElement { + return { + active: false, + label, + multiple: false, + name, + type: FieldType.text, + value: [defaultValue] + }; +} + +export function createBooleanField( + name: T, + label: string, + defaultValue: boolean, + labels: Record<"positive" | "negative", string> +): IFilterElement { + return { + active: false, + label, + multiple: false, + name, + options: [ + { + label: labels.positive, + value: true.toString() + }, + { + label: labels.negative, + value: false.toString() + } + ], + type: FieldType.boolean, + value: [defaultValue.toString()] + }; +} diff --git a/src/utils/filters/filters.ts b/src/utils/filters/filters.ts index c620d9fd7..59979161b 100644 --- a/src/utils/filters/filters.ts +++ b/src/utils/filters/filters.ts @@ -1,3 +1,8 @@ +import isArray from "lodash-es/isArray"; + +import { IFilterElement, IFilter } from "@saleor/components/Filter"; +import { findValueInEnum } from "@saleor/misc"; + function createFilterUtils< TQueryParams extends object, TFilters extends object @@ -5,13 +10,10 @@ function createFilterUtils< function getActiveFilters(params: TQueryParams): TFilters { return Object.keys(params) .filter(key => Object.keys(filters).includes(key)) - .reduce( - (acc, key) => { - acc[key] = params[key]; - return acc; - }, - {} as any - ); + .reduce((acc, key) => { + acc[key] = params[key]; + return acc; + }, {} as any); } function areFiltersApplied(params: TQueryParams): boolean { @@ -24,32 +26,141 @@ function createFilterUtils< }; } -export function valueOrFirst(value: T | T[]): T { - if (Array.isArray(value)) { - return value[0]; - } - - return value; -} - -export function arrayOrValue(value: T | T[]): T[] { - if (Array.isArray(value)) { - return value; - } - - return [value]; -} - -export function arrayOrUndefined(array: T[]): T[] | undefined { - if (array.length === 0) { - return undefined; - } - - return array; -} - export function dedupeFilter(array: T[]): T[] { + if (!isArray(array)) { + return [array]; + } + return Array.from(new Set(array)); } +export type GetFilterQueryParam< + TFilterKeys extends string, + TFilters extends object +> = (filter: IFilterElement, params?: object) => TFilters; +export function getFilterQueryParams< + TFilterKeys extends string, + TUrlFilters extends object +>( + filter: IFilter, + getFilterQueryParam: GetFilterQueryParam +): TUrlFilters { + return filter.reduce( + (acc, filterField) => ({ + ...acc, + ...getFilterQueryParam(filterField, acc) + }), + {} as TUrlFilters + ); +} + +type GteLte = Partial>; +export function getGteLteVariables(variables: GteLte): GteLte | null { + if ( + !![variables.gte, variables.lte].some( + v => v !== undefined && v !== null && !(typeof v === "number" && isNaN(v)) + ) + ) { + return variables; + } + + return null; +} + +export function getSingleValueQueryParam< + TKey extends string, + TUrlKey extends string +>(param: IFilterElement, key: TUrlKey) { + const { active, value } = param; + + if (!active) { + return { + [key]: undefined + }; + } + + return { + [key]: value[0] + }; +} + +export function getSingleEnumValueQueryParam< + TKey extends string, + TUrlKey extends string, + TEnum extends object +>(param: IFilterElement, key: TUrlKey, haystack: TEnum) { + const { active, value } = param; + + if (!active) { + return { + [key]: undefined + }; + } + + return { + [key]: findValueInEnum(value[0], haystack) + }; +} + +export function getMultipleEnumValueQueryParam< + TKey extends string, + TUrlKey extends string, + TEnum extends object +>(param: IFilterElement, key: TUrlKey, haystack: TEnum) { + const { active, value } = param; + + if (!active) { + return { + [key]: undefined + }; + } + + return { + [key]: value.map(val => findValueInEnum(val, haystack)) + }; +} + +export function getMultipleValueQueryParam< + TKey extends string, + TUrlKey extends string +>(param: IFilterElement, key: TUrlKey) { + const { active, value } = param; + + if (!active) { + return { + [key]: undefined + }; + } + + return { + [key]: value + }; +} + +export function getMinMaxQueryParam< + TKey extends string, + TUrlKey extends string +>(param: IFilterElement, keyFrom: TUrlKey, keyTo: TUrlKey) { + const { active, multiple, value } = param; + + if (!active) { + return { + [keyFrom]: undefined, + [keyTo]: undefined + }; + } + + if (multiple) { + return { + [keyFrom]: value[0], + [keyTo]: value[1] + }; + } + + return { + [keyFrom]: value[0], + [keyTo]: value[0] + }; +} + export default createFilterUtils; diff --git a/src/utils/handlers/filterHandlers.ts b/src/utils/handlers/filterHandlers.ts new file mode 100644 index 000000000..60c4dbc3c --- /dev/null +++ b/src/utils/handlers/filterHandlers.ts @@ -0,0 +1,70 @@ +import { IFilter } from "@saleor/components/Filter"; +import { UseNavigatorResult } from "@saleor/hooks/useNavigator"; +import { Sort, Pagination, ActiveTab, Search } from "@saleor/types"; +import { getFilterQueryParams, GetFilterQueryParam } from "../filters"; + +type RequiredParams = ActiveTab & Search & Sort & Pagination; +type CreateUrl = (params: RequiredParams) => string; +type CreateFilterHandlers = [ + (filter: IFilter) => void, + () => void, + (query: string) => void +]; + +function createFilterHandlers< + TFilterKeys extends string, + TFilters extends object +>(opts: { + getFilterQueryParam: GetFilterQueryParam; + navigate: UseNavigatorResult; + createUrl: CreateUrl; + params: RequiredParams; + cleanupFn?: () => void; +}): CreateFilterHandlers { + const { getFilterQueryParam, navigate, createUrl, params, cleanupFn } = opts; + + const changeFilters = (filter: IFilter) => { + if (!!cleanupFn) { + cleanupFn(); + } + + navigate( + createUrl({ + ...params, + ...getFilterQueryParams(filter, getFilterQueryParam), + activeTab: undefined + }) + ); + }; + + const resetFilters = () => { + if (!!cleanupFn) { + cleanupFn(); + } + + navigate( + createUrl({ + asc: params.asc, + sort: params.sort + }) + ); + }; + + const handleSearchChange = (query: string) => { + if (!!cleanupFn) { + cleanupFn(); + } + + navigate( + createUrl({ + ...params, + activeTab: undefined, + query + }) + ); + }; + + return [changeFilters, resetFilters, handleSearchChange]; +} + +export default createFilterHandlers; diff --git a/src/utils/lists/__snapshots__/lists.test.ts.snap b/src/utils/lists/__snapshots__/lists.test.ts.snap index 998d4c2e4..c849a90e0 100644 --- a/src/utils/lists/__snapshots__/lists.test.ts.snap +++ b/src/utils/lists/__snapshots__/lists.test.ts.snap @@ -56,6 +56,27 @@ Array [ ] `; +exports[`Properly calculates output arrays Updates 1`] = ` +Array [ + Object { + "name": "lorem", + "value": 0, + }, + Object { + "name": "ipsum", + "value": 1, + }, + Object { + "name": "dolor", + "value": 2, + }, + Object { + "name": "amet", + "value": 32, + }, +] +`; + exports[`Properly calculates output arrays Updates at index 1`] = ` Array [ "lorem", diff --git a/src/utils/lists/lists.test.ts b/src/utils/lists/lists.test.ts index dc6976b1c..99488dc2f 100644 --- a/src/utils/lists/lists.test.ts +++ b/src/utils/lists/lists.test.ts @@ -6,10 +6,12 @@ import { remove, removeAtIndex, toggle, + update, updateAtIndex } from "./lists"; const initialArray = ["lorem", "ipsum", "dolor"]; +const compare = (a, b) => a === b; describe("Properly calculates output arrays", () => { it("Adds", () => { @@ -20,12 +22,28 @@ describe("Properly calculates output arrays", () => { expect(addAtIndex("sit", initialArray, 2)).toMatchSnapshot(); }); + it("Updates", () => { + expect( + update( + { + name: "amet", + value: 32 + }, + initialArray.map((el, index) => ({ + name: el, + value: index + })), + (a, b) => a.name === b.name + ) + ).toMatchSnapshot(); + }); + it("Updates at index", () => { expect(updateAtIndex("amet", initialArray, 1)).toMatchSnapshot(); }); it("Removes", () => { - expect(remove("ipsum", initialArray, (a, b) => a === b)).toMatchSnapshot(); + expect(remove("ipsum", initialArray, compare)).toMatchSnapshot(); }); it("Removes at index", () => { @@ -33,16 +51,16 @@ describe("Properly calculates output arrays", () => { }); it("Matches", () => { - expect(isSelected("lorem", initialArray, (a, b) => a === b)).toBe(true); - expect(isSelected("sit", initialArray, (a, b) => a === b)).toBe(false); + expect(isSelected("lorem", initialArray, compare)).toBe(true); + expect(isSelected("sit", initialArray, compare)).toBe(false); }); it("Toggles", () => { - expect(toggle("lorem", initialArray, (a, b) => a === b)).toMatchSnapshot(); - expect(toggle("sit", initialArray, (a, b) => a === b)).toMatchSnapshot(); + expect(toggle("lorem", initialArray, compare)).toMatchSnapshot(); + expect(toggle("sit", initialArray, compare)).toMatchSnapshot(); }); it("Moves", () => { - expect(move("lorem", initialArray, (a, b) => a === b, 1)).toMatchSnapshot(); + expect(move("lorem", initialArray, compare, 1)).toMatchSnapshot(); }); }); diff --git a/src/utils/lists/lists.ts b/src/utils/lists/lists.ts index 21005bacc..067fc959c 100644 --- a/src/utils/lists/lists.ts +++ b/src/utils/lists/lists.ts @@ -30,6 +30,14 @@ export function move( return addAtIndex(data, remove(data, list, compare), index); } +export function update( + data: TData, + list: List, + compare: Compare +) { + return add(data, remove(data, list, compare)); +} + export function updateAtIndex( data: TData, list: List, diff --git a/src/utils/sort.ts b/src/utils/sort.ts index 6288e8191..0a12f26af 100644 --- a/src/utils/sort.ts +++ b/src/utils/sort.ts @@ -79,8 +79,16 @@ export function createGetSortQueryVariables< >( getSortQueryField: GetSortQueryField ): GetSortQueryVariables { - return (params: TParams) => ({ - direction: getOrderDirection(params.asc), - field: getSortQueryField(params.sort) - }); + return (params: TParams) => { + const field = getSortQueryField(params.sort); + + if (!!field) { + return { + direction: getOrderDirection(params.asc), + field + }; + } + + return undefined; + }; } diff --git a/src/utils/urls.ts b/src/utils/urls.ts index 332488a80..11791432d 100644 --- a/src/utils/urls.ts +++ b/src/utils/urls.ts @@ -1,7 +1,20 @@ import { stringify } from "qs"; +import isArray from "lodash-es/isArray"; export function stringifyQs(params: object): string { return stringify(params, { indices: false }); } + +export function getArrayQueryParam(param: string | string[]): string[] { + if (!param) { + return undefined; + } + + if (isArray(param)) { + return param; + } + + return [param]; +} diff --git a/src/webhooks/components/WebhooksListPage/WebhookListPage.stories.tsx b/src/webhooks/components/WebhooksListPage/WebhookListPage.stories.tsx index 4c9314949..077ed6ddf 100644 --- a/src/webhooks/components/WebhooksListPage/WebhookListPage.stories.tsx +++ b/src/webhooks/components/WebhooksListPage/WebhookListPage.stories.tsx @@ -6,7 +6,8 @@ import { pageListProps, searchPageProps, tabPageProps, - sortPageProps + sortPageProps, + filterPageProps } from "@saleor/fixtures"; import Decorator from "@saleor/storybook/Decorator"; import { WebhookListUrlSortField } from "@saleor/webhooks/urls"; @@ -19,6 +20,13 @@ const props: WebhooksListPageProps = { ...searchPageProps, ...sortPageProps, ...tabPageProps, + ...filterPageProps, + filterOpts: { + isActive: { + active: false, + value: true + } + }, onBack: () => undefined, onRemove: () => undefined, sort: { diff --git a/src/webhooks/components/WebhooksListPage/WebhooksListPage.tsx b/src/webhooks/components/WebhooksListPage/WebhooksListPage.tsx index 523b91b38..53ad7f01e 100644 --- a/src/webhooks/components/WebhooksListPage/WebhooksListPage.tsx +++ b/src/webhooks/components/WebhooksListPage/WebhooksListPage.tsx @@ -6,21 +6,26 @@ import { FormattedMessage, useIntl } from "react-intl"; import AppHeader from "@saleor/components/AppHeader"; import Container from "@saleor/components/Container"; import PageHeader from "@saleor/components/PageHeader"; -import SearchBar from "@saleor/components/SearchBar"; +import FilterBar from "@saleor/components/FilterBar"; import { sectionNames } from "@saleor/intl"; import { PageListProps, - SearchPageProps, + FilterPageProps, TabPageProps, SortPage } from "@saleor/types"; import { WebhookListUrlSortField } from "@saleor/webhooks/urls"; import { Webhooks_webhooks_edges_node } from "../../types/Webhooks"; -import WebhooksList from "../WebhooksList/WebhooksList"; +import WebhooksList from "../WebhooksList"; +import { + WebhookFilterKeys, + WebhookListFilterOpts, + createFilterStructure +} from "./filters"; export interface WebhooksListPageProps extends PageListProps, - SearchPageProps, + FilterPageProps, SortPage, TabPageProps { webhooks: Webhooks_webhooks_edges_node[]; @@ -29,11 +34,14 @@ export interface WebhooksListPageProps } const WebhooksListPage: React.FC = ({ + currencySymbol, currentTab, + filterOpts, initialSearch, onAdd, onAll, onBack, + onFilterChange, onSearchChange, onTabChange, onTabDelete, @@ -43,6 +51,9 @@ const WebhooksListPage: React.FC = ({ ...listProps }) => { const intl = useIntl(); + + const structure = createFilterStructure(intl, filterOpts); + return ( @@ -57,18 +68,21 @@ const WebhooksListPage: React.FC = ({ - ; +} + +const messages = defineMessages({ + active: { + defaultMessage: "Active", + description: "webhook" + }, + inactive: { + defaultMessage: "Inactive", + description: "webhook" + } +}); + +export function createFilterStructure( + intl: IntlShape, + opts: WebhookListFilterOpts +): IFilter { + return [ + { + ...createBooleanField( + WebhookFilterKeys.isActive, + intl.formatMessage(commonMessages.status), + opts.isActive.value, + { + negative: intl.formatMessage(messages.inactive), + positive: intl.formatMessage(messages.active) + } + ), + active: opts.isActive.active + } + ]; +} diff --git a/src/webhooks/components/WebhooksListPage/index.ts b/src/webhooks/components/WebhooksListPage/index.ts index 68f52d520..3c0f1c803 100644 --- a/src/webhooks/components/WebhooksListPage/index.ts +++ b/src/webhooks/components/WebhooksListPage/index.ts @@ -1,2 +1,3 @@ export { default } from "./WebhooksListPage"; export * from "./WebhooksListPage"; +export * from "./filters"; diff --git a/src/webhooks/urls.ts b/src/webhooks/urls.ts index 6a7de1b38..b01c39865 100644 --- a/src/webhooks/urls.ts +++ b/src/webhooks/urls.ts @@ -15,6 +15,7 @@ export const webhookSection = "/webhooks/"; export const webhookListPath = webhookSection; export enum WebhookListUrlFiltersEnum { + active = "active", query = "query" } export type WebhookListUrlFilters = Filters; diff --git a/src/webhooks/views/WebhookList/WebhookList.tsx b/src/webhooks/views/WebhookList/WebhookList.tsx index 396129a2d..20de84347 100644 --- a/src/webhooks/views/WebhookList/WebhookList.tsx +++ b/src/webhooks/views/WebhookList/WebhookList.tsx @@ -19,12 +19,14 @@ import { useIntl } from "react-intl"; import { getSortParams } from "@saleor/utils/sort"; import createSortHandler from "@saleor/utils/handlers/sortHandler"; +import useShop from "@saleor/hooks/useShop"; +import createFilterHandlers from "@saleor/utils/handlers/filterHandlers"; +import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers"; import WebhooksListPage from "../../components/WebhooksListPage/WebhooksListPage"; import { TypedWebhookDelete } from "../../mutations"; import { useWebhooksListQuery } from "../../queries"; import { WebhookListUrlDialog, - WebhookListUrlFilters, webhookAddUrl, webhookListUrl, WebhookListUrlQueryParams, @@ -37,8 +39,10 @@ import { getActiveFilters, getFilterTabs, getFilterVariables, - saveFilterTab -} from "./filter"; + getFilterQueryParam, + saveFilterTab, + getFilterOpts +} from "./filters"; interface WebhooksListProps { params: WebhookListUrlQueryParams; @@ -49,6 +53,7 @@ export const WebhooksList: React.FC = ({ params }) => { const paginate = usePaginator(); const notify = useNotifier(); const intl = useIntl(); + const shop = useShop(); const { updateListSettings, settings } = useListSettings( ListViews.WEBHOOK_LIST ); @@ -76,32 +81,21 @@ export const WebhooksList: React.FC = ({ params }) => { : 0 : parseInt(params.activeTab, 0); - const changeFilterField = (filter: WebhookListUrlFilters) => - navigate( - webhookListUrl({ - ...getActiveFilters(params), - ...filter, - activeTab: undefined - }) - ); - const closeModal = () => - navigate( - webhookListUrl({ - ...params, - action: undefined, - id: undefined - }), - true - ); + const [ + changeFilters, + resetFilters, + handleSearchChange + ] = createFilterHandlers({ + createUrl: webhookListUrl, + getFilterQueryParam, + navigate, + params + }); - const openModal = (action: WebhookListUrlDialog, id?: string) => - navigate( - webhookListUrl({ - ...params, - action, - id - }) - ); + const [openModal, closeModal] = createDialogActionHandlers< + WebhookListUrlDialog, + WebhookListUrlQueryParams + >(navigate, webhookListUrl, params); const handleTabChange = (tab: number) => { navigate( @@ -133,6 +127,7 @@ export const WebhooksList: React.FC = ({ params }) => { }; const handleSort = createSortHandler(navigate, webhookListUrl, params); + const currencySymbol = maybe(() => shop.defaultCurrency, "USD"); return ( @@ -162,10 +157,13 @@ export const WebhooksList: React.FC = ({ params }) => { return ( <> changeFilterField({ query })} - onAll={() => navigate(webhookListUrl())} + onFilterChange={changeFilters} + onSearchChange={handleSearchChange} + onAll={resetFilters} onTabChange={handleTabChange} onTabDelete={() => openModal("delete-search")} onTabSave={() => openModal("save-search")} diff --git a/src/webhooks/views/WebhookList/__snapshots__/filters.test.ts.snap b/src/webhooks/views/WebhookList/__snapshots__/filters.test.ts.snap new file mode 100644 index 000000000..c63a81372 --- /dev/null +++ b/src/webhooks/views/WebhookList/__snapshots__/filters.test.ts.snap @@ -0,0 +1,9 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Filtering URL params should not be empty if active filters are present 1`] = ` +Object { + "active": "false", +} +`; + +exports[`Filtering URL params should not be empty if active filters are present 2`] = `"active=false"`; diff --git a/src/webhooks/views/WebhookList/filter.ts b/src/webhooks/views/WebhookList/filter.ts deleted file mode 100644 index c5445c621..000000000 --- a/src/webhooks/views/WebhookList/filter.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { WebhookFilterInput } from "@saleor/types/globalTypes"; -import { createFilterTabUtils, createFilterUtils } from "@saleor/utils/filters"; -import { - WebhookListUrlFilters, - WebhookListUrlFiltersEnum, - WebhookListUrlQueryParams -} from "../../urls"; - -export const WEBHOOK_FILTERS_KEY = "webhookFilters"; - -export function getFilterVariables( - params: WebhookListUrlFilters -): WebhookFilterInput { - return { - search: params.query - }; -} - -export const { - deleteFilterTab, - getFilterTabs, - saveFilterTab -} = createFilterTabUtils(WEBHOOK_FILTERS_KEY); - -export const { areFiltersApplied, getActiveFilters } = createFilterUtils< - WebhookListUrlQueryParams, - WebhookListUrlFilters ->(WebhookListUrlFiltersEnum); diff --git a/src/webhooks/views/WebhookList/filters.test.ts b/src/webhooks/views/WebhookList/filters.test.ts new file mode 100644 index 000000000..2aa937136 --- /dev/null +++ b/src/webhooks/views/WebhookList/filters.test.ts @@ -0,0 +1,57 @@ +import { createIntl } from "react-intl"; +import { stringify as stringifyQs } from "qs"; + +import { WebhookListUrlFilters } from "@saleor/webhooks/urls"; +import { createFilterStructure } from "@saleor/webhooks/components/WebhooksListPage"; +import { getFilterQueryParams } from "@saleor/utils/filters"; +import { getExistingKeys, setFilterOptsStatus } from "@test/filters"; +import { config } from "@test/intl"; +import { getFilterVariables, getFilterQueryParam } from "./filters"; + +describe("Filtering query params", () => { + it("should be empty object if no params given", () => { + const params: WebhookListUrlFilters = {}; + const filterVariables = getFilterVariables(params); + + expect(getExistingKeys(filterVariables)).toHaveLength(0); + }); + + it("should not be empty object if params given", () => { + const params: WebhookListUrlFilters = { + active: false.toString() + }; + const filterVariables = getFilterVariables(params); + + expect(getExistingKeys(filterVariables)).toHaveLength(1); + }); +}); + +describe("Filtering URL params", () => { + const intl = createIntl(config); + + const filters = createFilterStructure(intl, { + isActive: { + active: false, + value: false + } + }); + + it("should be empty if no active filters", () => { + const filterQueryParams = getFilterQueryParams( + filters, + getFilterQueryParam + ); + + expect(getExistingKeys(filterQueryParams)).toHaveLength(0); + }); + + it("should not be empty if active filters are present", () => { + const filterQueryParams = getFilterQueryParams( + setFilterOptsStatus(filters, true), + getFilterQueryParam + ); + + expect(filterQueryParams).toMatchSnapshot(); + expect(stringifyQs(filterQueryParams)).toMatchSnapshot(); + }); +}); diff --git a/src/webhooks/views/WebhookList/filters.ts b/src/webhooks/views/WebhookList/filters.ts new file mode 100644 index 000000000..d8026e2f4 --- /dev/null +++ b/src/webhooks/views/WebhookList/filters.ts @@ -0,0 +1,64 @@ +import { WebhookFilterInput } from "@saleor/types/globalTypes"; +import { + createFilterTabUtils, + createFilterUtils, + getSingleValueQueryParam +} from "@saleor/utils/filters"; +import { IFilterElement } from "@saleor/components/Filter"; +import { + WebhookListFilterOpts, + WebhookFilterKeys +} from "@saleor/webhooks/components/WebhooksListPage"; +import { parseBoolean, maybe } from "@saleor/misc"; +import { + WebhookListUrlFilters, + WebhookListUrlFiltersEnum, + WebhookListUrlQueryParams +} from "../../urls"; + +export const WEBHOOK_FILTERS_KEY = "webhookFilters"; + +export function getFilterOpts( + params: WebhookListUrlFilters +): WebhookListFilterOpts { + return { + isActive: { + active: maybe(() => params.active !== undefined, false), + value: + params.active !== undefined ? parseBoolean(params.active, true) : true + } + }; +} + +export function getFilterVariables( + params: WebhookListUrlFilters +): WebhookFilterInput { + return { + isActive: params.active + ? parseBoolean(params.active, undefined) + : undefined, + search: params.query + }; +} + +export function getFilterQueryParam( + filter: IFilterElement +): WebhookListUrlFilters { + const { name } = filter; + + switch (name) { + case WebhookFilterKeys.isActive: + return getSingleValueQueryParam(filter, WebhookListUrlFiltersEnum.active); + } +} + +export const { + deleteFilterTab, + getFilterTabs, + saveFilterTab +} = createFilterTabUtils(WEBHOOK_FILTERS_KEY); + +export const { areFiltersApplied, getActiveFilters } = createFilterUtils< + WebhookListUrlQueryParams, + WebhookListUrlFilters +>(WebhookListUrlFiltersEnum); diff --git a/testUtils/filters.ts b/testUtils/filters.ts new file mode 100644 index 000000000..0c3987080 --- /dev/null +++ b/testUtils/filters.ts @@ -0,0 +1,21 @@ +import clone from "lodash-es/clone"; + +import { IFilter } from "@saleor/components/Filter"; + +export function getExistingKeys(o: object): string[] { + return Object.keys(o).filter(key => o[key] !== undefined && o[key] !== null); +} + +export function setFilterOptsStatus( + opts: IFilter, + status: boolean +): IFilter { + const newOpts = clone(opts); + for (const optName in opts) { + if (Object.prototype.hasOwnProperty.call(newOpts, optName)) { + newOpts[optName].active = status; + } + } + + return newOpts; +} diff --git a/testUtils/intl.ts b/testUtils/intl.ts new file mode 100644 index 000000000..796d46e21 --- /dev/null +++ b/testUtils/intl.ts @@ -0,0 +1,6 @@ +import { OptionalIntlConfig } from "react-intl/dist/components/provider"; + +export const config: OptionalIntlConfig = { + defaultLocale: "en", + locale: "en" +}; diff --git a/tsconfig.json b/tsconfig.json index 56464c40a..a015886ac 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,7 +12,8 @@ "paths": { "@assets/*": ["assets/*"], "@locale/*": ["locale/*"], - "@saleor/*": ["src/*"] + "@saleor/*": ["src/*"], + "@test/*": ["testUtils/*"] }, "resolveJsonModule": true },