diff --git a/CHANGELOG.md b/CHANGELOG.md index 8142d3ea7..306676228 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,3 +33,9 @@ All notable, unreleased changes to this project will be documented in this file. - Hide variants and attributes if product has none - #179 by @dominik-zeglen - Add service account section - #188 by @dominik-zeglen - Add webhook section - #206 by @benekex2 +- Add variant creator - #177 by @dominik-zeglen +- Add git hooks - #209 by @dominik-zeglen +- Do not send customer invitation email - #211 by @dominik-zeglen +- Send address update mutation only once - #210 by @dominik-zeglen +- Update sale details design - #207 by @dominik-zeglen + diff --git a/locale/messages.pot b/locale/messages.pot index cbb6f26a6..a5f2fceae 100644 --- a/locale/messages.pot +++ b/locale/messages.pot @@ -1,6 +1,6 @@ msgid "" msgstr "" -"POT-Creation-Date: 2019-10-04T11:19:12.447Z\n" +"POT-Creation-Date: 2019-10-15T15:56:00.137Z\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "MIME-Version: 1.0\n" @@ -99,6 +99,14 @@ msgctxt "staff member status" msgid "Active" msgstr "" +#: build/locale/src/discounts/components/DiscountDates/DiscountDates.json +#. [src.discounts.components.DiscountDates.1662220323] - time during discount is active, header +#. defaultMessage is: +#. Active Dates +msgctxt "time during discount is active, header" +msgid "Active Dates" +msgstr "" + #: build/locale/src/discounts/components/VoucherDates/VoucherDates.json #. [src.discounts.components.VoucherDates.1662220323] - time during voucher is active, header #. defaultMessage is: @@ -131,6 +139,22 @@ msgctxt "dialog title" msgid "Add Address" msgstr "" +#: build/locale/src/collections/components/CollectionCreatePage/CollectionCreatePage.json +#. [src.collections.components.CollectionCreatePage.951411809] - page header +#. defaultMessage is: +#. Add Collection +msgctxt "page header" +msgid "Add Collection" +msgstr "" + +#: build/locale/src/customers/components/CustomerCreatePage/CustomerCreatePage.json +#. [src.customers.components.CustomerCreatePage.2622255457] - page header +#. defaultMessage is: +#. Add Customer +msgctxt "page header" +msgid "Add Customer" +msgstr "" + #: build/locale/src/components/Filter/Filter.json #. [src.components.Filter.2852521946] - button #. defaultMessage is: @@ -171,6 +195,14 @@ msgctxt "dialog header" msgid "Add Price Rate" msgstr "" +#: build/locale/src/orders/components/OrderFulfillmentTrackingDialog/OrderFulfillmentTrackingDialog.json +#. [src.orders.components.OrderFulfillmentTrackingDialog.606831229] - dialog header +#. defaultMessage is: +#. Add Tracking Code +msgctxt "dialog header" +msgid "Add Tracking Code" +msgstr "" + #: build/locale/src/attributes/components/AttributeValueEditDialog/AttributeValueEditDialog.json #. [src.attributes.components.AttributeValueEditDialog.1841790893] - add attribute value #. defaultMessage is: @@ -203,22 +235,6 @@ msgctxt "button" msgid "Add authentication" msgstr "" -#: build/locale/src/collections/components/CollectionCreatePage/CollectionCreatePage.json -#. [src.collections.components.CollectionCreatePage.3958681866] - page header -#. defaultMessage is: -#. Add collection -msgctxt "page header" -msgid "Add collection" -msgstr "" - -#: build/locale/src/customers/components/CustomerCreatePage/CustomerCreatePage.json -#. [src.customers.components.CustomerCreatePage.1934221653] - page header -#. defaultMessage is: -#. Add customer -msgctxt "page header" -msgid "Add customer" -msgstr "" - #: build/locale/src/customers/components/CustomerListPage/CustomerListPage.json #. [src.customers.components.CustomerListPage.1934221653] - button #. defaultMessage is: @@ -359,14 +375,6 @@ msgctxt "fulfillment group tracking number" msgid "Add tracking" msgstr "" -#: build/locale/src/orders/components/OrderFulfillmentTrackingDialog/OrderFulfillmentTrackingDialog.json -#. [src.orders.components.OrderFulfillmentTrackingDialog.3680864271] - dialog header -#. defaultMessage is: -#. Add tracking code -msgctxt "dialog header" -msgid "Add tracking code" -msgstr "" - #: build/locale/src/products/components/ProductVariantNavigation/ProductVariantNavigation.json #. [src.products.components.ProductVariantNavigation.2845381934] - button #. defaultMessage is: @@ -639,6 +647,38 @@ msgctxt "voucher" msgid "Applies to" msgstr "" +#: build/locale/src/products/components/ProductVariantCreateDialog/ProductVariantCreatePrices.json +#. [src.products.components.ProductVariantCreateDialog.2783195765] +#. defaultMessage is: +#. Apply single price to all SKUs +msgctxt "description" +msgid "Apply single price to all SKUs" +msgstr "" + +#: build/locale/src/products/components/ProductVariantCreateDialog/ProductVariantCreatePrices.json +#. [src.products.components.ProductVariantCreateDialog.3601538615] +#. defaultMessage is: +#. Apply single stock to all SKUs +msgctxt "description" +msgid "Apply single stock to all SKUs" +msgstr "" + +#: build/locale/src/products/components/ProductVariantCreateDialog/ProductVariantCreatePrices.json +#. [src.products.components.ProductVariantCreateDialog.3570949907] +#. defaultMessage is: +#. Apply unique prices by attribute to each SKU +msgctxt "description" +msgid "Apply unique prices by attribute to each SKU" +msgstr "" + +#: build/locale/src/products/components/ProductVariantCreateDialog/ProductVariantCreatePrices.json +#. [src.products.components.ProductVariantCreateDialog.3387090508] +#. defaultMessage is: +#. Apply unique stock by attribute to each SKU +msgctxt "description" +msgid "Apply unique stock by attribute to each SKU" +msgstr "" + #: build/locale/src/orders/components/OrderCancelDialog/OrderCancelDialog.json #. [src.orders.components.OrderCancelDialog.3981375672] #. defaultMessage is: @@ -1139,6 +1179,10 @@ msgctxt "description" msgid "Are you sure you want to void this payment?" msgstr "" +#: build/locale/src/products/components/ProductVariantCreateDialog/ProductVariantCreateDialog.json +#. [src.products.components.ProductVariantCreateDialog.3922579741] - dialog header +#. defaultMessage is: +#. Assign Attribute #: build/locale/src/productTypes/components/AssignAttributeDialog/AssignAttributeDialog.json #. [src.productTypes.components.AssignAttributeDialog.3922579741] - dialog header #. defaultMessage is: @@ -1267,6 +1311,14 @@ msgctxt "assign attribute value button" msgid "Assign value" msgstr "" +#: build/locale/src/products/components/ProductVariantCreateDialog/ProductVariantCreatePrices.json +#. [src.products.components.ProductVariantCreateDialog.168343345] - variant attribute +#. defaultMessage is: +#. Attribute +msgctxt "variant attribute" +msgid "Attribute" +msgstr "" + #: build/locale/src/attributes/components/AttributeDetails/AttributeDetails.json #. [src.attributes.components.AttributeDetails.3605174225] - attribute's slug short code label #. defaultMessage is: @@ -1464,15 +1516,15 @@ msgid "Back to login" msgstr "" #: build/locale/src/categories/components/CategoryBackground/CategoryBackground.json -#. [src.categories.components.CategoryBackground.1849089820] - section header +#. [src.categories.components.CategoryBackground.2224943474] - section header #. defaultMessage is: -#. Background image (optional) +#. Background Image (optional) #: build/locale/src/collections/components/CollectionImage/CollectionImage.json -#. [src.collections.components.CollectionImage.1849089820] - section header +#. [src.collections.components.CollectionImage.2224943474] - section header #. defaultMessage is: -#. Background image (optional) +#. Background Image (optional) msgctxt "section header" -msgid "Background image (optional)" +msgid "Background Image (optional)" msgstr "" #: build/locale/src/misc.json @@ -1483,6 +1535,14 @@ msgctxt "tax rate" msgid "Bikes" msgstr "" +#: build/locale/src/customers/components/CustomerAddresses/CustomerAddresses.json +#. [src.customers.components.CustomerAddresses.4282475982] - subsection header +#. defaultMessage is: +#. Billing Address +msgctxt "subsection header" +msgid "Billing Address" +msgstr "" + #: build/locale/src/orders/components/OrderCustomer/OrderCustomer.json #. [src.orders.components.OrderCustomer.4282475982] #. defaultMessage is: @@ -1491,14 +1551,6 @@ msgctxt "description" msgid "Billing Address" msgstr "" -#: build/locale/src/customers/components/CustomerAddresses/CustomerAddresses.json -#. [src.customers.components.CustomerAddresses.2428885633] - subsection header -#. defaultMessage is: -#. Billing address -msgctxt "subsection header" -msgid "Billing address" -msgstr "" - #: build/locale/src/misc.json #. [src.books] - tax rate #. defaultMessage is: @@ -1511,6 +1563,10 @@ msgstr "" #. [src.cancel] - button #. defaultMessage is: #. Cancel +#: build/locale/src/products/components/ProductVariantCreateDialog/ProductVariantCreateDialog.json +#. [src.products.components.ProductVariantCreateDialog.3528672691] - button +#. defaultMessage is: +#. Cancel msgctxt "button" msgid "Cancel" msgstr "" @@ -1531,6 +1587,14 @@ msgctxt "dialog header" msgid "Cancel Fulfillment" msgstr "" +#: build/locale/src/orders/components/OrderCancelDialog/OrderCancelDialog.json +#. [src.orders.components.OrderCancelDialog.1258942306] - dialog header +#. defaultMessage is: +#. Cancel Order +msgctxt "dialog header" +msgid "Cancel Order" +msgstr "" + #: build/locale/src/orders/components/OrderBulkCancelDialog/OrderBulkCancelDialog.json #. [src.orders.components.OrderBulkCancelDialog.1528036340] - dialog header #. defaultMessage is: @@ -1547,14 +1611,6 @@ msgctxt "button" msgid "Cancel fulfillment" msgstr "" -#: build/locale/src/orders/components/OrderCancelDialog/OrderCancelDialog.json -#. [src.orders.components.OrderCancelDialog.1854613983] - dialog header -#. defaultMessage is: -#. Cancel order -msgctxt "dialog header" -msgid "Cancel order" -msgstr "" - #: build/locale/src/orders/components/OrderDetailsPage/OrderDetailsPage.json #. [src.orders.components.OrderDetailsPage.1854613983] - button #. defaultMessage is: @@ -1751,6 +1807,14 @@ msgctxt "tax rate" msgid "Children's clothing" msgstr "" +#: build/locale/src/products/components/ProductVariantCreateDialog/ProductVariantCreatePrices.json +#. [src.products.components.ProductVariantCreateDialog.2670525734] - variant attribute +#. defaultMessage is: +#. Choose attribute +msgctxt "variant attribute" +msgid "Choose attribute" +msgstr "" + #: build/locale/src/shipping/components/ShippingZoneCountriesAssignDialog/ShippingZoneCountriesAssignDialog.json #. [src.shipping.components.ShippingZoneCountriesAssignDialog.2404264158] #. defaultMessage is: @@ -1928,11 +1992,11 @@ msgid "Confirm Password" msgstr "" #: build/locale/src/orders/components/OrderCustomer/OrderCustomer.json -#. [src.orders.components.OrderCustomer.1111991638] - subheader +#. [src.orders.components.OrderCustomer.2312694610] - subheader #. defaultMessage is: -#. Contact information +#. Contact Information msgctxt "subheader" -msgid "Contact information" +msgid "Contact Information" msgstr "" #: build/locale/src/pages/components/PageInfo/PageInfo.json @@ -2139,6 +2203,14 @@ msgctxt "description" msgid "Country area" msgstr "" +#: build/locale/src/products/components/ProductVariantCreateDialog/ProductVariantCreateDialog.json +#. [src.products.components.ProductVariantCreateDialog.4120989039] - create multiple variants, button +#. defaultMessage is: +#. Create +msgctxt "create multiple variants, button" +msgid "Create" +msgstr "" + #: build/locale/src/services/components/ServiceTokenCreateDialog/ServiceTokenCreateDialog.json #. [src.services.components.ServiceTokenCreateDialog.4120989039] - create service token, button #. defaultMessage is: @@ -2203,6 +2275,14 @@ msgctxt "page header" msgid "Create Page" msgstr "" +#: build/locale/src/pages/views/PageCreate.json +#. [src.pages.views.1068617485] - header +#. defaultMessage is: +#. Create Page +msgctxt "header" +msgid "Create Page" +msgstr "" + #: build/locale/src/orders/components/OrderProductAddDialog/OrderProductAddDialog.json #. [src.orders.components.OrderProductAddDialog.1542417144] - dialog header #. defaultMessage is: @@ -2383,14 +2463,6 @@ msgctxt "button" msgid "Create page" msgstr "" -#: build/locale/src/pages/views/PageCreate.json -#. [src.pages.views.3785394515] - header -#. defaultMessage is: -#. Create page -msgctxt "header" -msgid "Create page" -msgstr "" - #: build/locale/src/shipping/components/ShippingZoneRateDialog/ShippingZoneRateDialog.json #. [src.shipping.components.ShippingZoneRateDialog.16061680] - button #. defaultMessage is: @@ -2435,6 +2507,14 @@ msgctxt "window title" msgid "Create variant" msgstr "" +#: build/locale/src/products/components/ProductVariants/ProductVariants.json +#. [src.products.components.ProductVariants.1721716102] - button +#. defaultMessage is: +#. Create variants +msgctxt "button" +msgid "Create variants" +msgstr "" + #: build/locale/src/discounts/components/VoucherListPage/VoucherListPage.json #. [src.discounts.components.VoucherListPage.614836274] - button #. defaultMessage is: @@ -2539,6 +2619,14 @@ msgctxt "description" msgid "Customer Name" msgstr "" +#: build/locale/src/customers/components/CustomerCreateDetails/CustomerCreateDetails.json +#. [src.customers.components.CustomerCreateDetails.3063084773] - header +#. defaultMessage is: +#. Customer Overview +msgctxt "header" +msgid "Customer Overview" +msgstr "" + #: build/locale/src/customers/views/CustomerDetails.json #. [src.customers.views.3901579344] #. defaultMessage is: @@ -2555,14 +2643,6 @@ msgctxt "description" msgid "Customer created" msgstr "" -#: build/locale/src/customers/components/CustomerCreateDetails/CustomerCreateDetails.json -#. [src.customers.components.CustomerCreateDetails.4157831287] - header -#. defaultMessage is: -#. Customer overview -msgctxt "header" -msgid "Customer overview" -msgstr "" - #: build/locale/src/customers/components/CustomerDetails/CustomerDetails.json #. [src.customers.components.CustomerDetails.2200102325] - section subheader #. defaultMessage is: @@ -2739,6 +2819,30 @@ msgctxt "dialog title" msgid "Delete Collection" msgstr "" +#: build/locale/src/customers/views/CustomerDetails.json +#. [src.customers.views.1998583641] - dialog header +#. defaultMessage is: +#. Delete Customer +msgctxt "dialog header" +msgid "Delete Customer" +msgstr "" + +#: build/locale/src/customers/views/CustomerList/CustomerList.json +#. [src.customers.views.CustomerList.2136923553] - dialog header +#. defaultMessage is: +#. Delete Customers +msgctxt "dialog header" +msgid "Delete Customers" +msgstr "" + +#: build/locale/src/orders/components/OrderDraftCancelDialog/OrderDraftCancelDialog.json +#. [src.orders.components.OrderDraftCancelDialog.1961675716] - dialog header +#. defaultMessage is: +#. Delete Daft Order +msgctxt "dialog header" +msgid "Delete Daft Order" +msgstr "" + #: build/locale/src/products/views/ProductImage.json #. [src.products.views.1731766393] - dialog header #. defaultMessage is: @@ -2747,6 +2851,26 @@ msgctxt "dialog header" msgid "Delete Image" msgstr "" +#: build/locale/src/navigation/views/MenuDetails/index.json +#. [menuDetailsDeleteMenuHeader] - dialog header +#. defaultMessage is: +#. Delete Menu +#: build/locale/src/navigation/views/MenuList.json +#. [menuListDeleteMenuHeader] - dialog header +#. defaultMessage is: +#. Delete Menu +msgctxt "dialog header" +msgid "Delete Menu" +msgstr "" + +#: build/locale/src/navigation/views/MenuList.json +#. [menuListDeleteMenusHeader] - dialog header +#. defaultMessage is: +#. Delete Menus +msgctxt "dialog header" +msgid "Delete Menus" +msgstr "" + #: build/locale/src/orders/views/OrderDraftList/OrderDraftList.json #. [src.orders.views.OrderDraftList.1161115149] - dialog header #. defaultMessage is: @@ -2987,36 +3111,12 @@ msgctxt "dialog title" msgid "Delete collections" msgstr "" -#: build/locale/src/customers/views/CustomerDetails.json -#. [src.customers.views.442409664] - dialog header -#. defaultMessage is: -#. Delete customer -msgctxt "dialog header" -msgid "Delete customer" -msgstr "" - -#: build/locale/src/customers/views/CustomerList/CustomerList.json -#. [src.customers.views.CustomerList.1946482599] - dialog header -#. defaultMessage is: -#. Delete customers -msgctxt "dialog header" -msgid "Delete customers" -msgstr "" - -#: build/locale/src/orders/components/OrderDraftCancelDialog/OrderDraftCancelDialog.json -#. [src.orders.components.OrderDraftCancelDialog.632633254] - dialog header -#. defaultMessage is: -#. Delete draft order -msgctxt "dialog header" -msgid "Delete draft order" -msgstr "" - #: build/locale/src/shipping/views/ShippingZoneDetails/ShippingZoneDetailsDialogs.json -#. [src.shipping.views.ShippingZoneDetails.1947090060] - unassign country, dialog header +#. [src.shipping.views.ShippingZoneDetails.3133838427] - unassign country, dialog header #. defaultMessage is: -#. Delete from shipping zone +#. Delete from Shipping Zone msgctxt "unassign country, dialog header" -msgid "Delete from shipping zone" +msgid "Delete from Shipping Zone" msgstr "" #: build/locale/src/collections/views/CollectionDetails.json @@ -3027,26 +3127,6 @@ msgctxt "dialog title" msgid "Delete image" msgstr "" -#: build/locale/src/navigation/views/MenuDetails/index.json -#. [menuDetailsDeleteMenuHeader] - dialog header -#. defaultMessage is: -#. Delete menu -#: build/locale/src/navigation/views/MenuList.json -#. [menuListDeleteMenuHeader] - dialog header -#. defaultMessage is: -#. Delete menu -msgctxt "dialog header" -msgid "Delete menu" -msgstr "" - -#: build/locale/src/navigation/views/MenuList.json -#. [menuListDeleteMenusHeader] - dialog header -#. defaultMessage is: -#. Delete menus -msgctxt "dialog header" -msgid "Delete menus" -msgstr "" - #: build/locale/src/staff/components/StaffProperties/StaffProperties.json #. [src.staff.components.StaffProperties.457748370] - button #. defaultMessage is: @@ -3163,12 +3243,12 @@ msgctxt "description" msgid "Discount Code" msgstr "" -#: build/locale/src/discounts/components/VoucherValue/VoucherValue.json -#. [src.discounts.components.VoucherValue.1971417066] +#: build/locale/src/discounts/components/SaleType/SaleType.json +#. [src.discounts.components.SaleType.3216816841] - percentage or fixed, header #. defaultMessage is: -#. Discount Specific Information -msgctxt "description" -msgid "Discount Specific Information" +#. Discount Type +msgctxt "percentage or fixed, header" +msgid "Discount Type" msgstr "" #: build/locale/src/discounts/components/VoucherTypes/VoucherTypes.json @@ -3179,10 +3259,14 @@ msgctxt "header" msgid "Discount Type" msgstr "" -#: build/locale/src/discounts/components/SalePricing/SalePricing.json -#. [src.discounts.components.SalePricing.1205967018] +#: build/locale/src/discounts/components/SaleValue/SaleValue.json +#. [src.discounts.components.SaleValue.1205967018] - sale discount #. defaultMessage is: #. Discount Value +msgctxt "sale discount" +msgid "Discount Value" +msgstr "" + #: build/locale/src/discounts/components/VoucherValue/VoucherValue.json #. [src.discounts.components.VoucherValue.1205967018] #. defaultMessage is: @@ -3319,6 +3403,22 @@ msgctxt "dialog title" msgid "Edit Address" msgstr "" +#: build/locale/src/orders/components/OrderAddressEditDialog/OrderAddressEditDialog.json +#. [src.orders.components.OrderAddressEditDialog.3982060155] - dialog header +#. defaultMessage is: +#. Edit Billing Address +msgctxt "dialog header" +msgid "Edit Billing Address" +msgstr "" + +#: build/locale/src/orders/components/OrderCustomerEditDialog/OrderCustomerEditDialog.json +#. [src.orders.components.OrderCustomerEditDialog.1549172886] - dialog header +#. defaultMessage is: +#. Edit Customer Details +msgctxt "dialog header" +msgid "Edit Customer Details" +msgstr "" + #: build/locale/src/navigation/components/MenuItemDialog/MenuItemDialog.json #. [menuItemDialogEditItem] - edit menu item, header #. defaultMessage is: @@ -3343,6 +3443,14 @@ msgctxt "dialog header" msgid "Edit Price Rate" msgstr "" +#: build/locale/src/orders/components/OrderAddressEditDialog/OrderAddressEditDialog.json +#. [src.orders.components.OrderAddressEditDialog.3278396777] - dialog header +#. defaultMessage is: +#. Edit Shipping Address +msgctxt "dialog header" +msgid "Edit Shipping Address" +msgstr "" + #: build/locale/src/orders/components/OrderShippingMethodEditDialog/OrderShippingMethodEditDialog.json #. [src.orders.components.OrderShippingMethodEditDialog.3369240294] - dialog header #. defaultMessage is: @@ -3367,30 +3475,6 @@ msgctxt "edit weight based shipping method, dialog header" msgid "Edit Weight Rate" msgstr "" -#: build/locale/src/orders/components/OrderAddressEditDialog/OrderAddressEditDialog.json -#. [src.orders.components.OrderAddressEditDialog.2935008093] - dialog header -#. defaultMessage is: -#. Edit billing address -msgctxt "dialog header" -msgid "Edit billing address" -msgstr "" - -#: build/locale/src/orders/components/OrderCustomerEditDialog/OrderCustomerEditDialog.json -#. [src.orders.components.OrderCustomerEditDialog.1411666943] - dialog header -#. defaultMessage is: -#. Edit customer details -msgctxt "dialog header" -msgid "Edit customer details" -msgstr "" - -#: build/locale/src/orders/components/OrderAddressEditDialog/OrderAddressEditDialog.json -#. [src.orders.components.OrderAddressEditDialog.462765358] - dialog header -#. defaultMessage is: -#. Edit shipping address -msgctxt "dialog header" -msgid "Edit shipping address" -msgstr "" - #: build/locale/src/components/SeoForm/SeoForm.json #. [src.components.SeoForm.3198271020] - button #. defaultMessage is: @@ -3563,6 +3647,14 @@ msgctxt "button" msgid "Finalize" msgstr "" +#: build/locale/src/orders/components/OrderDraftFinalizeDialog/OrderDraftFinalizeDialog.json +#. [src.orders.components.OrderDraftFinalizeDialog.1161061962] - dialog header +#. defaultMessage is: +#. Finalize Draft Order +msgctxt "dialog header" +msgid "Finalize Draft Order" +msgstr "" + #: build/locale/src/orders/components/OrderDraftFinalizeDialog/OrderDraftFinalizeDialog.json #. [src.orders.components.OrderDraftFinalizeDialog.678764806] - button #. defaultMessage is: @@ -3571,14 +3663,6 @@ msgctxt "button" msgid "Finalize anyway" msgstr "" -#: build/locale/src/orders/components/OrderDraftFinalizeDialog/OrderDraftFinalizeDialog.json -#. [src.orders.components.OrderDraftFinalizeDialog.845440998] - dialog header -#. defaultMessage is: -#. Finalize draft order -msgctxt "dialog header" -msgid "Finalize draft order" -msgstr "" - #: build/locale/src/intl.json #. [src.firstName] #. defaultMessage is: @@ -3587,6 +3671,14 @@ msgctxt "description" msgid "First Name" msgstr "" +#: build/locale/src/discounts/components/SaleType/SaleType.json +#. [src.discounts.components.SaleType.46415128] - discount type +#. defaultMessage is: +#. Fixed Amount +msgctxt "discount type" +msgid "Fixed Amount" +msgstr "" + #: build/locale/src/discounts/components/VoucherTypes/VoucherTypes.json #. [src.discounts.components.VoucherTypes.46415128] - voucher discount type #. defaultMessage is: @@ -3628,11 +3720,11 @@ msgid "Fulfill" msgstr "" #: build/locale/src/orders/components/OrderFulfillmentDialog/OrderFulfillmentDialog.json -#. [src.orders.components.OrderFulfillmentDialog.3236546219] - dialog header +#. [src.orders.components.OrderFulfillmentDialog.3928354289] - dialog header #. defaultMessage is: -#. Fulfill products +#. Fulfill Oroducts msgctxt "dialog header" -msgid "Fulfill products" +msgid "Fulfill Oroducts" msgstr "" #: build/locale/src/misc.json @@ -3931,6 +4023,14 @@ msgctxt "product stock" msgid "Inventory" msgstr "" +#: build/locale/src/products/components/ProductVariantCreateDialog/ProductVariantCreateSummary.json +#. [src.products.components.ProductVariantCreateDialog.3490038570] - variant stock amount +#. defaultMessage is: +#. Inventory +msgctxt "variant stock amount" +msgid "Inventory" +msgstr "" + #: build/locale/src/products/components/ProductVariantStock/ProductVariantStock.json #. [src.products.components.ProductVariantStock.3490038570] - product variant stock #. defaultMessage is: @@ -4507,6 +4607,14 @@ msgctxt "tax rate" msgid "Newspapers" msgstr "" +#: build/locale/src/products/components/ProductVariantCreateDialog/ProductVariantCreateDialog.json +#. [src.products.components.ProductVariantCreateDialog.3673120330] - button +#. defaultMessage is: +#. Next +msgctxt "button" +msgid "Next" +msgstr "" + #: build/locale/src/intl.json #. [src.no] #. defaultMessage is: @@ -5519,6 +5627,14 @@ msgctxt "order history message" msgid "Payment was voided" msgstr "" +#: build/locale/src/discounts/components/SaleType/SaleType.json +#. [src.discounts.components.SaleType.3688224049] - discount type +#. defaultMessage is: +#. Percentage +msgctxt "discount type" +msgid "Percentage" +msgstr "" + #: build/locale/src/discounts/components/VoucherTypes/VoucherTypes.json #. [src.discounts.components.VoucherTypes.3688224049] - voucher discount type #. defaultMessage is: @@ -5651,6 +5767,14 @@ msgctxt "order payment" msgid "Preauthorized amount" msgstr "" +#: build/locale/src/products/components/ProductVariantCreateDialog/ProductVariantCreateDialog.json +#. [src.products.components.ProductVariantCreateDialog.904693740] - previous step, button +#. defaultMessage is: +#. Previous +msgctxt "previous step, button" +msgid "Previous" +msgstr "" + #: build/locale/src/categories/components/CategoryProductList/CategoryProductList.json #. [src.categories.components.CategoryProductList.1134347598] - product price #. defaultMessage is: @@ -5695,10 +5819,34 @@ msgstr "" #. [src.products.components.ProductListFilter.1134347598] #. defaultMessage is: #. Price +#: build/locale/src/products/components/ProductVariantCreateDialog/ProductVariantCreatePrices.json +#. [productVariantCreatePricesPriceInputLabel] +#. defaultMessage is: +#. Price msgctxt "description" msgid "Price" msgstr "" +#: build/locale/src/products/components/ProductVariantCreateDialog/ProductVariantCreatePrices.json +#. [src.products.components.ProductVariantCreateDialog.1134347598] - variant price, header +#. defaultMessage is: +#. Price +msgctxt "variant price, header" +msgid "Price" +msgstr "" + +#: build/locale/src/products/components/ProductVariantCreateDialog/ProductVariantCreatePrices.json +#. [productVariantCreatePricesSetPricePlaceholder] - variant price +#. defaultMessage is: +#. Price +#: build/locale/src/products/components/ProductVariantCreateDialog/ProductVariantCreateSummary.json +#. [src.products.components.ProductVariantCreateDialog.1134347598] - variant price +#. defaultMessage is: +#. Price +msgctxt "variant price" +msgid "Price" +msgstr "" + #: build/locale/src/products/components/ProductVariants/ProductVariants.json #. [src.products.components.ProductVariants.1134347598] - product variant price #. defaultMessage is: @@ -5747,12 +5895,12 @@ msgctxt "filter by price" msgid "Price to {price}" msgstr "" -#: build/locale/src/discounts/components/SalePricing/SalePricing.json -#. [src.discounts.components.SalePricing.1099355007] - sale pricing, header +#: build/locale/src/products/components/ProductVariantCreateDialog/ProductVariantCreateTabs.json +#. [src.products.components.ProductVariantCreateDialog.705096461] - variant creation step #. defaultMessage is: -#. Pricing -msgctxt "sale pricing, header" -msgid "Pricing" +#. Prices and SKU +msgctxt "variant creation step" +msgid "Prices and SKU" msgstr "" #: build/locale/src/products/components/ProductPricing/ProductPricing.json @@ -5772,11 +5920,11 @@ msgid "Pricing" msgstr "" #: build/locale/src/customers/components/CustomerCreateAddress/CustomerCreateAddress.json -#. [src.customers.components.CustomerCreateAddress.1922654050] - page header +#. [src.customers.components.CustomerCreateAddress.1751533141] - page header #. defaultMessage is: -#. Primary address +#. Primary Address msgctxt "page header" -msgid "Primary address" +msgid "Primary Address" msgstr "" #: build/locale/src/orders/components/OrderDraftDetailsProducts/OrderDraftDetailsProducts.json @@ -6232,11 +6380,11 @@ msgid "Ready to Capture" msgstr "" #: build/locale/src/customers/components/CustomerOrders/CustomerOrders.json -#. [src.customers.components.CustomerOrders.3878642352] - section header +#. [src.customers.components.CustomerOrders.1899831623] - section header #. defaultMessage is: -#. Recent orders +#. Recent Orders msgctxt "section header" -msgid "Recent orders" +msgid "Recent Orders" msgstr "" #: build/locale/src/taxes/components/CountryList/CountryList.json @@ -6387,6 +6535,10 @@ msgctxt "product's sku" msgid "SKU" msgstr "" +#: build/locale/src/products/components/ProductVariantCreateDialog/ProductVariantCreateSummary.json +#. [src.products.components.ProductVariantCreateDialog.693960049] +#. defaultMessage is: +#. SKU #: build/locale/src/products/components/ProductVariants/ProductVariants.json #. [src.products.components.ProductVariants.693960049] #. defaultMessage is: @@ -6839,6 +6991,14 @@ 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: +#. Select Values +msgctxt "attribute values, variant creation step" +msgid "Select Values" +msgstr "" + #: build/locale/src/products/components/ProductVariantImages/ProductVariantImages.json #. [src.products.components.ProductVariantImages.3449133076] #. defaultMessage is: @@ -6951,6 +7111,10 @@ msgctxt "button" msgid "Set as default shipping address" msgstr "" +#: build/locale/src/discounts/components/DiscountDates/DiscountDates.json +#. [src.discounts.components.DiscountDates.1596226028] - voucher end date, switch button +#. defaultMessage is: +#. Set end date #: build/locale/src/discounts/components/VoucherDates/VoucherDates.json #. [src.discounts.components.VoucherDates.1596226028] - voucher end date, switch button #. defaultMessage is: @@ -7023,6 +7187,14 @@ msgctxt "header" msgid "Shipping" msgstr "" +#: build/locale/src/customers/components/CustomerAddresses/CustomerAddresses.json +#. [src.customers.components.CustomerAddresses.2758581442] - subsection header +#. defaultMessage is: +#. Shipping Address +msgctxt "subsection header" +msgid "Shipping Address" +msgstr "" + #: build/locale/src/orders/components/OrderCustomer/OrderCustomer.json #. [src.orders.components.OrderCustomer.2758581442] #. defaultMessage is: @@ -7031,6 +7203,14 @@ msgctxt "description" msgid "Shipping Address" msgstr "" +#: build/locale/src/shipping/components/ShippingZonesList/ShippingZonesList.json +#. [src.shipping.components.ShippingZonesList.120574110] - sort shipping methods by zone, section header +#. defaultMessage is: +#. Shipping By Zone +msgctxt "sort shipping methods by zone, section header" +msgid "Shipping By Zone" +msgstr "" + #: build/locale/src/intl.json #. [src.shipping] - shipping section name #. defaultMessage is: @@ -7055,22 +7235,6 @@ msgctxt "description" msgid "Shipping Zone Name" msgstr "" -#: build/locale/src/customers/components/CustomerAddresses/CustomerAddresses.json -#. [src.customers.components.CustomerAddresses.3517722732] - subsection header -#. defaultMessage is: -#. Shipping address -msgctxt "subsection header" -msgid "Shipping address" -msgstr "" - -#: build/locale/src/shipping/components/ShippingZonesList/ShippingZonesList.json -#. [src.shipping.components.ShippingZonesList.2942726079] - sort shipping methods by zone, section header -#. defaultMessage is: -#. Shipping by zone -msgctxt "sort shipping methods by zone, section header" -msgid "Shipping by zone" -msgstr "" - #: build/locale/src/orders/components/OrderHistory/OrderHistory.json #. [src.orders.components.OrderHistory.651019008] - order history message #. defaultMessage is: @@ -7347,6 +7511,30 @@ msgctxt "product stock" msgid "Stock" msgstr "" +#: build/locale/src/products/components/ProductVariantCreateDialog/ProductVariantCreatePrices.json +#. [src.products.components.ProductVariantCreateDialog.3841616483] - variant stock, header +#. defaultMessage is: +#. Stock +msgctxt "variant stock, header" +msgid "Stock" +msgstr "" + +#: build/locale/src/products/components/ProductVariantCreateDialog/ProductVariantCreatePrices.json +#. [productVariantCreatePricesStockInputLabel] +#. defaultMessage is: +#. Stock +msgctxt "description" +msgid "Stock" +msgstr "" + +#: build/locale/src/products/components/ProductVariantCreateDialog/ProductVariantCreatePrices.json +#. [productVariantCreatePricesSetStockPlaceholder] - variant stock +#. defaultMessage is: +#. Stock +msgctxt "variant stock" +msgid "Stock" +msgstr "" + #: build/locale/src/products/components/ProductVariantStock/ProductVariantStock.json #. [src.products.components.ProductVariantStock.3841616483] - product variant stock, section header #. defaultMessage is: @@ -7371,6 +7559,14 @@ msgctxt "description" msgid "Store Description" msgstr "" +#: build/locale/src/siteSettings/components/SiteSettingsAddress/SiteSettingsAddress.json +#. [src.siteSettings.components.SiteSettingsAddress.229184360] - section header +#. defaultMessage is: +#. Store Information +msgctxt "section header" +msgid "Store Information" +msgstr "" + #: build/locale/src/siteSettings/components/SiteSettingsDetails/SiteSettingsDetails.json #. [src.siteSettings.components.SiteSettingsDetails.529433178] #. defaultMessage is: @@ -7379,14 +7575,6 @@ msgctxt "description" msgid "Store description is shown on taskbar after your store name" msgstr "" -#: build/locale/src/siteSettings/components/SiteSettingsAddress/SiteSettingsAddress.json -#. [src.siteSettings.components.SiteSettingsAddress.1150975268] - section header -#. defaultMessage is: -#. Store information -msgctxt "section header" -msgid "Store information" -msgstr "" - #: build/locale/src/attributes/components/AttributeProperties/AttributeProperties.json #. [src.attributes.components.AttributeProperties.1877630205] - attribute properties regarding storefront #. defaultMessage is: @@ -7491,6 +7679,14 @@ msgctxt "description" msgid "Summary" msgstr "" +#: build/locale/src/products/components/ProductVariantCreateDialog/ProductVariantCreateTabs.json +#. [src.products.components.ProductVariantCreateDialog.2745385064] - variant creation step +#. defaultMessage is: +#. Summary +msgctxt "variant creation step" +msgid "Summary" +msgstr "" + #: build/locale/src/productTypes/components/ProductTypeList/ProductTypeList.json #. [src.productTypes.components.ProductTypeList.1240292548] - tax rate for a product type #. defaultMessage is: @@ -7707,14 +7903,6 @@ msgctxt "description" msgid "This will be shown to customers at checkout" msgstr "" -#: build/locale/src/discounts/components/SalePricing/SalePricing.json -#. [src.discounts.components.SalePricing.2503204759] - time during which sale is active -#. defaultMessage is: -#. Time Frame -msgctxt "time during which sale is active" -msgid "Time Frame" -msgstr "" - #: build/locale/src/pages/components/PageInfo/PageInfo.json #. [src.pages.components.PageInfo.1124600214] - page title #. defaultMessage is: @@ -7754,9 +7942,9 @@ msgstr "" #: build/locale/src/home/components/HomeProductListCard/HomeProductListCard.json #. [homeProductsListCardHeader] - header #. defaultMessage is: -#. Top products +#. Top Products msgctxt "header" -msgid "Top products" +msgid "Top Products" msgstr "" #: build/locale/src/customers/components/CustomerOrders/CustomerOrders.json @@ -8383,6 +8571,14 @@ msgctxt "sale value" msgid "Value" msgstr "" +#: build/locale/src/discounts/components/SaleValue/SaleValue.json +#. [src.discounts.components.SaleValue.1148029984] - sale value, header +#. defaultMessage is: +#. Value +msgctxt "sale value, header" +msgid "Value" +msgstr "" + #: build/locale/src/discounts/components/VoucherList/VoucherList.json #. [src.discounts.components.VoucherList.1148029984] - voucher value #. defaultMessage is: @@ -8459,6 +8655,14 @@ msgctxt "attribute values" msgid "Values" msgstr "" +#: build/locale/src/products/components/ProductVariantCreateDialog/ProductVariantCreateSummary.json +#. [src.products.components.ProductVariantCreateDialog.998917294] - variant name +#. defaultMessage is: +#. Variant +msgctxt "variant name" +msgid "Variant" +msgstr "" + #: build/locale/src/translations/components/TranslationsProductTypesPage/TranslationsProductTypesPage.json #. [src.translations.components.TranslationsProductTypesPage.3538502409] - header #. defaultMessage is: @@ -8607,6 +8811,14 @@ msgctxt "description" msgid "Voucher Name" msgstr "" +#: build/locale/src/discounts/components/VoucherValue/VoucherValue.json +#. [src.discounts.components.VoucherValue.1960678372] +#. defaultMessage is: +#. Voucher Specific Information +msgctxt "description" +msgid "Voucher Specific Information" +msgstr "" + #: build/locale/src/discounts/components/VoucherDetailsPage/VoucherDetailsPage.json #. [src.discounts.components.VoucherDetailsPage.2071139683] #. defaultMessage is: @@ -8711,6 +8923,14 @@ msgctxt "description" msgid "Yes" msgstr "" +#: build/locale/src/products/components/ProductVariantCreateDialog/ProductVariantCreateSummary.json +#. [src.products.components.ProductVariantCreateDialog.1009678918] - header +#. defaultMessage is: +#. You will create variants below +msgctxt "header" +msgid "You will create variants below" +msgstr "" + #: build/locale/src/components/AddressEdit/AddressEdit.json #. [src.components.AddressEdit.2965971965] #. defaultMessage is: diff --git a/package-lock.json b/package-lock.json index 6881d9537..e437333ce 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1742,12 +1742,40 @@ "glob-to-regexp": "^0.3.0" } }, + "@nodelib/fs.scandir": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz", + "integrity": "sha512-eGmwYQn3gxo4r7jdQnkrrN6bY478C3P+a/y72IJukF8LjB6ZHeB3c+Ehacj3sYeSmUXGlnA67/PmbM9CVwL7Dw==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "2.0.3", + "run-parallel": "^1.1.9" + }, + "dependencies": { + "@nodelib/fs.stat": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.3.tgz", + "integrity": "sha512-bQBFruR2TAwoevBEd/NWMoAAtNGzTRgdrqnYCc7dhzfoNvqPzLyqlEQnzZ3kVnNrSp25iyxE00/3h2fqGAGArA==", + "dev": true + } + } + }, "@nodelib/fs.stat": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz", "integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==", "dev": true }, + "@nodelib/fs.walk": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.4.tgz", + "integrity": "sha512-1V9XOY4rDW0rehzbrcqAmHnz8e7SKvX27gh8Gt2WgB0+pdzdiLV83p72kZPU+jvMbS1qU5mauP2iOvO8rhmurQ==", + "dev": true, + "requires": { + "@nodelib/fs.scandir": "2.1.3", + "fastq": "^1.6.0" + } + }, "@oclif/color": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/@oclif/color/-/color-0.0.0.tgz", @@ -3416,6 +3444,30 @@ "es6-promisify": "^5.0.0" } }, + "aggregate-error": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.0.1.tgz", + "integrity": "sha512-quoaXsZ9/BLNae5yiNoUz+Nhkwz83GhWwtYFglcjEQB2NDHCIpApbqXxIFnm4Pq/Nvhrsq5sYJFyohrrxnTGAA==", + "dev": true, + "requires": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "dependencies": { + "clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true + }, + "indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true + } + } + }, "airbnb-js-shims": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/airbnb-js-shims/-/airbnb-js-shims-2.2.0.tgz", @@ -8938,6 +8990,15 @@ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=" }, + "fastq": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.6.0.tgz", + "integrity": "sha512-jmxqQ3Z/nXoeyDmWAzF9kH1aGZSis6e/SbfPmJpUnyZ0ogr6iscHQaml4wsEepEWSdtmpy+eVXmCRIMpxaXqOA==", + "dev": true, + "requires": { + "reusify": "^1.0.0" + } + }, "fault": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/fault/-/fault-1.0.3.tgz", @@ -9967,6 +10028,12 @@ "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", "dev": true }, + "get-own-enumerable-property-symbols": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.1.tgz", + "integrity": "sha512-09/VS4iek66Dh2bctjRkowueRJbY1JDGR1L/zRxO1Qk8Uxs6PnqaNSqalpizPT+CDjre3hnEsuzvhgomz9qYrA==", + "dev": true + }, "get-stdin": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", @@ -10911,6 +10978,48 @@ } } }, + "husky": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/husky/-/husky-3.0.8.tgz", + "integrity": "sha512-HFOsgcyrX3qe/rBuqyTt+P4Gxn5P0seJmr215LAZ/vnwK3jWB3r0ck7swbzGRUbufCf9w/lgHPVbF/YXQALgfQ==", + "dev": true, + "requires": { + "chalk": "^2.4.2", + "cosmiconfig": "^5.2.1", + "execa": "^1.0.0", + "get-stdin": "^7.0.0", + "is-ci": "^2.0.0", + "opencollective-postinstall": "^2.0.2", + "pkg-dir": "^4.2.0", + "please-upgrade-node": "^3.2.0", + "read-pkg": "^5.1.1", + "run-node": "^1.0.0", + "slash": "^3.0.0" + }, + "dependencies": { + "get-stdin": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-7.0.0.tgz", + "integrity": "sha512-zRKcywvrXlXsA0v0i9Io4KDRaAw7+a1ZpjRwl9Wox8PFlVCCHra7E9c4kqXCoCM9nR5tBkaTTZRBoCm60bFqTQ==", + "dev": true + }, + "pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "requires": { + "find-up": "^4.0.0" + } + }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + } + } + }, "hyperlinker": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/hyperlinker/-/hyperlinker-1.0.0.tgz", @@ -11536,6 +11645,12 @@ "integrity": "sha1-8mWrian0RQNO9q/xWo8AsA9VF5k=", "dev": true }, + "is-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", + "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=", + "dev": true + }, "is-object": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-object/-/is-object-1.0.1.tgz", @@ -11611,6 +11726,12 @@ "has": "^1.0.1" } }, + "is-regexp": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz", + "integrity": "sha1-/S2INUXEa6xaYz57mgnof6LLUGk=", + "dev": true + }, "is-relative": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz", @@ -12940,6 +13061,319 @@ "integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=", "dev": true }, + "lint-staged": { + "version": "9.4.2", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-9.4.2.tgz", + "integrity": "sha512-OFyGokJSWTn2M6vngnlLXjaHhi8n83VIZZ5/1Z26SULRUWgR3ITWpAEQC9Pnm3MC/EpCxlwts/mQWDHNji2+zA==", + "dev": true, + "requires": { + "chalk": "^2.4.2", + "commander": "^2.20.0", + "cosmiconfig": "^5.2.1", + "debug": "^4.1.1", + "dedent": "^0.7.0", + "del": "^5.0.0", + "execa": "^2.0.3", + "listr": "^0.14.3", + "log-symbols": "^3.0.0", + "micromatch": "^4.0.2", + "normalize-path": "^3.0.0", + "please-upgrade-node": "^3.1.1", + "string-argv": "^0.3.0", + "stringify-object": "^3.3.0" + }, + "dependencies": { + "@nodelib/fs.stat": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.3.tgz", + "integrity": "sha512-bQBFruR2TAwoevBEd/NWMoAAtNGzTRgdrqnYCc7dhzfoNvqPzLyqlEQnzZ3kVnNrSp25iyxE00/3h2fqGAGArA==", + "dev": true + }, + "array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "cross-spawn": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.1.tgz", + "integrity": "sha512-u7v4o84SwFpD32Z8IIcPZ6z1/ie24O6RU3RbtL5Y316l3KuHVPx9ItBgWQ6VlfAFnRnTtMUrsQ9MUUTuEZjogg==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "dedent": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", + "integrity": "sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw=", + "dev": true + }, + "del": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/del/-/del-5.1.0.tgz", + "integrity": "sha512-wH9xOVHnczo9jN2IW68BabcecVPxacIA3g/7z6vhSU/4stOKQzeCRK0yD0A24WiAAUJmmVpWqrERcTxnLo3AnA==", + "dev": true, + "requires": { + "globby": "^10.0.1", + "graceful-fs": "^4.2.2", + "is-glob": "^4.0.1", + "is-path-cwd": "^2.2.0", + "is-path-inside": "^3.0.1", + "p-map": "^3.0.0", + "rimraf": "^3.0.0", + "slash": "^3.0.0" + } + }, + "dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "requires": { + "path-type": "^4.0.0" + } + }, + "execa": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-2.1.0.tgz", + "integrity": "sha512-Y/URAVapfbYy2Xp/gb6A0E7iR8xeqOCXsuuaoMn7A5PzrXUK84E1gyiEfq0wQd/GHA6GsoHWwhNq8anb0mleIw==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.0", + "get-stream": "^5.0.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^3.0.0", + "onetime": "^5.1.0", + "p-finally": "^2.0.0", + "signal-exit": "^3.0.2", + "strip-final-newline": "^2.0.0" + } + }, + "fast-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.1.0.tgz", + "integrity": "sha512-TrUz3THiq2Vy3bjfQUB2wNyPdGBeGmdjbzzBLhfHN4YFurYptCKwGq/TfiRavbGywFRzY6U2CdmQ1zmsY5yYaw==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.0", + "merge2": "^1.3.0", + "micromatch": "^4.0.2" + }, + "dependencies": { + "merge2": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.3.0.tgz", + "integrity": "sha512-2j4DAdlBOkiSZIsaXk4mTE3sRS02yBHAtfy127xRV3bQUFqXkjHCHLW6Scv7DwNRbIWNHH8zpnz9zMaKXIdvYw==", + "dev": true + } + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "get-stream": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.1.0.tgz", + "integrity": "sha512-EXr1FOzrzTfGeL0gQdeFEvOMm2mzMOglyiOXSTpPC+iAjAKftbr3jpCMWynogwYnM+eSj9sHGc6wjIcDvYiygw==", + "dev": true, + "requires": { + "pump": "^3.0.0" + } + }, + "glob-parent": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.0.tgz", + "integrity": "sha512-qjtRgnIVmOfnKUE3NJAQEdk+lKrxfw8t5ke7SXtfMTHcjsBfOfWXCQfdb30zfDoZQ2IRSIiidmjtbHZPZ++Ihw==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "globby": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/globby/-/globby-10.0.1.tgz", + "integrity": "sha512-sSs4inE1FB2YQiymcmTv6NWENryABjUNPeWhOvmn4SjtKybglsyPZxFB3U1/+L1bYi0rNZDqCLlHyLYDl1Pq5A==", + "dev": true, + "requires": { + "@types/glob": "^7.1.1", + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.0.3", + "glob": "^7.1.3", + "ignore": "^5.1.1", + "merge2": "^1.2.3", + "slash": "^3.0.0" + } + }, + "graceful-fs": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.2.tgz", + "integrity": "sha512-IItsdsea19BoLC7ELy13q1iJFNmd7ofZH5+X/pJr90/nRoPEX0DJo1dHDbgtYWOhJhcCgMDTOw84RZ72q6lB+Q==", + "dev": true + }, + "ignore": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.4.tgz", + "integrity": "sha512-MzbUSahkTW1u7JpKKjY7LCARd1fU5W2rLdxlM4kdkayuCwZImjkpluF9CM1aLewYJguPDqewLam18Y6AU69A8A==", + "dev": true + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "is-path-inside": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.2.tgz", + "integrity": "sha512-/2UGPSgmtqwo1ktx8NDHjuPwZWmHhO+gj0f93EkhLB5RgW9RZevWYYlIkS6zePc6U2WpOdQYIwHe9YC4DWEBVg==", + "dev": true + }, + "is-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", + "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==", + "dev": true + }, + "log-symbols": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-3.0.0.tgz", + "integrity": "sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ==", + "dev": true, + "requires": { + "chalk": "^2.4.2" + } + }, + "merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "micromatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", + "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", + "dev": true, + "requires": { + "braces": "^3.0.1", + "picomatch": "^2.0.5" + } + }, + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true + }, + "onetime": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.0.tgz", + "integrity": "sha512-5NcSkPHhwTVFIQN+TUqXoS5+dlElHXdpAWu9I0HP20YOtIi+aZ0Ct82jdlILDxjLEAWwvm+qj1m6aEtsDVmm6Q==", + "dev": true, + "requires": { + "mimic-fn": "^2.1.0" + } + }, + "p-finally": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-2.0.1.tgz", + "integrity": "sha512-vpm09aKwq6H9phqRQzecoDpD8TmVyGw70qmWlyq5onxY7tqyTTFVvxMykxQSQKILBSFlbXpypIw2T1Ml7+DDtw==", + "dev": true + }, + "p-map": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz", + "integrity": "sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==", + "dev": true, + "requires": { + "aggregate-error": "^3.0.0" + } + }, + "path-key": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.0.tgz", + "integrity": "sha512-8cChqz0RP6SHJkMt48FW0A7+qUOn+OsnOsVtzI59tZ8m+5bCSk7hzwET0pulwOM2YMn9J1efb07KB9l9f30SGg==", + "dev": true + }, + "path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true + }, + "rimraf": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.0.tgz", + "integrity": "sha512-NDGVxTsjqfunkds7CqsOiEnxln4Bo7Nddl3XhS4pXg5OzwkLqJ971ZVAAnB+DDLnF76N+VnDEiBHaVV8I06SUg==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "which": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.1.tgz", + "integrity": "sha512-N7GBZOTswtB9lkQBZA4+zAXrjEIWAUOB93AvzUiudRzRxhUdLURQ7D/gAIMY1gatT/LTbmbcv8SiYazy3eYB7w==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, "linux-platform-info": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/linux-platform-info/-/linux-platform-info-0.0.3.tgz", @@ -14588,6 +15022,12 @@ "is-wsl": "^1.1.0" } }, + "opencollective-postinstall": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/opencollective-postinstall/-/opencollective-postinstall-2.0.2.tgz", + "integrity": "sha512-pVOEP16TrAO2/fjej1IdOyupJY8KDUM1CvsaScRbw6oddvpQoOfGk4ywha0HKKVAD6RkW4x6Q+tNBwhf3Bgpuw==", + "dev": true + }, "opn": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/opn/-/opn-3.0.3.tgz", @@ -15071,6 +15511,12 @@ "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" }, + "picomatch": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.0.7.tgz", + "integrity": "sha512-oLHIdio3tZ0qH76NybpeneBhYVj0QFTfXEFTc/B3zKQspYfYYkWYgFsmzo+4kvId/bQRcNkVeguI3y+CD22BtA==", + "dev": true + }, "pify": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", @@ -15205,6 +15651,15 @@ } } }, + "please-upgrade-node": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz", + "integrity": "sha512-gQR3WpIgNIKwBMVLkpMUeR3e1/E1y42bqDQZfql+kDeXd8COYfM8PQA4X6y7a8u9Ua9FHmsrrmirW2vHs45hWg==", + "dev": true, + "requires": { + "semver-compare": "^1.0.0" + } + }, "plop": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/plop/-/plop-2.4.0.tgz", @@ -17080,6 +17535,12 @@ "integrity": "sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs=", "dev": true }, + "reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true + }, "rimraf": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.0.tgz", @@ -17124,6 +17585,18 @@ "is-promise": "^2.1.0" } }, + "run-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/run-node/-/run-node-1.0.0.tgz", + "integrity": "sha512-kc120TBlQ3mih1LSzdAJXo4xn/GWS2ec0l3S+syHDXP9uRr0JAT8Qd3mdMuyjqCzeZktgP3try92cEgf9Nks8A==", + "dev": true + }, + "run-parallel": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.1.9.tgz", + "integrity": "sha512-DEqnSRTDw/Tc3FXf49zedI638Z9onwUotBMiUFKmrO2sdFKIbXamXGQ3Axd4qgphxKB4kw/qP1w5kTxnfU1B9Q==", + "dev": true + }, "run-queue": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/run-queue/-/run-queue-1.0.3.tgz", @@ -17239,6 +17712,12 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==" }, + "semver-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", + "integrity": "sha1-De4hahyUGrN+nvsXiPavxf9VN/w=", + "dev": true + }, "send": { "version": "0.17.1", "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", @@ -18040,6 +18519,12 @@ "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz", "integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=" }, + "string-argv": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.1.tgz", + "integrity": "sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg==", + "dev": true + }, "string-length": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/string-length/-/string-length-2.0.0.tgz", @@ -18164,6 +18649,17 @@ } } }, + "stringify-object": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/stringify-object/-/stringify-object-3.3.0.tgz", + "integrity": "sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==", + "dev": true, + "requires": { + "get-own-enumerable-property-symbols": "^3.0.0", + "is-obj": "^1.0.1", + "is-regexp": "^1.0.0" + } + }, "strip-ansi": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", @@ -18182,6 +18678,12 @@ "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=" }, + "strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true + }, "style-loader": { "version": "0.23.1", "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-0.23.1.tgz", diff --git a/package.json b/package.json index a02ae882e..9ba48a59b 100644 --- a/package.json +++ b/package.json @@ -115,8 +115,10 @@ "file-loader": "^1.1.11", "fork-ts-checker-webpack-plugin": "^0.5.2", "html-webpack-plugin": "^3.2.0", + "husky": "^3.0.8", "jest": "^24.8.0", "jest-file": "^1.0.0", + "lint-staged": "^9.4.2", "plop": "^2.4.0", "react-intl-po": "^2.2.2", "react-test-renderer": "^16.8.6", @@ -157,6 +159,18 @@ "^lodash-es(.*)$": "lodash/$1" } }, + "husky": { + "hooks": { + "pre-commit": "lint-staged", + "pre-push": "npm run check-types" + } + }, + "lint-staged": { + "*.{ts,tsx}": [ + "tslint --fix", + "git add" + ] + }, "scripts": { "build": "webpack -p", "extract-json-messages": "rimraf build/locale && babel src 'src/**/*.{ts,tsx}' -o build/dashboard.bundle.js", @@ -164,9 +178,8 @@ "extract-messages": "npm run extract-json-messages && npm run extract-pot-messages", "build-messages": "rip po2json 'locale/**/*.po' -m 'build/locale/**/*.json' -o 'locale' -c 'description'", "build-types": "apollo client:codegen --target=typescript types --globalTypesFile=src/types/globalTypes.ts", + "check-types": "tsc --noEmit", "generate-component": "plop --plopfile .plop/plopfile.js", - "lint": "tslint 'src/**/*.{ts,tsx}'", - "lint-fix": "tslint 'src/**/*.{ts,tsx}' --fix", "start": "webpack-dev-server --open -d", "storybook": "start-storybook -p 3000 -c src/storybook/", "build-storybook": "build-storybook -c src/storybook/ -o build/storybook", diff --git a/src/attributes/fixtures.ts b/src/attributes/fixtures.ts index 4c236da3a..92ab3ef0d 100644 --- a/src/attributes/fixtures.ts +++ b/src/attributes/fixtures.ts @@ -1,3 +1,4 @@ +import { ProductDetails_product_productType_variantAttributes } from "@saleor/products/types/ProductDetails"; import { AttributeInputTypeEnum, AttributeValueType @@ -35,7 +36,10 @@ export const attribute: AttributeDetailsFragment = { visibleInStorefront: true }; -export const attributes: AttributeList_attributes_edges_node[] = [ +export const attributes: Array< + AttributeList_attributes_edges_node & + ProductDetails_product_productType_variantAttributes +> = [ { node: { __typename: "Attribute" as "Attribute", diff --git a/src/categories/components/CategoryBackground/CategoryBackground.tsx b/src/categories/components/CategoryBackground/CategoryBackground.tsx index 2c5d9925e..2d43de581 100644 --- a/src/categories/components/CategoryBackground/CategoryBackground.tsx +++ b/src/categories/components/CategoryBackground/CategoryBackground.tsx @@ -60,7 +60,7 @@ const CategoryBackground: React.FC = props => { diff --git a/src/collections/components/CollectionImage/CollectionImage.tsx b/src/collections/components/CollectionImage/CollectionImage.tsx index 1ebd5eb85..8162c60b6 100644 --- a/src/collections/components/CollectionImage/CollectionImage.tsx +++ b/src/collections/components/CollectionImage/CollectionImage.tsx @@ -80,7 +80,7 @@ export const CollectionImage = withStyles(styles)( span": { - padding: "6px" +const useStyles = makeStyles( + (theme: Theme) => ({ + formLabel: { + marginBottom: theme.spacing.unit + }, + radioLabel: { + marginBottom: -theme.spacing.unit * 1.5 + }, + root: { + "& $radioLabel": { + "&:last-of-type": { + marginBottom: 0 + } + }, + padding: 0, + width: "100%" + }, + rootNoLabel: { + marginTop: -theme.spacing.unit * 1.5 } + }), + { + name: "RadioGroupField" } -}); +); -interface RadioGroupFieldChoice { +export interface RadioGroupFieldChoice { value: string; label: React.ReactNode; } @@ -39,16 +50,13 @@ interface RadioGroupFieldProps { hint?: string; label?: string; name?: string; - value?: string; + value: string; onChange: (event: React.ChangeEvent) => void; } -export const RadioGroupField = withStyles(styles, { - name: "RadioGroupField" -})( - ({ +export const RadioGroupField: React.FC = props => { + const { className, - classes, disabled, error, label, @@ -57,42 +65,45 @@ export const RadioGroupField = withStyles(styles, { onChange, name, hint - }: RadioGroupFieldProps & WithStyles) => { - return ( - + {label ? ( + {label} + ) : null} + - {label ? ( - {label} - ) : null} - - {choices.length > 0 ? ( - choices.map(choice => ( - } - label={choice.label} - key={choice.value} - /> - )) - ) : ( - - - - )} - - {hint && {hint}} - - ); - } -); + {choices.length > 0 ? ( + choices.map(choice => ( + } + label={choice.label} + key={choice.value} + /> + )) + ) : ( + + + + )} + + {hint && {hint}} + + ); +}; RadioGroupField.displayName = "RadioGroupField"; export default RadioGroupField; diff --git a/src/components/Timeline/Timeline.tsx b/src/components/Timeline/Timeline.tsx index 0bb77b6b5..1c449c8e8 100644 --- a/src/components/Timeline/Timeline.tsx +++ b/src/components/Timeline/Timeline.tsx @@ -30,6 +30,9 @@ const styles = (theme: Theme) => "& > div": { padding: "0 14px" }, + "& fieldset": { + background: theme.palette.background.paper + }, "& textarea": { "&::placeholder": { opacity: [[1], "!important"] as any diff --git a/src/customers/components/CustomerAddressDialog/CustomerAddressDialog.tsx b/src/customers/components/CustomerAddressDialog/CustomerAddressDialog.tsx index 4d4253657..6ae1cc938 100644 --- a/src/customers/components/CustomerAddressDialog/CustomerAddressDialog.tsx +++ b/src/customers/components/CustomerAddressDialog/CustomerAddressDialog.tsx @@ -88,7 +88,7 @@ const CustomerAddressDialog = withStyles(styles, {})( maxWidth="sm" >
- {({ change, data, errors, submit }) => { + {({ change, data, errors }) => { const handleCountrySelect = createSingleAutocompleteSelectHandler( change, setCountryDisplayName, @@ -128,7 +128,6 @@ const CustomerAddressDialog = withStyles(styles, {})( transitionState={confirmButtonState} color="primary" variant="contained" - onClick={submit} type="submit" > diff --git a/src/customers/components/CustomerAddresses/CustomerAddresses.tsx b/src/customers/components/CustomerAddresses/CustomerAddresses.tsx index 500354122..e85beb683 100644 --- a/src/customers/components/CustomerAddresses/CustomerAddresses.tsx +++ b/src/customers/components/CustomerAddresses/CustomerAddresses.tsx @@ -66,7 +66,7 @@ const CustomerAddresses = withStyles(styles, { name: "CustomerAddresses" })( @@ -84,7 +84,7 @@ const CustomerAddresses = withStyles(styles, { name: "CustomerAddresses" })( diff --git a/src/customers/components/CustomerCreateAddress/CustomerCreateAddress.tsx b/src/customers/components/CustomerCreateAddress/CustomerCreateAddress.tsx index 40012bae8..efc42590a 100644 --- a/src/customers/components/CustomerCreateAddress/CustomerCreateAddress.tsx +++ b/src/customers/components/CustomerCreateAddress/CustomerCreateAddress.tsx @@ -47,7 +47,7 @@ const CustomerCreateAddress = withStyles(styles, { diff --git a/src/customers/components/CustomerCreateDetails/CustomerCreateDetails.tsx b/src/customers/components/CustomerCreateDetails/CustomerCreateDetails.tsx index b64276b28..f5220669f 100644 --- a/src/customers/components/CustomerCreateDetails/CustomerCreateDetails.tsx +++ b/src/customers/components/CustomerCreateDetails/CustomerCreateDetails.tsx @@ -48,7 +48,7 @@ const CustomerCreateDetails = withStyles(styles, { diff --git a/src/customers/components/CustomerCreatePage/CustomerCreatePage.tsx b/src/customers/components/CustomerCreatePage/CustomerCreatePage.tsx index c40bb6929..3b89b0157 100644 --- a/src/customers/components/CustomerCreatePage/CustomerCreatePage.tsx +++ b/src/customers/components/CustomerCreatePage/CustomerCreatePage.tsx @@ -89,7 +89,7 @@ const CustomerCreatePage: React.StatelessComponent = ({ diff --git a/src/customers/components/CustomerOrders/CustomerOrders.tsx b/src/customers/components/CustomerOrders/CustomerOrders.tsx index 1fc5e0d26..61a8cb392 100644 --- a/src/customers/components/CustomerOrders/CustomerOrders.tsx +++ b/src/customers/components/CustomerOrders/CustomerOrders.tsx @@ -51,7 +51,7 @@ const CustomerOrders = withStyles(styles, { name: "CustomerOrders" })( = () => { firstName: formData.customerFirstName, lastName: formData.customerLastName, note: formData.note, - sendPasswordEmail: true + sendPasswordEmail: false } } }); diff --git a/src/customers/views/CustomerDetails.tsx b/src/customers/views/CustomerDetails.tsx index 35bd8e17f..45e82da24 100644 --- a/src/customers/views/CustomerDetails.tsx +++ b/src/customers/views/CustomerDetails.tsx @@ -133,7 +133,7 @@ export const CustomerDetailsView: React.StatelessComponent< onClose={() => navigate(customerUrl(id), true)} onConfirm={() => removeCustomer()} title={intl.formatMessage({ - defaultMessage: "Delete customer", + defaultMessage: "Delete Customer", description: "dialog header" })} variant="delete" diff --git a/src/customers/views/CustomerList/CustomerList.tsx b/src/customers/views/CustomerList/CustomerList.tsx index 68348ee99..3742426c6 100644 --- a/src/customers/views/CustomerList/CustomerList.tsx +++ b/src/customers/views/CustomerList/CustomerList.tsx @@ -216,7 +216,7 @@ export const CustomerList: React.StatelessComponent = ({ } variant="delete" title={intl.formatMessage({ - defaultMessage: "Delete customers", + defaultMessage: "Delete Customers", description: "dialog header" })} > diff --git a/src/discounts/components/DiscountDates/DiscountDates.tsx b/src/discounts/components/DiscountDates/DiscountDates.tsx new file mode 100644 index 000000000..0a8e935f3 --- /dev/null +++ b/src/discounts/components/DiscountDates/DiscountDates.tsx @@ -0,0 +1,119 @@ +import Card from "@material-ui/core/Card"; +import CardContent from "@material-ui/core/CardContent"; +import TextField from "@material-ui/core/TextField"; +import React from "react"; +import { useIntl } from "react-intl"; + +import CardTitle from "@saleor/components/CardTitle"; +import { ControlledCheckbox } from "@saleor/components/ControlledCheckbox"; +import Grid from "@saleor/components/Grid"; +import { commonMessages } from "@saleor/intl"; +import { FormErrors } from "../../../types"; + +interface DiscountDatesProps { + data: { + endDate: string; + endTime: string; + hasEndDate: boolean; + startDate: string; + startTime: string; + }; + defaultCurrency: string; + disabled: boolean; + errors: FormErrors<"endDate" | "startDate">; + onChange: (event: React.ChangeEvent) => void; +} + +const DiscountDates = ({ + data, + disabled, + errors, + onChange +}: DiscountDatesProps) => { + const intl = useIntl(); + + return ( + + + + + + + + + {data.hasEndDate && ( + + + + + )} + + + ); +}; +export default DiscountDates; diff --git a/src/discounts/components/DiscountDates/index.ts b/src/discounts/components/DiscountDates/index.ts new file mode 100644 index 000000000..263111c31 --- /dev/null +++ b/src/discounts/components/DiscountDates/index.ts @@ -0,0 +1,2 @@ +export { default } from "./DiscountDates"; +export * from "./DiscountDates"; diff --git a/src/discounts/components/SaleCreatePage/SaleCreatePage.tsx b/src/discounts/components/SaleCreatePage/SaleCreatePage.tsx index 67577afc6..800ced791 100644 --- a/src/discounts/components/SaleCreatePage/SaleCreatePage.tsx +++ b/src/discounts/components/SaleCreatePage/SaleCreatePage.tsx @@ -11,16 +11,20 @@ import PageHeader from "@saleor/components/PageHeader"; import SaveButtonBar from "@saleor/components/SaveButtonBar"; import { sectionNames } from "@saleor/intl"; import { UserError } from "../../../types"; -import { SaleType } from "../../../types/globalTypes"; +import { SaleType as SaleTypeEnum } from "../../../types/globalTypes"; +import DiscountDates from "../DiscountDates"; import SaleInfo from "../SaleInfo"; -import SalePricing from "../SalePricing"; +import SaleType from "../SaleType"; export interface FormData { + endDate: string; + endTime: string; + hasEndDate: boolean; name: string; startDate: string; - endDate: string; + startTime: string; + type: SaleTypeEnum; value: string; - type: SaleType; } export interface SaleCreatePageProps { @@ -44,9 +48,12 @@ const SaleCreatePage: React.StatelessComponent = ({ const initialForm: FormData = { endDate: "", + endTime: "", + hasEndDate: false, name: "", startDate: "", - type: SaleType.FIXED, + startTime: "", + type: SaleTypeEnum.FIXED, value: "" }; return ( @@ -71,10 +78,12 @@ const SaleCreatePage: React.StatelessComponent = ({ onChange={change} /> - + + diff --git a/src/discounts/components/SaleDetailsPage/SaleDetailsPage.tsx b/src/discounts/components/SaleDetailsPage/SaleDetailsPage.tsx index 83f361676..830e96e57 100644 --- a/src/discounts/components/SaleDetailsPage/SaleDetailsPage.tsx +++ b/src/discounts/components/SaleDetailsPage/SaleDetailsPage.tsx @@ -11,23 +11,28 @@ import PageHeader from "@saleor/components/PageHeader"; import SaveButtonBar from "@saleor/components/SaveButtonBar"; import { Tab, TabContainer } from "@saleor/components/Tab"; import { sectionNames } from "@saleor/intl"; -import { maybe } from "../../../misc"; +import { maybe, splitDateTime } from "../../../misc"; import { ListProps, TabListActions, UserError } from "../../../types"; -import { SaleType } from "../../../types/globalTypes"; +import { SaleType as SaleTypeEnum } from "../../../types/globalTypes"; import { SaleDetails_sale } from "../../types/SaleDetails"; import DiscountCategories from "../DiscountCategories"; import DiscountCollections from "../DiscountCollections"; +import DiscountDates from "../DiscountDates"; import DiscountProducts from "../DiscountProducts"; import SaleInfo from "../SaleInfo"; -import SalePricing from "../SalePricing"; import SaleSummary from "../SaleSummary"; +import SaleType from "../SaleType"; +import SaleValue from "../SaleValue"; export interface FormData { + endDate: string; + endTime: string; + hasEndDate: boolean; name: string; startDate: string; - endDate: string; + startTime: string; + type: SaleTypeEnum; value: string; - type: SaleType; } export enum SaleDetailsPageTab { @@ -106,10 +111,13 @@ const SaleDetailsPage: React.StatelessComponent = ({ const intl = useIntl(); const initialForm: FormData = { - endDate: maybe(() => (sale.endDate ? sale.endDate : ""), ""), + endDate: splitDateTime(maybe(() => sale.endDate, "")).date, + endTime: splitDateTime(maybe(() => sale.endDate, "")).time, + hasEndDate: maybe(() => !!sale.endDate), name: maybe(() => sale.name, ""), - startDate: maybe(() => sale.startDate, ""), - type: maybe(() => sale.type, SaleType.FIXED), + startDate: splitDateTime(maybe(() => sale.startDate, "")).date, + startTime: splitDateTime(maybe(() => sale.startDate, "")).time, + type: maybe(() => sale.type, SaleTypeEnum.FIXED), value: maybe(() => sale.value.toString(), "") }; return ( @@ -129,9 +137,11 @@ const SaleDetailsPage: React.StatelessComponent = ({ onChange={change} /> - + + = ({ toolbar={productListToolbar} /> )} + +
diff --git a/src/discounts/components/SalePricing/SalePricing.tsx b/src/discounts/components/SalePricing/SalePricing.tsx deleted file mode 100644 index ae63b90d3..000000000 --- a/src/discounts/components/SalePricing/SalePricing.tsx +++ /dev/null @@ -1,138 +0,0 @@ -import Card from "@material-ui/core/Card"; -import CardContent from "@material-ui/core/CardContent"; -import { - createStyles, - Theme, - WithStyles, - withStyles -} 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 CardTitle from "@saleor/components/CardTitle"; -import Hr from "@saleor/components/Hr"; -import TextFieldWithChoice from "@saleor/components/TextFieldWithChoice"; -import { commonMessages } from "@saleor/intl"; -import { FormErrors } from "../../../types"; -import { SaleType } from "../../../types/globalTypes"; -import { FormData } from "../SaleDetailsPage"; - -export interface SalePricingProps { - data: FormData; - defaultCurrency: string; - disabled: boolean; - errors: FormErrors<"startDate" | "endDate" | "value">; - onChange: (event: React.ChangeEvent) => void; -} - -const styles = (theme: Theme) => - createStyles({ - root: { - display: "grid", - gridColumnGap: theme.spacing.unit * 2 + "px", - gridTemplateColumns: "1fr 1fr" - }, - subheading: { - gridColumnEnd: "span 2", - marginBottom: theme.spacing.unit * 2 - } - }); - -const SalePricing = withStyles(styles, { - name: "SalePricing" -})( - ({ - classes, - data, - defaultCurrency, - disabled, - errors, - onChange - }: SalePricingProps & WithStyles) => { - const intl = useIntl(); - - return ( - - - - - -
- - - - - - - -
- ); - } -); -SalePricing.displayName = "SalePricing"; -export default SalePricing; diff --git a/src/discounts/components/SalePricing/index.ts b/src/discounts/components/SalePricing/index.ts deleted file mode 100644 index 347c08adc..000000000 --- a/src/discounts/components/SalePricing/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from "./SalePricing"; -export * from "./SalePricing"; diff --git a/src/discounts/components/SaleType/SaleType.tsx b/src/discounts/components/SaleType/SaleType.tsx new file mode 100644 index 000000000..8a977f8b1 --- /dev/null +++ b/src/discounts/components/SaleType/SaleType.tsx @@ -0,0 +1,84 @@ +import Card from "@material-ui/core/Card"; +import CardContent from "@material-ui/core/CardContent"; +import { Theme } from "@material-ui/core/styles"; +import { makeStyles } from "@material-ui/styles"; +import React from "react"; +import { IntlShape, useIntl } from "react-intl"; + +import CardTitle from "@saleor/components/CardTitle"; +import RadioGroupField, { + RadioGroupFieldChoice +} from "@saleor/components/RadioGroupField"; +import { FormChange } from "@saleor/hooks/useForm"; +import { SaleType as SaleTypeEnum } from "@saleor/types/globalTypes"; +import { FormData } from "../SaleDetailsPage"; + +export interface SaleTypeProps { + data: FormData; + disabled: boolean; + onChange: FormChange; +} + +const useStyles = makeStyles( + (theme: Theme) => ({ + root: { + "&&": { + paddingBottom: theme.spacing.unit * 1.5 + } + } + }), + { + name: "SaleType" + } +); + +function createChoices(intl: IntlShape): RadioGroupFieldChoice[] { + return [ + { + label: intl.formatMessage({ + defaultMessage: "Percentage", + description: "discount type" + }), + value: SaleTypeEnum.PERCENTAGE + }, + { + label: intl.formatMessage({ + defaultMessage: "Fixed Amount", + description: "discount type" + }), + value: SaleTypeEnum.FIXED + } + ]; +} + +const SaleType: React.FC = props => { + const { data, disabled, onChange } = props; + + const classes = useStyles(props); + const intl = useIntl(); + + const choices = createChoices(intl); + + return ( + + + + + + + ); +}; + +SaleType.displayName = "SaleType"; +export default SaleType; diff --git a/src/discounts/components/SaleType/index.ts b/src/discounts/components/SaleType/index.ts new file mode 100644 index 000000000..ea6da6728 --- /dev/null +++ b/src/discounts/components/SaleType/index.ts @@ -0,0 +1,2 @@ +export { default } from "./SaleType"; +export * from "./SaleType"; diff --git a/src/discounts/components/SaleValue/SaleValue.tsx b/src/discounts/components/SaleValue/SaleValue.tsx new file mode 100644 index 000000000..ff424547e --- /dev/null +++ b/src/discounts/components/SaleValue/SaleValue.tsx @@ -0,0 +1,61 @@ +import Card from "@material-ui/core/Card"; +import CardContent from "@material-ui/core/CardContent"; +import TextField from "@material-ui/core/TextField"; +import React from "react"; +import { useIntl } from "react-intl"; + +import CardTitle from "@saleor/components/CardTitle"; +import { FormChange } from "@saleor/hooks/useForm"; +import { FormErrors } from "@saleor/types"; +import { SaleType } from "@saleor/types/globalTypes"; +import { FormData } from "../SaleDetailsPage"; + +export interface SaleValueProps { + currencySymbol: string; + data: FormData; + disabled: boolean; + errors: FormErrors<"value">; + onChange: FormChange; +} + +const SaleValue: React.FC = ({ + currencySymbol, + data, + disabled, + errors, + onChange +}) => { + const intl = useIntl(); + + return ( + + + + + + + ); +}; + +SaleValue.displayName = "SaleValue"; +export default SaleValue; diff --git a/src/discounts/components/SaleValue/index.ts b/src/discounts/components/SaleValue/index.ts new file mode 100644 index 000000000..d3aba981f --- /dev/null +++ b/src/discounts/components/SaleValue/index.ts @@ -0,0 +1,2 @@ +export { default } from "./SaleValue"; +export * from "./SaleValue"; diff --git a/src/discounts/components/VoucherDetailsPage/VoucherDetailsPage.tsx b/src/discounts/components/VoucherDetailsPage/VoucherDetailsPage.tsx index 780ba974e..cf783f291 100644 --- a/src/discounts/components/VoucherDetailsPage/VoucherDetailsPage.tsx +++ b/src/discounts/components/VoucherDetailsPage/VoucherDetailsPage.tsx @@ -23,8 +23,8 @@ import { import { VoucherDetails_voucher } from "../../types/VoucherDetails"; import DiscountCategories from "../DiscountCategories"; import DiscountCollections from "../DiscountCollections"; +import DiscountDates from "../DiscountDates"; import DiscountProducts from "../DiscountProducts"; -import VoucherDates from "../VoucherDates"; import VoucherInfo from "../VoucherInfo"; import VoucherLimits from "../VoucherLimits"; import VoucherRequirements from "../VoucherRequirements"; @@ -349,7 +349,7 @@ const VoucherDetailsPage: React.StatelessComponent = ({ onChange={change} /> - - + {[RequirementsPicker.ORDER, RequirementsPicker.ITEM].includes( + data.requirementsPicker + ) && } {data.requirementsPicker === RequirementsPicker.ORDER ? ( { +const useStyles = makeStyles( + (theme: Theme) => ({ + hr: { + margin: `${theme.spacing.unit * 2}px 0` + } + }), + { + name: "VoucherValue" + } +); + +const VoucherValue: React.FC = props => { + const { data, defaultCurrency, disabled, errors, variant, onChange } = props; + + const classes = useStyles(props); const intl = useIntl(); const translatedVoucherTypes = translateVoucherTypes(intl); @@ -81,22 +90,22 @@ const VoucherValue = ({ {variant === "update" && ( <> +
- )} -
+
= ({ variables: { id, input: { - endDate: - formData.endDate === "" - ? null - : formData.endDate, + endDate: formData.hasEndDate + ? joinDateTime( + formData.endDate, + formData.endTime + ) + : null, name: formData.name, - startDate: - formData.startDate === "" - ? null - : formData.startDate, + startDate: joinDateTime( + formData.startDate, + formData.startTime + ), type: discountValueTypeEnum( formData.type ), diff --git a/src/home/components/HomeProductListCard/HomeProductListCard.tsx b/src/home/components/HomeProductListCard/HomeProductListCard.tsx index 6b5964d18..80fb454ce 100644 --- a/src/home/components/HomeProductListCard/HomeProductListCard.tsx +++ b/src/home/components/HomeProductListCard/HomeProductListCard.tsx @@ -57,7 +57,7 @@ export const HomeProductList = withStyles(styles, { name: "HomeProductList" })( = ({ id, params }) => { } variant="delete" title={intl.formatMessage({ - defaultMessage: "Delete menu", + defaultMessage: "Delete Menu", description: "dialog header", id: "menuDetailsDeleteMenuHeader" })} diff --git a/src/navigation/views/MenuList.tsx b/src/navigation/views/MenuList.tsx index 2995e0199..9781d0697 100644 --- a/src/navigation/views/MenuList.tsx +++ b/src/navigation/views/MenuList.tsx @@ -202,7 +202,7 @@ const MenuList: React.FC = ({ params }) => { } variant="delete" title={intl.formatMessage({ - defaultMessage: "Delete menu", + defaultMessage: "Delete Menu", description: "dialog header", id: "menuListDeleteMenuHeader" })} @@ -239,7 +239,7 @@ const MenuList: React.FC = ({ params }) => { } variant="delete" title={intl.formatMessage({ - defaultMessage: "Delete menus", + defaultMessage: "Delete Menus", description: "dialog header", id: "menuListDeleteMenusHeader" })} diff --git a/src/orders/components/OrderAddressEditDialog/OrderAddressEditDialog.tsx b/src/orders/components/OrderAddressEditDialog/OrderAddressEditDialog.tsx index e6468a9bf..b79f0777f 100644 --- a/src/orders/components/OrderAddressEditDialog/OrderAddressEditDialog.tsx +++ b/src/orders/components/OrderAddressEditDialog/OrderAddressEditDialog.tsx @@ -83,11 +83,11 @@ const OrderAddressEditDialog = withStyles(styles, { {variant === "billing" ? intl.formatMessage({ - defaultMessage: "Edit billing address", + defaultMessage: "Edit Billing Address", description: "dialog header" }) : intl.formatMessage({ - defaultMessage: "Edit shipping address", + defaultMessage: "Edit Shipping Address", description: "dialog header" })} diff --git a/src/orders/components/OrderCancelDialog/OrderCancelDialog.tsx b/src/orders/components/OrderCancelDialog/OrderCancelDialog.tsx index 0bc4e8e13..0546b5528 100644 --- a/src/orders/components/OrderCancelDialog/OrderCancelDialog.tsx +++ b/src/orders/components/OrderCancelDialog/OrderCancelDialog.tsx @@ -67,7 +67,7 @@ const OrderCancelDialog = withStyles(styles, { name: "OrderCancelDialog" })( <> diff --git a/src/orders/components/OrderCustomer/OrderCustomer.tsx b/src/orders/components/OrderCustomer/OrderCustomer.tsx index 3f800300c..55fadd593 100644 --- a/src/orders/components/OrderCustomer/OrderCustomer.tsx +++ b/src/orders/components/OrderCustomer/OrderCustomer.tsx @@ -187,7 +187,7 @@ const OrderCustomer = withStyles(styles, { name: "OrderCustomer" })(
diff --git a/src/orders/components/OrderCustomerEditDialog/OrderCustomerEditDialog.tsx b/src/orders/components/OrderCustomerEditDialog/OrderCustomerEditDialog.tsx index a629fc285..891e901c7 100644 --- a/src/orders/components/OrderCustomerEditDialog/OrderCustomerEditDialog.tsx +++ b/src/orders/components/OrderCustomerEditDialog/OrderCustomerEditDialog.tsx @@ -79,7 +79,7 @@ const OrderCustomerEditDialog = withStyles(styles, { diff --git a/src/orders/components/OrderDraftCancelDialog/OrderDraftCancelDialog.tsx b/src/orders/components/OrderDraftCancelDialog/OrderDraftCancelDialog.tsx index 0baeb130c..a4cbe1e7d 100644 --- a/src/orders/components/OrderDraftCancelDialog/OrderDraftCancelDialog.tsx +++ b/src/orders/components/OrderDraftCancelDialog/OrderDraftCancelDialog.tsx @@ -25,7 +25,7 @@ const OrderDraftCancelDialog: React.StatelessComponent< onConfirm={onConfirm} open={open} title={intl.formatMessage({ - defaultMessage: "Delete draft order", + defaultMessage: "Delete Daft Order", description: "dialog header" })} variant="delete" diff --git a/src/orders/components/OrderDraftFinalizeDialog/OrderDraftFinalizeDialog.tsx b/src/orders/components/OrderDraftFinalizeDialog/OrderDraftFinalizeDialog.tsx index a6138c3aa..91ae73a5f 100644 --- a/src/orders/components/OrderDraftFinalizeDialog/OrderDraftFinalizeDialog.tsx +++ b/src/orders/components/OrderDraftFinalizeDialog/OrderDraftFinalizeDialog.tsx @@ -64,7 +64,7 @@ const OrderDraftFinalizeDialog: React.StatelessComponent< onConfirm={onConfirm} open={open} title={intl.formatMessage({ - defaultMessage: "Finalize draft order", + defaultMessage: "Finalize Draft Order", description: "dialog header" })} confirmButtonLabel={ diff --git a/src/orders/components/OrderFulfillmentDialog/OrderFulfillmentDialog.tsx b/src/orders/components/OrderFulfillmentDialog/OrderFulfillmentDialog.tsx index 6c7bfcee6..12541335f 100644 --- a/src/orders/components/OrderFulfillmentDialog/OrderFulfillmentDialog.tsx +++ b/src/orders/components/OrderFulfillmentDialog/OrderFulfillmentDialog.tsx @@ -120,7 +120,7 @@ const OrderFulfillmentDialog = withStyles(styles, { <> diff --git a/src/orders/components/OrderFulfillmentTrackingDialog/OrderFulfillmentTrackingDialog.tsx b/src/orders/components/OrderFulfillmentTrackingDialog/OrderFulfillmentTrackingDialog.tsx index 4de13a599..0bfa43ab8 100644 --- a/src/orders/components/OrderFulfillmentTrackingDialog/OrderFulfillmentTrackingDialog.tsx +++ b/src/orders/components/OrderFulfillmentTrackingDialog/OrderFulfillmentTrackingDialog.tsx @@ -37,7 +37,7 @@ const OrderFulfillmentTrackingDialog: React.StatelessComponent< <> diff --git a/src/pages/views/PageCreate.tsx b/src/pages/views/PageCreate.tsx index d264b709e..ff431ff8b 100644 --- a/src/pages/views/PageCreate.tsx +++ b/src/pages/views/PageCreate.tsx @@ -43,7 +43,7 @@ export const PageCreate: React.StatelessComponent = () => { <> diff --git a/src/products/components/ProductUpdatePage/ProductUpdatePage.tsx b/src/products/components/ProductUpdatePage/ProductUpdatePage.tsx index ac57ec9fc..f341f5875 100644 --- a/src/products/components/ProductUpdatePage/ProductUpdatePage.tsx +++ b/src/products/components/ProductUpdatePage/ProductUpdatePage.tsx @@ -60,6 +60,7 @@ export interface ProductUpdatePageProps extends ListActions { saveButtonBarState: ConfirmButtonTransitionState; fetchCategories: (query: string) => void; fetchCollections: (query: string) => void; + onVariantsAdd: () => void; onVariantShow: (id: string) => () => void; onImageDelete: (id: string) => () => void; onBack?(); @@ -100,6 +101,7 @@ export const ProductUpdatePage: React.FC = ({ onSeoClick, onSubmit, onVariantAdd, + onVariantsAdd, onVariantShow, isChecked, selected, @@ -236,6 +238,7 @@ export const ProductUpdatePage: React.FC = ({ fallbackPrice={product ? product.basePrice : undefined} onRowClick={onVariantShow} onVariantAdd={onVariantAdd} + onVariantsAdd={onVariantsAdd} toolbar={toolbar} isChecked={isChecked} selected={selected} diff --git a/src/products/components/ProductVariantCreateDialog/ProductVariantCreate.stories.tsx b/src/products/components/ProductVariantCreateDialog/ProductVariantCreate.stories.tsx new file mode 100644 index 000000000..2caf5fba2 --- /dev/null +++ b/src/products/components/ProductVariantCreateDialog/ProductVariantCreate.stories.tsx @@ -0,0 +1,124 @@ +import Card from "@material-ui/core/Card"; +import CardContent from "@material-ui/core/CardContent"; +import { storiesOf } from "@storybook/react"; +import React from "react"; + +import { attributes } from "@saleor/attributes/fixtures"; +import { ProductVariantBulkCreate_productVariantBulkCreate_bulkProductErrors } from "@saleor/products/types/ProductVariantBulkCreate"; +import { ProductErrorCode } from "@saleor/types/globalTypes"; +import Decorator from "../../../storybook/Decorator"; +import { createVariants } from "./createVariants"; +import { AllOrAttribute } from "./form"; +import ProductVariantCreateContent, { + ProductVariantCreateContentProps +} from "./ProductVariantCreateContent"; +import ProductVariantCreateDialog from "./ProductVariantCreateDialog"; + +const selectedAttributes = [1, 4, 5].map(index => attributes[index]); + +const price: AllOrAttribute = { + all: false, + attribute: selectedAttributes[1].id, + value: "2.79", + values: selectedAttributes[1].values.map((attribute, attributeIndex) => ({ + slug: attribute.slug, + value: (attributeIndex + 4).toFixed(2) + })) +}; + +const stock: AllOrAttribute = { + all: false, + attribute: selectedAttributes[1].id, + value: "8", + values: selectedAttributes[1].values.map((attribute, attributeIndex) => ({ + slug: attribute.slug, + value: (selectedAttributes.length * 10 - attributeIndex).toString() + })) +}; + +const dataAttributes = selectedAttributes.map(attribute => ({ + id: attribute.id, + values: attribute.values + .map(value => value.slug) + .filter((_, valueIndex) => valueIndex % 2 !== 1) +})); + +const errors: ProductVariantBulkCreate_productVariantBulkCreate_bulkProductErrors[] = [ + { + __typename: "BulkProductError", + code: ProductErrorCode.UNIQUE, + field: "sku", + index: 3, + message: "Duplicated SKU." + } +]; + +const props: ProductVariantCreateContentProps = { + attributes, + currencySymbol: "USD", + data: { + attributes: dataAttributes, + price, + stock, + variants: createVariants({ + attributes: dataAttributes, + price, + stock, + variants: [] + }) + }, + dispatchFormDataAction: () => undefined, + errors: [], + onStepClick: () => undefined, + step: "values" +}; + +storiesOf("Views / Products / Create multiple variants", module) + .addDecorator(storyFn => ( + + {storyFn()} + + )) + .addDecorator(Decorator) + .add("choose values", () => ) + .add("prices and SKU", () => ( + + )); + +storiesOf("Views / Products / Create multiple variants / summary", module) + .addDecorator(storyFn => ( + + {storyFn()} + + )) + .addDecorator(Decorator) + .add("default", () => ( + + )) + .add("errors", () => ( + + )); + +storiesOf("Views / Products / Create multiple variants", module) + .addDecorator(Decorator) + .add("interactive", () => ( + undefined} + onSubmit={() => undefined} + /> + )); diff --git a/src/products/components/ProductVariantCreateDialog/ProductVariantCreateContent.tsx b/src/products/components/ProductVariantCreateDialog/ProductVariantCreateContent.tsx new file mode 100644 index 000000000..cbc32664f --- /dev/null +++ b/src/products/components/ProductVariantCreateDialog/ProductVariantCreateContent.tsx @@ -0,0 +1,147 @@ +import { Theme } from "@material-ui/core/styles"; +import { makeStyles } from "@material-ui/styles"; +import React from "react"; + +import { ProductDetails_product_productType_variantAttributes } from "@saleor/products/types/ProductDetails"; +import { ProductVariantBulkCreate_productVariantBulkCreate_bulkProductErrors } from "@saleor/products/types/ProductVariantBulkCreate"; +import { isSelected } from "@saleor/utils/lists"; +import { ProductVariantCreateFormData } from "./form"; +import ProductVariantCreatePrices from "./ProductVariantCreatePrices"; +import ProductVariantCreateSummary from "./ProductVariantCreateSummary"; +import ProductVariantCreateTabs from "./ProductVariantCreateTabs"; +import ProductVariantCreateValues from "./ProductVariantCreateValues"; +import { ProductVariantCreateReducerAction } from "./reducer"; +import { ProductVariantCreateStep } from "./types"; + +const useStyles = makeStyles((theme: Theme) => ({ + root: { + maxHeight: 400, + overflowX: "hidden", + overflowY: "scroll", + paddingLeft: theme.spacing.unit * 3, + paddingRight: theme.spacing.unit * 2, + position: "relative", + right: theme.spacing.unit * 3, + width: `calc(100% + ${theme.spacing.unit * 3}px)` + } +})); + +export interface ProductVariantCreateContentProps { + attributes: ProductDetails_product_productType_variantAttributes[]; + currencySymbol: string; + data: ProductVariantCreateFormData; + dispatchFormDataAction: React.Dispatch; + errors: ProductVariantBulkCreate_productVariantBulkCreate_bulkProductErrors[]; + step: ProductVariantCreateStep; + onStepClick: (step: ProductVariantCreateStep) => void; +} + +const ProductVariantCreateContent: React.FC< + ProductVariantCreateContentProps +> = props => { + const { + attributes, + currencySymbol, + data, + dispatchFormDataAction, + errors, + step, + onStepClick + } = props; + const classes = useStyles(props); + + const selectedAttributes = attributes.filter(attribute => + isSelected( + attribute.id, + data.attributes.map(dataAttribute => dataAttribute.id), + (a, b) => a === b + ) + ); + + return ( +
+ +
+ {step === "values" && ( + + dispatchFormDataAction({ + attributeId, + type: "selectValue", + valueId + }) + } + /> + )} + {step === "prices" && ( + + dispatchFormDataAction({ + all, + type: type === "price" ? "applyPriceToAll" : "applyStockToAll" + }) + } + onApplyToAllChange={(value, type) => + dispatchFormDataAction({ + type: + type === "price" + ? "changeApplyPriceToAllValue" + : "changeApplyStockToAllValue", + value + }) + } + onAttributeSelect={(attributeId, type) => + dispatchFormDataAction({ + attributeId, + type: + type === "price" + ? "changeApplyPriceToAttributeId" + : "changeApplyStockToAttributeId" + }) + } + onAttributeValueChange={(valueId, value, type) => + dispatchFormDataAction({ + type: + type === "price" + ? "changeAttributeValuePrice" + : "changeAttributeValueStock", + value, + valueId + }) + } + /> + )} + {step === "summary" && ( + + dispatchFormDataAction({ + field, + type: "changeVariantData", + value, + variantIndex + }) + } + onVariantDelete={variantIndex => + dispatchFormDataAction({ + type: "deleteVariant", + variantIndex + }) + } + /> + )} +
+
+ ); +}; + +ProductVariantCreateContent.displayName = "ProductVariantCreateContent"; +export default ProductVariantCreateContent; diff --git a/src/products/components/ProductVariantCreateDialog/ProductVariantCreateDialog.tsx b/src/products/components/ProductVariantCreateDialog/ProductVariantCreateDialog.tsx new file mode 100644 index 000000000..c898e9157 --- /dev/null +++ b/src/products/components/ProductVariantCreateDialog/ProductVariantCreateDialog.tsx @@ -0,0 +1,214 @@ +import Button from "@material-ui/core/Button"; +import Dialog from "@material-ui/core/Dialog"; +import DialogActions from "@material-ui/core/DialogActions"; +import DialogContent from "@material-ui/core/DialogContent"; +import DialogTitle from "@material-ui/core/DialogTitle"; +import { Theme } from "@material-ui/core/styles"; +import { makeStyles } from "@material-ui/styles"; +import React from "react"; +import { FormattedMessage } from "react-intl"; + +import useModalDialogErrors from "@saleor/hooks/useModalDialogErrors"; +import useModalDialogOpen from "@saleor/hooks/useModalDialogOpen"; +import { ProductVariantBulkCreateInput } from "../../../types/globalTypes"; +import { createInitialForm, ProductVariantCreateFormData } from "./form"; +import ProductVariantCreateContent, { + ProductVariantCreateContentProps +} from "./ProductVariantCreateContent"; +import reduceProductVariantCreateFormData from "./reducer"; +import { ProductVariantCreateStep } from "./types"; + +const useStyles = makeStyles((theme: Theme) => ({ + button: { + marginLeft: theme.spacing.unit * 2 + }, + content: { + overflowX: "visible", + overflowY: "hidden", + width: 800 + }, + spacer: { + flex: 1 + } +})); + +function canHitNext( + step: ProductVariantCreateStep, + data: ProductVariantCreateFormData +): boolean { + switch (step) { + case "values": + return data.attributes.every(attribute => attribute.values.length > 0); + case "prices": + if (data.price.all) { + if (data.price.value === "") { + return false; + } + } else { + if ( + data.price.attribute === "" || + data.price.values.some(attributeValue => attributeValue.value === "") + ) { + return false; + } + } + + if (data.stock.all) { + if (data.stock.value === "") { + return false; + } + } else { + if ( + data.stock.attribute === "" || + data.stock.values.some(attributeValue => attributeValue.value === "") + ) { + return false; + } + } + + return true; + case "summary": + return data.variants.every(variant => variant.sku !== ""); + + default: + return false; + } +} + +export interface ProductVariantCreateDialogProps + extends Omit< + ProductVariantCreateContentProps, + "data" | "dispatchFormDataAction" | "step" | "onStepClick" + > { + defaultPrice: string; + open: boolean; + onClose: () => void; + onSubmit: (data: ProductVariantBulkCreateInput[]) => void; +} + +const ProductVariantCreateDialog: React.FC< + ProductVariantCreateDialogProps +> = props => { + const { + attributes, + defaultPrice, + errors: apiErrors, + open, + onClose, + onSubmit, + ...contentProps + } = props; + const classes = useStyles(props); + const [step, setStep] = React.useState("values"); + + function handleNextStep() { + switch (step) { + case "values": + setStep("prices"); + break; + case "prices": + setStep("summary"); + break; + } + } + + function handlePrevStep() { + switch (step) { + case "prices": + setStep("values"); + break; + case "summary": + setStep("prices"); + break; + } + } + + const [data, dispatchFormDataAction] = React.useReducer( + reduceProductVariantCreateFormData, + createInitialForm(attributes, defaultPrice) + ); + + const reloadForm = () => + dispatchFormDataAction({ + data: createInitialForm(attributes, defaultPrice), + type: "reload" + }); + + React.useEffect(reloadForm, [attributes.length]); + + useModalDialogOpen(open, { + onClose: () => { + reloadForm(); + setStep("values"); + } + }); + + const errors = useModalDialogErrors(apiErrors, open); + + return ( + + + + + + setStep(step)} + /> + + + +
+ {step !== "values" && ( + + )} + {step !== "summary" ? ( + + ) : ( + + )} + +
+ ); +}; + +ProductVariantCreateDialog.displayName = "ProductVariantCreateDialog"; +export default ProductVariantCreateDialog; diff --git a/src/products/components/ProductVariantCreateDialog/ProductVariantCreatePrices.tsx b/src/products/components/ProductVariantCreateDialog/ProductVariantCreatePrices.tsx new file mode 100644 index 000000000..bdfe40469 --- /dev/null +++ b/src/products/components/ProductVariantCreateDialog/ProductVariantCreatePrices.tsx @@ -0,0 +1,313 @@ +import FormControlLabel from "@material-ui/core/FormControlLabel"; +import Radio from "@material-ui/core/Radio"; +import RadioGroup from "@material-ui/core/RadioGroup"; +import { Theme } from "@material-ui/core/styles"; +import TextField from "@material-ui/core/TextField"; +import Typography from "@material-ui/core/Typography"; +import { makeStyles } from "@material-ui/styles"; +import React from "react"; +import { FormattedMessage, useIntl } from "react-intl"; + +import FormSpacer from "@saleor/components/FormSpacer"; +import Grid from "@saleor/components/Grid"; +import Hr from "@saleor/components/Hr"; +import SingleSelectField from "@saleor/components/SingleSelectField"; +import { ProductDetails_product_productType_variantAttributes } from "@saleor/products/types/ProductDetails"; +import { ProductVariantCreateFormData } from "./form"; + +const useStyles = makeStyles((theme: Theme) => ({ + hr: { + marginBottom: theme.spacing.unit, + marginTop: theme.spacing.unit / 2 + }, + hrAttribute: { + marginTop: theme.spacing.unit * 2 + }, + label: { + alignSelf: "center" + }, + shortInput: { + width: "50%" + } +})); + +export type PriceOrStock = "price" | "stock"; +export interface ProductVariantCreatePricesProps { + attributes: ProductDetails_product_productType_variantAttributes[]; + currencySymbol: string; + data: ProductVariantCreateFormData; + onApplyPriceOrStockChange: (applyToAll: boolean, type: PriceOrStock) => void; + onApplyToAllChange: (value: string, type: PriceOrStock) => void; + onAttributeSelect: (id: string, type: PriceOrStock) => void; + onAttributeValueChange: ( + id: string, + value: string, + type: PriceOrStock + ) => void; +} + +const ProductVariantCreatePrices: React.FC< + ProductVariantCreatePricesProps +> = props => { + const { + attributes, + currencySymbol, + data, + onApplyPriceOrStockChange, + onApplyToAllChange, + onAttributeSelect, + onAttributeValueChange + } = props; + const classes = useStyles(props); + const intl = useIntl(); + + const attributeChoices = attributes.map(attribute => ({ + label: attribute.name, + value: attribute.id + })); + const priceAttributeValues = data.price.all + ? null + : data.price.attribute + ? attributes + .find(attribute => attribute.id === data.price.attribute) + .values.filter(value => + data.attributes + .find(attribute => attribute.id === data.price.attribute) + .values.includes(value.slug) + ) + : []; + const stockAttributeValues = data.stock.all + ? null + : data.stock.attribute + ? attributes + .find(attribute => attribute.id === data.stock.attribute) + .values.filter(value => + data.attributes + .find(attribute => attribute.id === data.stock.attribute) + .values.includes(value.slug) + ) + : []; + + return ( + <> + + + +
+ + } + label={intl.formatMessage({ + defaultMessage: "Apply single price to all SKUs" + })} + onChange={() => onApplyPriceOrStockChange(true, "price")} + /> + + onApplyToAllChange(event.target.value, "price")} + /> + + } + label={intl.formatMessage({ + defaultMessage: "Apply unique prices by attribute to each SKU" + })} + onChange={() => onApplyPriceOrStockChange(false, "price")} + /> + + {!data.price.all && ( + <> + + +
+ + + +
+
+ + onAttributeSelect(event.target.value, "price") + } + /> +
+
+
+ {priceAttributeValues && + priceAttributeValues.map(attributeValue => ( + + + +
+ {attributeValue.name} +
+
+ value.slug === attributeValue.slug + ).value + } + onChange={event => + onAttributeValueChange( + attributeValue.slug, + event.target.value, + "price" + ) + } + /> +
+
+
+ ))} + + )} + + + + +
+ + } + label={intl.formatMessage({ + defaultMessage: "Apply single stock to all SKUs" + })} + onChange={() => onApplyPriceOrStockChange(true, "stock")} + /> + + onApplyToAllChange(event.target.value, "stock")} + /> + + } + label={intl.formatMessage({ + defaultMessage: "Apply unique stock by attribute to each SKU" + })} + onChange={() => onApplyPriceOrStockChange(false, "stock")} + /> + + {!data.stock.all && ( + <> + + +
+ + + +
+
+ + onAttributeSelect(event.target.value, "stock") + } + /> +
+
+
+ {stockAttributeValues && + stockAttributeValues.map(attributeValue => ( + + + +
+ {attributeValue.name} +
+
+ value.slug === attributeValue.slug + ).value + } + onChange={event => + onAttributeValueChange( + attributeValue.slug, + event.target.value, + "stock" + ) + } + /> +
+
+
+ ))} + + )} + + ); +}; + +ProductVariantCreatePrices.displayName = "ProductVariantCreatePrices"; +export default ProductVariantCreatePrices; diff --git a/src/products/components/ProductVariantCreateDialog/ProductVariantCreateSummary.tsx b/src/products/components/ProductVariantCreateDialog/ProductVariantCreateSummary.tsx new file mode 100644 index 000000000..ff87cda05 --- /dev/null +++ b/src/products/components/ProductVariantCreateDialog/ProductVariantCreateSummary.tsx @@ -0,0 +1,301 @@ +import blue from "@material-ui/core/colors/blue"; +import cyan from "@material-ui/core/colors/cyan"; +import green from "@material-ui/core/colors/green"; +import purple from "@material-ui/core/colors/purple"; +import yellow from "@material-ui/core/colors/yellow"; +import IconButton from "@material-ui/core/IconButton"; +import { Theme } from "@material-ui/core/styles"; +import TextField from "@material-ui/core/TextField"; +import Typography from "@material-ui/core/Typography"; +import DeleteIcon from "@material-ui/icons/Delete"; +import { makeStyles } from "@material-ui/styles"; +import classNames from "classnames"; +import React from "react"; +import { FormattedMessage } from "react-intl"; + +import Hr from "@saleor/components/Hr"; +import { maybe } from "@saleor/misc"; +import { ProductVariantBulkCreate_productVariantBulkCreate_bulkProductErrors } from "@saleor/products/types/ProductVariantBulkCreate"; +import { ProductVariantBulkCreateInput } from "@saleor/types/globalTypes"; +import { ProductDetails_product_productType_variantAttributes } from "../../types/ProductDetails"; +import { ProductVariantCreateFormData } from "./form"; +import { VariantField } from "./reducer"; + +export interface ProductVariantCreateSummaryProps { + attributes: ProductDetails_product_productType_variantAttributes[]; + currencySymbol: string; + data: ProductVariantCreateFormData; + errors: ProductVariantBulkCreate_productVariantBulkCreate_bulkProductErrors[]; + onVariantDataChange: ( + variantIndex: number, + field: VariantField, + value: string + ) => void; + onVariantDelete: (variantIndex: number) => void; +} + +const colors = [blue, cyan, green, purple, yellow].map(color => color[800]); + +const useStyles = makeStyles( + (theme: Theme) => ({ + attributeValue: { + display: "inline-block", + marginRight: theme.spacing.unit + }, + col: { + ...theme.typography.body2, + fontSize: 14, + paddingLeft: theme.spacing.unit, + paddingRight: theme.spacing.unit + }, + colHeader: { + ...theme.typography.body2, + fontSize: 14 + }, + colName: { + "&&": { + paddingLeft: "0 !important" + }, + "&:not($colHeader)": { + paddingTop: theme.spacing.unit * 2 + } + }, + colPrice: {}, + colSku: {}, + colStock: {}, + delete: { + marginTop: theme.spacing.unit / 2 + }, + errorRow: {}, + hr: { + marginBottom: theme.spacing.unit, + marginTop: theme.spacing.unit / 2 + }, + input: { + "& input": { + padding: "16px 12px 17px" + }, + marginTop: theme.spacing.unit / 2 + }, + row: { + borderBottom: `1px solid ${theme.palette.divider}`, + display: "grid", + gridTemplateColumns: "1fr 180px 120px 180px 64px", + padding: `${theme.spacing.unit}px 0` + } + }), + { + name: "ProductVariantCreateSummary" + } +); + +function getVariantName( + variant: ProductVariantBulkCreateInput, + attributes: ProductDetails_product_productType_variantAttributes[] +): string[] { + return attributes.reduce( + (acc, attribute) => [ + ...acc, + attribute.values.find( + value => + value.slug === + variant.attributes.find( + variantAttribute => variantAttribute.id === attribute.id + ).values[0] + ).name + ], + [] + ); +} + +const ProductVariantCreateSummary: React.FC< + ProductVariantCreateSummaryProps +> = props => { + const { + attributes, + currencySymbol, + data, + errors, + onVariantDataChange, + onVariantDelete + } = props; + const classes = useStyles(props); + + return ( + <> + + + +
+
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ {data.variants.map((variant, variantIndex) => { + const variantErrors = errors.filter( + error => error.index === variantIndex + ); + + return ( +
0 + })} + key={variant.attributes + .map(attribute => attribute.values[0]) + .join(":")} + > +
+ {getVariantName(variant, attributes).map( + (value, valueIndex) => ( + + {value} + + ) + )} +
+
+ error.field === "priceOverride" + ) + } + helperText={maybe( + () => + variantErrors.find( + error => error.field === "priceOverride" + ).message + )} + inputProps={{ + min: 0, + type: "number" + }} + fullWidth + value={variant.priceOverride} + onChange={event => + onVariantDataChange( + variantIndex, + "price", + event.target.value + ) + } + /> +
+
+ error.field === "quantity") + } + helperText={maybe( + () => + variantErrors.find(error => error.field === "quantity") + .message + )} + inputProps={{ + min: 0, + type: "number" + }} + fullWidth + value={variant.quantity} + onChange={event => + onVariantDataChange( + variantIndex, + "stock", + event.target.value + ) + } + /> +
+
+ error.field === "sku")} + helperText={maybe( + () => + variantErrors.find(error => error.field === "sku").message + )} + fullWidth + value={variant.sku} + onChange={event => + onVariantDataChange(variantIndex, "sku", event.target.value) + } + /> +
+
+ onVariantDelete(variantIndex)} + > + + +
+
+ ); + })} +
+ + ); +}; + +ProductVariantCreateSummary.displayName = "ProductVariantCreateSummary"; +export default ProductVariantCreateSummary; diff --git a/src/products/components/ProductVariantCreateDialog/ProductVariantCreateTabs.tsx b/src/products/components/ProductVariantCreateDialog/ProductVariantCreateTabs.tsx new file mode 100644 index 000000000..3d4667f23 --- /dev/null +++ b/src/products/components/ProductVariantCreateDialog/ProductVariantCreateTabs.tsx @@ -0,0 +1,109 @@ +import { Theme } from "@material-ui/core/styles"; +import Typography from "@material-ui/core/Typography"; +import { makeStyles } from "@material-ui/styles"; +import classNames from "classnames"; +import React from "react"; +import { IntlShape, useIntl } from "react-intl"; + +import { ProductVariantCreateStep } from "./types"; + +interface Step { + label: string; + value: ProductVariantCreateStep; +} +function getSteps(intl: IntlShape): Step[] { + return [ + { + label: intl.formatMessage({ + defaultMessage: "Select Values", + description: "attribute values, variant creation step" + }), + value: "values" + }, + { + label: intl.formatMessage({ + defaultMessage: "Prices and SKU", + description: "variant creation step" + }), + value: "prices" + }, + { + label: intl.formatMessage({ + defaultMessage: "Summary", + description: "variant creation step" + }), + value: "summary" + } + ]; +} + +const useStyles = makeStyles( + (theme: Theme) => ({ + label: { + fontSize: 14, + textAlign: "center" + }, + root: { + borderBottom: `1px solid ${theme.palette.divider}`, + display: "flex", + justifyContent: "space-between", + marginBottom: theme.spacing.unit * 3 + }, + tab: { + flex: 1, + paddingBottom: theme.spacing.unit, + userSelect: "none" + }, + tabActive: { + fontWeight: 600 + }, + tabVisited: { + borderBottom: `3px solid ${theme.palette.primary.main}`, + cursor: "pointer" + } + }), + { + name: "ProductVariantCreateTabs" + } +); + +export interface ProductVariantCreateTabsProps { + step: ProductVariantCreateStep; + onStepClick: (step: ProductVariantCreateStep) => void; +} + +const ProductVariantCreateTabs: React.FC< + ProductVariantCreateTabsProps +> = props => { + const { step: currentStep, onStepClick } = props; + const classes = useStyles(props); + const intl = useIntl(); + const steps = getSteps(intl); + + return ( +
+ {steps.map((step, stepIndex) => { + const visitedStep = + steps.findIndex(step => step.value === currentStep) >= stepIndex; + + return ( +
onStepClick(step.value) : undefined} + key={step.value} + > + + {step.label} + +
+ ); + })} +
+ ); +}; + +ProductVariantCreateTabs.displayName = "ProductVariantCreateTabs"; +export default ProductVariantCreateTabs; diff --git a/src/products/components/ProductVariantCreateDialog/ProductVariantCreateValues.tsx b/src/products/components/ProductVariantCreateDialog/ProductVariantCreateValues.tsx new file mode 100644 index 000000000..dd6fe6dc3 --- /dev/null +++ b/src/products/components/ProductVariantCreateDialog/ProductVariantCreateValues.tsx @@ -0,0 +1,79 @@ +import { Theme } from "@material-ui/core/styles"; +import Typography from "@material-ui/core/Typography"; +import makeStyles from "@material-ui/styles/makeStyles"; +import React from "react"; + +import ControlledCheckbox from "@saleor/components/ControlledCheckbox"; +import Debounce from "@saleor/components/Debounce"; +import Hr from "@saleor/components/Hr"; +import Skeleton from "@saleor/components/Skeleton"; +import { maybe } from "@saleor/misc"; +import { ProductDetails_product_productType_variantAttributes } from "@saleor/products/types/ProductDetails"; +import { isSelected } from "@saleor/utils/lists"; +import { ProductVariantCreateFormData } from "./form"; + +export interface ProductVariantCreateValuesProps { + attributes: ProductDetails_product_productType_variantAttributes[]; + data: ProductVariantCreateFormData; + onValueClick: (attributeId: string, valueId: string) => void; +} + +const useStyles = makeStyles((theme: Theme) => ({ + hr: { + marginBottom: theme.spacing.unit, + marginTop: theme.spacing.unit / 2 + }, + valueContainer: { + display: "grid", + gridColumnGap: theme.spacing.unit * 3 + "px", + gridTemplateColumns: "repeat(3, 1fr)", + marginBottom: theme.spacing.unit * 3 + } +})); + +const ProductVariantCreateValues: React.FC< + ProductVariantCreateValuesProps +> = props => { + const { attributes, data, onValueClick } = props; + const classes = useStyles(props); + + return ( + <> + {attributes.map(attribute => ( + + + {maybe(() => attribute.name, )} + +
+
+ {attribute.values.map(value => ( + onValueClick(attribute.id, value.slug)} + time={100} + key={value.slug} + > + {change => ( + attribute.id === dataAttribute.id + ).values, + (a, b) => a === b + )} + name={`value:${value.slug}`} + label={value.name} + onChange={change} + /> + )} + + ))} +
+
+ ))} + + ); +}; + +ProductVariantCreateValues.displayName = "ProductVariantCreateValues"; +export default ProductVariantCreateValues; diff --git a/src/products/components/ProductVariantCreateDialog/__snapshots__/reducer.test.ts.snap b/src/products/components/ProductVariantCreateDialog/__snapshots__/reducer.test.ts.snap new file mode 100644 index 000000000..22a84acfc --- /dev/null +++ b/src/products/components/ProductVariantCreateDialog/__snapshots__/reducer.test.ts.snap @@ -0,0 +1,1028 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Reducer is able to select attribute values 1`] = ` +Object { + "attributes": Array [ + Object { + "id": "attr-1", + "values": Array [ + "val-1-1", + "val-1-7", + ], + }, + Object { + "id": "attr-2", + "values": Array [ + "val-2-2", + "val-2-4", + ], + }, + Object { + "id": "attr-4", + "values": Array [ + "val-4-1", + "val-4-5", + ], + }, + ], + "price": Object { + "all": true, + "attribute": undefined, + "value": "10.99", + "values": Array [], + }, + "stock": Object { + "all": true, + "attribute": undefined, + "value": "", + "values": Array [], + }, + "variants": Array [], +} +`; + +exports[`Reducer is able to select price for all variants 1`] = ` +Object { + "attributes": Array [ + Object { + "id": "attr-1", + "values": Array [ + "val-1-1", + "val-1-7", + ], + }, + Object { + "id": "attr-2", + "values": Array [ + "val-2-2", + "val-2-4", + ], + }, + Object { + "id": "attr-4", + "values": Array [ + "val-4-1", + "val-4-5", + ], + }, + ], + "price": Object { + "all": true, + "attribute": undefined, + "value": "45.99", + "values": Array [], + }, + "stock": Object { + "all": true, + "attribute": undefined, + "value": "", + "values": Array [], + }, + "variants": Array [ + Object { + "attributes": Array [ + Object { + "id": "attr-1", + "values": Array [ + "val-1-1", + ], + }, + Object { + "id": "attr-2", + "values": Array [ + "val-2-2", + ], + }, + Object { + "id": "attr-4", + "values": Array [ + "val-4-1", + ], + }, + ], + "priceOverride": "45.99", + "quantity": NaN, + "sku": "", + }, + Object { + "attributes": Array [ + Object { + "id": "attr-1", + "values": Array [ + "val-1-1", + ], + }, + Object { + "id": "attr-2", + "values": Array [ + "val-2-2", + ], + }, + Object { + "id": "attr-4", + "values": Array [ + "val-4-5", + ], + }, + ], + "priceOverride": "45.99", + "quantity": NaN, + "sku": "", + }, + Object { + "attributes": Array [ + Object { + "id": "attr-1", + "values": Array [ + "val-1-1", + ], + }, + Object { + "id": "attr-2", + "values": Array [ + "val-2-4", + ], + }, + Object { + "id": "attr-4", + "values": Array [ + "val-4-1", + ], + }, + ], + "priceOverride": "45.99", + "quantity": NaN, + "sku": "", + }, + Object { + "attributes": Array [ + Object { + "id": "attr-1", + "values": Array [ + "val-1-1", + ], + }, + Object { + "id": "attr-2", + "values": Array [ + "val-2-4", + ], + }, + Object { + "id": "attr-4", + "values": Array [ + "val-4-5", + ], + }, + ], + "priceOverride": "45.99", + "quantity": NaN, + "sku": "", + }, + Object { + "attributes": Array [ + Object { + "id": "attr-1", + "values": Array [ + "val-1-7", + ], + }, + Object { + "id": "attr-2", + "values": Array [ + "val-2-2", + ], + }, + Object { + "id": "attr-4", + "values": Array [ + "val-4-1", + ], + }, + ], + "priceOverride": "45.99", + "quantity": NaN, + "sku": "", + }, + Object { + "attributes": Array [ + Object { + "id": "attr-1", + "values": Array [ + "val-1-7", + ], + }, + Object { + "id": "attr-2", + "values": Array [ + "val-2-2", + ], + }, + Object { + "id": "attr-4", + "values": Array [ + "val-4-5", + ], + }, + ], + "priceOverride": "45.99", + "quantity": NaN, + "sku": "", + }, + Object { + "attributes": Array [ + Object { + "id": "attr-1", + "values": Array [ + "val-1-7", + ], + }, + Object { + "id": "attr-2", + "values": Array [ + "val-2-4", + ], + }, + Object { + "id": "attr-4", + "values": Array [ + "val-4-1", + ], + }, + ], + "priceOverride": "45.99", + "quantity": NaN, + "sku": "", + }, + Object { + "attributes": Array [ + Object { + "id": "attr-1", + "values": Array [ + "val-1-7", + ], + }, + Object { + "id": "attr-2", + "values": Array [ + "val-2-4", + ], + }, + Object { + "id": "attr-4", + "values": Array [ + "val-4-5", + ], + }, + ], + "priceOverride": "45.99", + "quantity": NaN, + "sku": "", + }, + ], +} +`; + +exports[`Reducer is able to select price to each attribute value 1`] = ` +Object { + "attributes": Array [ + Object { + "id": "attr-1", + "values": Array [ + "val-1-1", + "val-1-7", + ], + }, + Object { + "id": "attr-2", + "values": Array [ + "val-2-2", + "val-2-4", + ], + }, + Object { + "id": "attr-4", + "values": Array [ + "val-4-1", + "val-4-5", + ], + }, + ], + "price": Object { + "all": false, + "attribute": "attr-1", + "value": "10.99", + "values": Array [ + Object { + "slug": "val-1-1", + "value": "45.99", + }, + Object { + "slug": "val-1-7", + "value": "51.99", + }, + ], + }, + "stock": Object { + "all": true, + "attribute": undefined, + "value": "", + "values": Array [], + }, + "variants": Array [ + Object { + "attributes": Array [ + Object { + "id": "attr-1", + "values": Array [ + "val-1-1", + ], + }, + Object { + "id": "attr-2", + "values": Array [ + "val-2-2", + ], + }, + Object { + "id": "attr-4", + "values": Array [ + "val-4-1", + ], + }, + ], + "priceOverride": "45.99", + "quantity": NaN, + "sku": "", + }, + Object { + "attributes": Array [ + Object { + "id": "attr-1", + "values": Array [ + "val-1-1", + ], + }, + Object { + "id": "attr-2", + "values": Array [ + "val-2-2", + ], + }, + Object { + "id": "attr-4", + "values": Array [ + "val-4-5", + ], + }, + ], + "priceOverride": "45.99", + "quantity": NaN, + "sku": "", + }, + Object { + "attributes": Array [ + Object { + "id": "attr-1", + "values": Array [ + "val-1-1", + ], + }, + Object { + "id": "attr-2", + "values": Array [ + "val-2-4", + ], + }, + Object { + "id": "attr-4", + "values": Array [ + "val-4-1", + ], + }, + ], + "priceOverride": "45.99", + "quantity": NaN, + "sku": "", + }, + Object { + "attributes": Array [ + Object { + "id": "attr-1", + "values": Array [ + "val-1-1", + ], + }, + Object { + "id": "attr-2", + "values": Array [ + "val-2-4", + ], + }, + Object { + "id": "attr-4", + "values": Array [ + "val-4-5", + ], + }, + ], + "priceOverride": "45.99", + "quantity": NaN, + "sku": "", + }, + Object { + "attributes": Array [ + Object { + "id": "attr-1", + "values": Array [ + "val-1-7", + ], + }, + Object { + "id": "attr-2", + "values": Array [ + "val-2-2", + ], + }, + Object { + "id": "attr-4", + "values": Array [ + "val-4-1", + ], + }, + ], + "priceOverride": "51.99", + "quantity": NaN, + "sku": "", + }, + Object { + "attributes": Array [ + Object { + "id": "attr-1", + "values": Array [ + "val-1-7", + ], + }, + Object { + "id": "attr-2", + "values": Array [ + "val-2-2", + ], + }, + Object { + "id": "attr-4", + "values": Array [ + "val-4-5", + ], + }, + ], + "priceOverride": "51.99", + "quantity": NaN, + "sku": "", + }, + Object { + "attributes": Array [ + Object { + "id": "attr-1", + "values": Array [ + "val-1-7", + ], + }, + Object { + "id": "attr-2", + "values": Array [ + "val-2-4", + ], + }, + Object { + "id": "attr-4", + "values": Array [ + "val-4-1", + ], + }, + ], + "priceOverride": "51.99", + "quantity": NaN, + "sku": "", + }, + Object { + "attributes": Array [ + Object { + "id": "attr-1", + "values": Array [ + "val-1-7", + ], + }, + Object { + "id": "attr-2", + "values": Array [ + "val-2-4", + ], + }, + Object { + "id": "attr-4", + "values": Array [ + "val-4-5", + ], + }, + ], + "priceOverride": "51.99", + "quantity": NaN, + "sku": "", + }, + ], +} +`; + +exports[`Reducer is able to select stock for all variants 1`] = ` +Object { + "attributes": Array [ + Object { + "id": "attr-1", + "values": Array [ + "val-1-1", + "val-1-7", + ], + }, + Object { + "id": "attr-2", + "values": Array [ + "val-2-2", + "val-2-4", + ], + }, + Object { + "id": "attr-4", + "values": Array [ + "val-4-1", + "val-4-5", + ], + }, + ], + "price": Object { + "all": true, + "attribute": undefined, + "value": "10.99", + "values": Array [], + }, + "stock": Object { + "all": true, + "attribute": undefined, + "value": "45.99", + "values": Array [], + }, + "variants": Array [ + Object { + "attributes": Array [ + Object { + "id": "attr-1", + "values": Array [ + "val-1-1", + ], + }, + Object { + "id": "attr-2", + "values": Array [ + "val-2-2", + ], + }, + Object { + "id": "attr-4", + "values": Array [ + "val-4-1", + ], + }, + ], + "priceOverride": "10.99", + "quantity": 45, + "sku": "", + }, + Object { + "attributes": Array [ + Object { + "id": "attr-1", + "values": Array [ + "val-1-1", + ], + }, + Object { + "id": "attr-2", + "values": Array [ + "val-2-2", + ], + }, + Object { + "id": "attr-4", + "values": Array [ + "val-4-5", + ], + }, + ], + "priceOverride": "10.99", + "quantity": 45, + "sku": "", + }, + Object { + "attributes": Array [ + Object { + "id": "attr-1", + "values": Array [ + "val-1-1", + ], + }, + Object { + "id": "attr-2", + "values": Array [ + "val-2-4", + ], + }, + Object { + "id": "attr-4", + "values": Array [ + "val-4-1", + ], + }, + ], + "priceOverride": "10.99", + "quantity": 45, + "sku": "", + }, + Object { + "attributes": Array [ + Object { + "id": "attr-1", + "values": Array [ + "val-1-1", + ], + }, + Object { + "id": "attr-2", + "values": Array [ + "val-2-4", + ], + }, + Object { + "id": "attr-4", + "values": Array [ + "val-4-5", + ], + }, + ], + "priceOverride": "10.99", + "quantity": 45, + "sku": "", + }, + Object { + "attributes": Array [ + Object { + "id": "attr-1", + "values": Array [ + "val-1-7", + ], + }, + Object { + "id": "attr-2", + "values": Array [ + "val-2-2", + ], + }, + Object { + "id": "attr-4", + "values": Array [ + "val-4-1", + ], + }, + ], + "priceOverride": "10.99", + "quantity": 45, + "sku": "", + }, + Object { + "attributes": Array [ + Object { + "id": "attr-1", + "values": Array [ + "val-1-7", + ], + }, + Object { + "id": "attr-2", + "values": Array [ + "val-2-2", + ], + }, + Object { + "id": "attr-4", + "values": Array [ + "val-4-5", + ], + }, + ], + "priceOverride": "10.99", + "quantity": 45, + "sku": "", + }, + Object { + "attributes": Array [ + Object { + "id": "attr-1", + "values": Array [ + "val-1-7", + ], + }, + Object { + "id": "attr-2", + "values": Array [ + "val-2-4", + ], + }, + Object { + "id": "attr-4", + "values": Array [ + "val-4-1", + ], + }, + ], + "priceOverride": "10.99", + "quantity": 45, + "sku": "", + }, + Object { + "attributes": Array [ + Object { + "id": "attr-1", + "values": Array [ + "val-1-7", + ], + }, + Object { + "id": "attr-2", + "values": Array [ + "val-2-4", + ], + }, + Object { + "id": "attr-4", + "values": Array [ + "val-4-5", + ], + }, + ], + "priceOverride": "10.99", + "quantity": 45, + "sku": "", + }, + ], +} +`; + +exports[`Reducer is able to select stock to each attribute value 1`] = ` +Object { + "attributes": Array [ + Object { + "id": "attr-1", + "values": Array [ + "val-1-1", + "val-1-7", + ], + }, + Object { + "id": "attr-2", + "values": Array [ + "val-2-2", + "val-2-4", + ], + }, + Object { + "id": "attr-4", + "values": Array [ + "val-4-1", + "val-4-5", + ], + }, + ], + "price": Object { + "all": true, + "attribute": undefined, + "value": "10.99", + "values": Array [], + }, + "stock": Object { + "all": false, + "attribute": "attr-1", + "value": "", + "values": Array [ + Object { + "slug": "val-1-1", + "value": "13", + }, + Object { + "slug": "val-1-7", + "value": "19", + }, + ], + }, + "variants": Array [ + Object { + "attributes": Array [ + Object { + "id": "attr-1", + "values": Array [ + "val-1-1", + ], + }, + Object { + "id": "attr-2", + "values": Array [ + "val-2-2", + ], + }, + Object { + "id": "attr-4", + "values": Array [ + "val-4-1", + ], + }, + ], + "priceOverride": "10.99", + "quantity": 13, + "sku": "", + }, + Object { + "attributes": Array [ + Object { + "id": "attr-1", + "values": Array [ + "val-1-1", + ], + }, + Object { + "id": "attr-2", + "values": Array [ + "val-2-2", + ], + }, + Object { + "id": "attr-4", + "values": Array [ + "val-4-5", + ], + }, + ], + "priceOverride": "10.99", + "quantity": 13, + "sku": "", + }, + Object { + "attributes": Array [ + Object { + "id": "attr-1", + "values": Array [ + "val-1-1", + ], + }, + Object { + "id": "attr-2", + "values": Array [ + "val-2-4", + ], + }, + Object { + "id": "attr-4", + "values": Array [ + "val-4-1", + ], + }, + ], + "priceOverride": "10.99", + "quantity": 13, + "sku": "", + }, + Object { + "attributes": Array [ + Object { + "id": "attr-1", + "values": Array [ + "val-1-1", + ], + }, + Object { + "id": "attr-2", + "values": Array [ + "val-2-4", + ], + }, + Object { + "id": "attr-4", + "values": Array [ + "val-4-5", + ], + }, + ], + "priceOverride": "10.99", + "quantity": 13, + "sku": "", + }, + Object { + "attributes": Array [ + Object { + "id": "attr-1", + "values": Array [ + "val-1-7", + ], + }, + Object { + "id": "attr-2", + "values": Array [ + "val-2-2", + ], + }, + Object { + "id": "attr-4", + "values": Array [ + "val-4-1", + ], + }, + ], + "priceOverride": "10.99", + "quantity": 19, + "sku": "", + }, + Object { + "attributes": Array [ + Object { + "id": "attr-1", + "values": Array [ + "val-1-7", + ], + }, + Object { + "id": "attr-2", + "values": Array [ + "val-2-2", + ], + }, + Object { + "id": "attr-4", + "values": Array [ + "val-4-5", + ], + }, + ], + "priceOverride": "10.99", + "quantity": 19, + "sku": "", + }, + Object { + "attributes": Array [ + Object { + "id": "attr-1", + "values": Array [ + "val-1-7", + ], + }, + Object { + "id": "attr-2", + "values": Array [ + "val-2-4", + ], + }, + Object { + "id": "attr-4", + "values": Array [ + "val-4-1", + ], + }, + ], + "priceOverride": "10.99", + "quantity": 19, + "sku": "", + }, + Object { + "attributes": Array [ + Object { + "id": "attr-1", + "values": Array [ + "val-1-7", + ], + }, + Object { + "id": "attr-2", + "values": Array [ + "val-2-4", + ], + }, + Object { + "id": "attr-4", + "values": Array [ + "val-4-5", + ], + }, + ], + "priceOverride": "10.99", + "quantity": 19, + "sku": "", + }, + ], +} +`; diff --git a/src/products/components/ProductVariantCreateDialog/createVariants.test.ts b/src/products/components/ProductVariantCreateDialog/createVariants.test.ts new file mode 100644 index 000000000..e41c65229 --- /dev/null +++ b/src/products/components/ProductVariantCreateDialog/createVariants.test.ts @@ -0,0 +1,220 @@ +import { + createVariantFlatMatrixDimension, + createVariants +} from "./createVariants"; +import { attributes, thirdStep } from "./fixtures"; +import { ProductVariantCreateFormData } from "./form"; + +describe("Creates variant matrix", () => { + it("with proper size", () => { + const attributes = thirdStep.attributes; + + const matrix = createVariantFlatMatrixDimension([[]], attributes); + + expect(matrix).toHaveLength( + attributes.reduce((acc, attribute) => acc * attribute.values.length, 1) + ); + }); + + it("with constant price and stock", () => { + const price = "49.99"; + const stock = 80; + + const data: ProductVariantCreateFormData = { + ...thirdStep, + price: { + ...thirdStep.price, + all: true, + value: price + }, + stock: { + ...thirdStep.stock, + all: true, + value: stock.toString() + } + }; + + const variants = createVariants(data); + expect(variants).toHaveLength( + thirdStep.attributes.reduce( + (acc, attribute) => acc * attribute.values.length, + 1 + ) + ); + + variants.forEach(variant => { + expect(variant.priceOverride).toBe(price); + expect(variant.quantity).toBe(stock); + }); + }); + + it("with constant stock and attribute dependent price", () => { + const price = 49.99; + const stock = 80; + const attribute = attributes.find( + attribute => attribute.id === thirdStep.attributes[0].id + ); + + const data: ProductVariantCreateFormData = { + ...thirdStep, + price: { + ...thirdStep.price, + all: false, + attribute: attribute.id, + values: attribute.values.map((attributeValue, attributeValueIndex) => ({ + slug: attributeValue, + value: (price * (attributeValueIndex + 1)).toString() + })) + }, + stock: { + ...thirdStep.stock, + all: true, + value: stock.toString() + } + }; + + const variants = createVariants(data); + expect(variants).toHaveLength( + thirdStep.attributes.reduce( + (acc, attribute) => acc * attribute.values.length, + 1 + ) + ); + + variants.forEach(variant => { + expect(variant.quantity).toBe(stock); + }); + + attribute.values.forEach((attributeValue, attributeValueIndex) => { + variants + .filter( + variant => + variant.attributes.find( + variantAttribute => variantAttribute.id === attribute.id + ).values[0] === attributeValue + ) + .forEach(variant => { + expect(variant.priceOverride).toBe( + (price * (attributeValueIndex + 1)).toString() + ); + }); + }); + }); + + it("with constant price and attribute dependent stock", () => { + const price = "49.99"; + const stock = 80; + const attribute = attributes.find( + attribute => attribute.id === thirdStep.attributes[0].id + ); + + const data: ProductVariantCreateFormData = { + ...thirdStep, + price: { + ...thirdStep.price, + all: true, + value: price + }, + stock: { + ...thirdStep.stock, + all: false, + attribute: attribute.id, + values: attribute.values.map((attributeValue, attributeValueIndex) => ({ + slug: attributeValue, + value: (stock * (attributeValueIndex + 1)).toString() + })) + } + }; + + const variants = createVariants(data); + expect(variants).toHaveLength( + thirdStep.attributes.reduce( + (acc, attribute) => acc * attribute.values.length, + 1 + ) + ); + + variants.forEach(variant => { + expect(variant.priceOverride).toBe(price); + }); + + attribute.values.forEach((attributeValue, attributeValueIndex) => { + variants + .filter( + variant => + variant.attributes.find( + variantAttribute => variantAttribute.id === attribute.id + ).values[0] === attributeValue + ) + .forEach(variant => { + expect(variant.quantity).toBe(stock * (attributeValueIndex + 1)); + }); + }); + }); + + it("with attribute dependent price and stock", () => { + const price = 49.99; + const stock = 80; + const attribute = attributes.find( + attribute => attribute.id === thirdStep.attributes[0].id + ); + + const data: ProductVariantCreateFormData = { + ...thirdStep, + price: { + ...thirdStep.price, + all: false, + attribute: attribute.id, + values: attribute.values.map((attributeValue, attributeValueIndex) => ({ + slug: attributeValue, + value: (price * (attributeValueIndex + 1)).toString() + })) + }, + stock: { + ...thirdStep.stock, + all: false, + attribute: attribute.id, + values: attribute.values.map((attributeValue, attributeValueIndex) => ({ + slug: attributeValue, + value: (stock * (attributeValueIndex + 1)).toString() + })) + } + }; + + const variants = createVariants(data); + expect(variants).toHaveLength( + thirdStep.attributes.reduce( + (acc, attribute) => acc * attribute.values.length, + 1 + ) + ); + + attribute.values.forEach((attributeValue, attributeValueIndex) => { + variants + .filter( + variant => + variant.attributes.find( + variantAttribute => variantAttribute.id === attribute.id + ).values[0] === attributeValue + ) + .forEach(variant => { + expect(variant.priceOverride).toBe( + (price * (attributeValueIndex + 1)).toString() + ); + }); + }); + + attribute.values.forEach((attributeValue, attributeValueIndex) => { + variants + .filter( + variant => + variant.attributes.find( + variantAttribute => variantAttribute.id === attribute.id + ).values[0] === attributeValue + ) + .forEach(variant => { + expect(variant.quantity).toBe(stock * (attributeValueIndex + 1)); + }); + }); + }); +}); diff --git a/src/products/components/ProductVariantCreateDialog/createVariants.ts b/src/products/components/ProductVariantCreateDialog/createVariants.ts new file mode 100644 index 000000000..6239832e2 --- /dev/null +++ b/src/products/components/ProductVariantCreateDialog/createVariants.ts @@ -0,0 +1,105 @@ +import { ProductVariantBulkCreateInput } from "@saleor/types/globalTypes"; +import { + AllOrAttribute, + Attribute, + ProductVariantCreateFormData +} from "./form"; + +interface CreateVariantAttributeValueInput { + attributeId: string; + attributeValueSlug: string; +} +type CreateVariantInput = CreateVariantAttributeValueInput[]; + +function getAttributeValuePriceOrStock( + attributes: CreateVariantInput, + priceOrStock: AllOrAttribute +): string { + const attribute = attributes.find( + attribute => attribute.attributeId === priceOrStock.attribute + ); + + const attributeValue = priceOrStock.values.find( + attributeValue => attribute.attributeValueSlug === attributeValue.slug + ); + + return attributeValue.value; +} + +function createVariant( + data: ProductVariantCreateFormData, + attributes: CreateVariantInput +): ProductVariantBulkCreateInput { + const priceOverride = data.price.all + ? data.price.value + : getAttributeValuePriceOrStock(attributes, data.price); + const quantity = parseInt( + data.stock.all + ? data.stock.value + : getAttributeValuePriceOrStock(attributes, data.stock), + 10 + ); + + return { + attributes: attributes.map(attribute => ({ + id: attribute.attributeId, + values: [attribute.attributeValueSlug] + })), + priceOverride, + quantity, + sku: "" + }; +} + +function addAttributeToVariant( + attribute: Attribute, + variant: CreateVariantInput +): CreateVariantInput[] { + return attribute.values.map(attributeValueSlug => [ + ...variant, + { + attributeId: attribute.id, + attributeValueSlug + } + ]); +} +function addVariantAttributeInput( + data: CreateVariantInput[], + attribute: Attribute +): CreateVariantInput[] { + const variants = data + .map(variant => addAttributeToVariant(attribute, variant)) + .reduce((acc, variantInput) => [...acc, ...variantInput]); + + return variants; +} + +export function createVariantFlatMatrixDimension( + variants: CreateVariantInput[], + attributes: Attribute[] +): CreateVariantInput[] { + if (attributes.length > 0) { + return createVariantFlatMatrixDimension( + addVariantAttributeInput(variants, attributes[0]), + attributes.slice(1) + ); + } else { + return variants; + } +} + +export function createVariants( + data: ProductVariantCreateFormData +): ProductVariantBulkCreateInput[] { + if ( + (!data.price.all && !data.price.attribute) || + (!data.stock.all && !data.stock.attribute) + ) { + return []; + } + const variants = createVariantFlatMatrixDimension([[]], data.attributes).map( + variant => createVariant(data, variant) + ); + + return variants; +} diff --git a/src/products/components/ProductVariantCreateDialog/fixtures.ts b/src/products/components/ProductVariantCreateDialog/fixtures.ts new file mode 100644 index 000000000..5cc1b3ce2 --- /dev/null +++ b/src/products/components/ProductVariantCreateDialog/fixtures.ts @@ -0,0 +1,110 @@ +import { createVariants } from "./createVariants"; +import { + AllOrAttribute, + createInitialForm, + ProductVariantCreateFormData +} from "./form"; + +export const attributes = [ + { + id: "attr-1", + values: Array(9) + .fill(0) + .map((_, index) => `val-1-${index + 1}`) + }, + { + id: "attr-2", + values: Array(6) + .fill(0) + .map((_, index) => `val-2-${index + 1}`) + }, + { + id: "attr-3", + values: Array(4) + .fill(0) + .map((_, index) => `val-3-${index + 1}`) + }, + { + id: "attr-4", + values: Array(11) + .fill(0) + .map((_, index) => `val-4-${index + 1}`) + } +]; + +export const secondStep: ProductVariantCreateFormData = { + ...createInitialForm([], "10.99"), + attributes: [ + { + id: attributes[0].id, + values: [] + }, + { + id: attributes[1].id, + values: [] + }, + { + id: attributes[3].id, + values: [] + } + ] +}; + +export const thirdStep: ProductVariantCreateFormData = { + ...secondStep, + attributes: [ + { + id: attributes[0].id, + values: [0, 6].map(index => attributes[0].values[index]) + }, + { + id: attributes[1].id, + values: [1, 3].map(index => attributes[1].values[index]) + }, + { + id: attributes[3].id, + values: [0, 4].map(index => attributes[3].values[index]) + } + ] +}; + +const price: AllOrAttribute = { + all: false, + attribute: thirdStep.attributes[1].id, + value: "", + values: [ + { + slug: thirdStep.attributes[1].values[0], + value: "24.99" + }, + { + slug: thirdStep.attributes[1].values[1], + value: "26.99" + } + ] +}; +const stock: AllOrAttribute = { + all: false, + attribute: thirdStep.attributes[2].id, + value: "", + values: [ + { + slug: thirdStep.attributes[2].values[0], + value: "50" + }, + { + slug: thirdStep.attributes[2].values[1], + value: "35" + } + ] +}; +export const fourthStep: ProductVariantCreateFormData = { + ...thirdStep, + price, + stock, + variants: createVariants({ + ...thirdStep, + price, + stock + }) +}; diff --git a/src/products/components/ProductVariantCreateDialog/form.ts b/src/products/components/ProductVariantCreateDialog/form.ts new file mode 100644 index 000000000..d51f13b5d --- /dev/null +++ b/src/products/components/ProductVariantCreateDialog/form.ts @@ -0,0 +1,46 @@ +import { ProductDetails_product_productType_variantAttributes } from "@saleor/products/types/ProductDetails"; +import { ProductVariantBulkCreateInput } from "../../../types/globalTypes"; + +export interface AttributeValue { + slug: string; + value: string; +} +export interface AllOrAttribute { + all: boolean; + attribute: string; + value: string; + values: AttributeValue[]; +} +export interface Attribute { + id: string; + values: string[]; +} +export interface ProductVariantCreateFormData { + attributes: Attribute[]; + price: AllOrAttribute; + stock: AllOrAttribute; + variants: ProductVariantBulkCreateInput[]; +} + +export const createInitialForm = ( + attributes: ProductDetails_product_productType_variantAttributes[], + price: string +): ProductVariantCreateFormData => ({ + attributes: attributes.map(attribute => ({ + id: attribute.id, + values: [] + })), + price: { + all: true, + attribute: undefined, + value: price || "", + values: [] + }, + stock: { + all: true, + attribute: undefined, + value: "", + values: [] + }, + variants: [] +}); diff --git a/src/products/components/ProductVariantCreateDialog/index.ts b/src/products/components/ProductVariantCreateDialog/index.ts new file mode 100644 index 000000000..e69de29bb diff --git a/src/products/components/ProductVariantCreateDialog/reducer.test.ts b/src/products/components/ProductVariantCreateDialog/reducer.test.ts new file mode 100644 index 000000000..9a224af0f --- /dev/null +++ b/src/products/components/ProductVariantCreateDialog/reducer.test.ts @@ -0,0 +1,185 @@ +import { attributes, fourthStep, secondStep, thirdStep } from "./fixtures"; +import reducer, { VariantField } from "./reducer"; + +function execActions( + initialState: TState, + reducer: (state: TState, action: TAction) => TState, + actions: TAction[] +): TState { + return actions.reduce((acc, action) => reducer(acc, action), initialState); +} + +describe("Reducer is able to", () => { + it("select attribute values", () => { + const state = execActions(secondStep, reducer, [ + { + attributeId: attributes[0].id, + type: "selectValue", + valueId: attributes[0].values[0] + }, + { + attributeId: attributes[0].id, + type: "selectValue", + valueId: attributes[0].values[6] + }, + { + attributeId: attributes[1].id, + type: "selectValue", + valueId: attributes[1].values[1] + }, + { + attributeId: attributes[1].id, + type: "selectValue", + valueId: attributes[1].values[3] + }, + { + attributeId: attributes[3].id, + type: "selectValue", + valueId: attributes[3].values[0] + }, + { + attributeId: attributes[3].id, + type: "selectValue", + valueId: attributes[3].values[4] + } + ]); + + expect(state.attributes[0].values).toHaveLength(2); + expect(state.attributes[1].values).toHaveLength(2); + expect(state.attributes[2].values).toHaveLength(2); + expect(state).toMatchSnapshot(); + }); + + it("select price for all variants", () => { + const value = "45.99"; + const state = execActions(thirdStep, reducer, [ + { + all: true, + type: "applyPriceToAll" + }, + { + type: "changeApplyPriceToAllValue", + value + } + ]); + + expect(state.price.all).toBeTruthy(); + expect(state.price.value).toBe(value); + expect(state).toMatchSnapshot(); + }); + + it("select stock for all variants", () => { + const value = 45.99; + const state = execActions(thirdStep, reducer, [ + { + all: true, + type: "applyStockToAll" + }, + { + type: "changeApplyStockToAllValue", + value: value.toString() + } + ]); + + expect(state.stock.all).toBeTruthy(); + expect(state.stock.value).toBe(value.toString()); + expect(state).toMatchSnapshot(); + }); + + it("select price to each attribute value", () => { + const attribute = thirdStep.attributes[0]; + const value = 45.99; + const state = execActions(thirdStep, reducer, [ + { + all: false, + type: "applyPriceToAll" + }, + { + attributeId: attribute.id, + type: "changeApplyPriceToAttributeId" + }, + { + type: "changeAttributeValuePrice", + value: value.toString(), + valueId: attribute.values[0] + }, + { + type: "changeAttributeValuePrice", + value: (value + 6).toString(), + valueId: attribute.values[1] + } + ]); + + expect(state.price.all).toBeFalsy(); + expect(state.price.values).toHaveLength( + state.attributes.find(attribute => state.price.attribute === attribute.id) + .values.length + ); + expect(state).toMatchSnapshot(); + }); + + it("select stock to each attribute value", () => { + const attribute = thirdStep.attributes[0]; + const value = 13; + const state = execActions(thirdStep, reducer, [ + { + all: false, + type: "applyStockToAll" + }, + { + attributeId: attribute.id, + type: "changeApplyStockToAttributeId" + }, + { + type: "changeAttributeValueStock", + value: value.toString(), + valueId: attribute.values[0] + }, + { + type: "changeAttributeValueStock", + value: (value + 6).toString(), + valueId: attribute.values[1] + } + ]); + + expect(state.stock.all).toBeFalsy(); + expect(state.stock.values).toHaveLength( + state.attributes.find(attribute => state.stock.attribute === attribute.id) + .values.length + ); + expect(state).toMatchSnapshot(); + }); + + it("modify individual variant price", () => { + const field: VariantField = "price"; + const value = "49.99"; + const variantIndex = 3; + + const state = execActions(fourthStep, reducer, [ + { + field, + type: "changeVariantData", + value, + variantIndex + } + ]); + + expect(state.variants[variantIndex].priceOverride).toBe(value); + expect(state.variants[variantIndex - 1].priceOverride).toBe( + fourthStep.variants[variantIndex - 1].priceOverride + ); + }); + + it("delete variant", () => { + const variantIndex = 3; + + const state = execActions(fourthStep, reducer, [ + { + type: "deleteVariant", + variantIndex + } + ]); + + expect(state.variants.length).toBe(fourthStep.variants.length - 1); + }); +}); diff --git a/src/products/components/ProductVariantCreateDialog/reducer.ts b/src/products/components/ProductVariantCreateDialog/reducer.ts new file mode 100644 index 000000000..ee8480ef0 --- /dev/null +++ b/src/products/components/ProductVariantCreateDialog/reducer.ts @@ -0,0 +1,363 @@ +//#region +import { + add, + remove, + removeAtIndex, + toggle, + updateAtIndex +} from "@saleor/utils/lists"; +import { createVariants } from "./createVariants"; +import { ProductVariantCreateFormData } from "./form"; + +export type ProductVariantCreateReducerActionType = + | "applyPriceToAll" + | "applyPriceToAttribute" + | "applyStockToAll" + | "applyStockToAttribute" + | "changeApplyPriceToAllValue" + | "changeApplyPriceToAttributeId" + | "changeApplyStockToAllValue" + | "changeApplyStockToAttributeId" + | "changeAttributeValuePrice" + | "changeAttributeValueStock" + | "changeVariantData" + | "deleteVariant" + | "reload" + | "selectValue"; + +export type VariantField = "stock" | "price" | "sku"; +export interface ProductVariantCreateReducerAction { + all?: boolean; + attributeId?: string; + data?: ProductVariantCreateFormData; + field?: VariantField; + type: ProductVariantCreateReducerActionType; + value?: string; + valueId?: string; + variantIndex?: number; +} +//#endregion +function selectValue( + prevState: ProductVariantCreateFormData, + attributeId: string, + valueSlug: string +): ProductVariantCreateFormData { + const attribute = prevState.attributes.find( + attribute => attribute.id === attributeId + ); + const values = toggle(valueSlug, attribute.values, (a, b) => a === b); + const updatedAttributes = add( + { + id: attributeId, + values + }, + remove(attribute, prevState.attributes, (a, b) => a.id === b.id) + ); + + const priceValues = + prevState.price.attribute === attributeId + ? toggle( + { + slug: valueSlug, + value: "" + }, + prevState.price.values, + (a, b) => a.slug === b.slug + ) + : prevState.price.values; + + const stockValues = + prevState.stock.attribute === attributeId + ? toggle( + { + slug: valueSlug, + value: "" + }, + prevState.stock.values, + (a, b) => a.slug === b.slug + ) + : prevState.stock.values; + + return { + ...prevState, + attributes: updatedAttributes, + price: { + ...prevState.price, + values: priceValues + }, + stock: { + ...prevState.stock, + values: stockValues + } + }; +} + +function applyPriceToAll( + state: ProductVariantCreateFormData, + value: boolean +): ProductVariantCreateFormData { + const data = { + ...state, + price: { + ...state.price, + all: value + } + }; + + return { + ...data, + variants: createVariants(data) + }; +} + +function applyStockToAll( + state: ProductVariantCreateFormData, + value: boolean +): ProductVariantCreateFormData { + const data = { + ...state, + stock: { + ...state.stock, + all: value + } + }; + + return { + ...data, + variants: createVariants(data) + }; +} + +function changeAttributeValuePrice( + state: ProductVariantCreateFormData, + attributeValueSlug: string, + price: string +): ProductVariantCreateFormData { + const index = state.price.values.findIndex( + value => value.slug === attributeValueSlug + ); + + if (index === -1) { + throw new Error(`Value with id ${attributeValueSlug} not found`); + } + + const values = updateAtIndex( + { + slug: attributeValueSlug, + value: price + }, + state.price.values, + index + ); + + const data = { + ...state, + price: { + ...state.price, + values + } + }; + + return { + ...data, + variants: createVariants(data) + }; +} + +function changeAttributeValueStock( + state: ProductVariantCreateFormData, + attributeValueSlug: string, + stock: string +): ProductVariantCreateFormData { + const index = state.stock.values.findIndex( + value => value.slug === attributeValueSlug + ); + + if (index === -1) { + throw new Error(`Value with id ${attributeValueSlug} not found`); + } + + const values = updateAtIndex( + { + slug: attributeValueSlug, + value: stock + }, + state.stock.values, + index + ); + + const data = { + ...state, + stock: { + ...state.stock, + values + } + }; + + return { + ...data, + variants: createVariants(data) + }; +} + +function changeApplyPriceToAttributeId( + state: ProductVariantCreateFormData, + attributeId: string +): ProductVariantCreateFormData { + const attribute = state.attributes.find( + attribute => attribute.id === attributeId + ); + const values = attribute.values.map(slug => ({ + slug, + value: "" + })); + const data = { + ...state, + price: { + ...state.price, + attribute: attributeId, + values + } + }; + + return { + ...data, + variants: createVariants(data) + }; +} + +function changeApplyStockToAttributeId( + state: ProductVariantCreateFormData, + attributeId: string +): ProductVariantCreateFormData { + const attribute = state.attributes.find( + attribute => attribute.id === attributeId + ); + const values = attribute.values.map(slug => ({ + slug, + value: "" + })); + + const data = { + ...state, + stock: { + ...state.stock, + attribute: attributeId, + values + } + }; + + return { + ...data, + variants: createVariants(data) + }; +} + +function changeApplyPriceToAllValue( + state: ProductVariantCreateFormData, + value: string +): ProductVariantCreateFormData { + const data = { + ...state, + price: { + ...state.price, + value + } + }; + + return { + ...data, + variants: createVariants(data) + }; +} + +function changeApplyStockToAllValue( + state: ProductVariantCreateFormData, + value: string +): ProductVariantCreateFormData { + const data = { + ...state, + stock: { + ...state.stock, + value + } + }; + + return { + ...data, + variants: createVariants(data) + }; +} + +function changeVariantData( + state: ProductVariantCreateFormData, + field: VariantField, + value: string, + variantIndex: number +): ProductVariantCreateFormData { + const variant = state.variants[variantIndex]; + if (field === "price") { + variant.priceOverride = value; + } else if (field === "sku") { + variant.sku = value; + } else { + variant.quantity = parseInt(value, 10); + } + + return { + ...state, + variants: updateAtIndex(variant, state.variants, variantIndex) + }; +} + +function deleteVariant( + state: ProductVariantCreateFormData, + variantIndex: number +): ProductVariantCreateFormData { + return { + ...state, + variants: removeAtIndex(state.variants, variantIndex) + }; +} + +function reduceProductVariantCreateFormData( + prevState: ProductVariantCreateFormData, + action: ProductVariantCreateReducerAction +) { + switch (action.type) { + case "selectValue": + return selectValue(prevState, action.attributeId, action.valueId); + + case "applyPriceToAll": + return applyPriceToAll(prevState, action.all); + case "applyStockToAll": + return applyStockToAll(prevState, action.all); + case "changeAttributeValuePrice": + return changeAttributeValuePrice(prevState, action.valueId, action.value); + case "changeAttributeValueStock": + return changeAttributeValueStock(prevState, action.valueId, action.value); + case "changeApplyPriceToAttributeId": + return changeApplyPriceToAttributeId(prevState, action.attributeId); + case "changeApplyStockToAttributeId": + return changeApplyStockToAttributeId(prevState, action.attributeId); + case "changeApplyPriceToAllValue": + return changeApplyPriceToAllValue(prevState, action.value); + case "changeApplyStockToAllValue": + return changeApplyStockToAllValue(prevState, action.value); + case "changeVariantData": + return changeVariantData( + prevState, + action.field, + action.value, + action.variantIndex + ); + case "deleteVariant": + return deleteVariant(prevState, action.variantIndex); + case "reload": + return action.data; + default: + return prevState; + } +} + +export default reduceProductVariantCreateFormData; diff --git a/src/products/components/ProductVariantCreateDialog/types.ts b/src/products/components/ProductVariantCreateDialog/types.ts new file mode 100644 index 000000000..576a569aa --- /dev/null +++ b/src/products/components/ProductVariantCreateDialog/types.ts @@ -0,0 +1 @@ +export type ProductVariantCreateStep = "values" | "prices" | "summary"; diff --git a/src/products/components/ProductVariants/ProductVariants.tsx b/src/products/components/ProductVariants/ProductVariants.tsx index bf6332396..0de9f974d 100644 --- a/src/products/components/ProductVariants/ProductVariants.tsx +++ b/src/products/components/ProductVariants/ProductVariants.tsx @@ -69,6 +69,7 @@ interface ProductVariantsProps extends ListActions, WithStyles { fallbackPrice?: ProductVariant_costPrice; onRowClick: (id: string) => () => void; onVariantAdd?(); + onVariantsAdd?(); } const numberOfColumns = 5; @@ -81,6 +82,7 @@ export const ProductVariants = withStyles(styles, { name: "ProductVariants" })( fallbackPrice, onRowClick, onVariantAdd, + onVariantsAdd, isChecked, selected, toggle, @@ -98,7 +100,7 @@ export const ProductVariants = withStyles(styles, { name: "ProductVariants" })( description: "section header" })} toolbar={ - <> + hasVariants ? ( - + ) : ( + + ) } /> {!variants.length && ( diff --git a/src/products/containers/ProductUpdateOperations.tsx b/src/products/containers/ProductUpdateOperations.tsx index 3ed040bbc..73940d8e5 100644 --- a/src/products/containers/ProductUpdateOperations.tsx +++ b/src/products/containers/ProductUpdateOperations.tsx @@ -7,6 +7,7 @@ import { TypedProductImageCreateMutation, TypedProductImageDeleteMutation, TypedProductUpdateMutation, + TypedProductVariantBulkCreateMutation, TypedProductVariantBulkDeleteMutation, TypedSimpleProductUpdateMutation } from "../mutations"; @@ -25,6 +26,10 @@ import { ProductImageReorderVariables } from "../types/ProductImageReorder"; import { ProductUpdate, ProductUpdateVariables } from "../types/ProductUpdate"; +import { + ProductVariantBulkCreate, + ProductVariantBulkCreateVariables +} from "../types/ProductVariantBulkCreate"; import { ProductVariantBulkDelete, ProductVariantBulkDeleteVariables @@ -38,6 +43,10 @@ import ProductImagesReorderProvider from "./ProductImagesReorder"; interface ProductUpdateOperationsProps { product: ProductDetails_product; children: (props: { + bulkProductVariantCreate: PartialMutationProviderOutput< + ProductVariantBulkCreate, + ProductVariantBulkCreateVariables + >; bulkProductVariantDelete: PartialMutationProviderOutput< ProductVariantBulkDelete, ProductVariantBulkDeleteVariables @@ -67,6 +76,7 @@ interface ProductUpdateOperationsProps { SimpleProductUpdateVariables >; }) => React.ReactNode; + onBulkProductVariantCreate?: (data: ProductVariantBulkCreate) => void; onBulkProductVariantDelete?: (data: ProductVariantBulkDelete) => void; onDelete?: (data: ProductDelete) => void; onImageCreate?: (data: ProductImageCreate) => void; @@ -80,6 +90,7 @@ const ProductUpdateOperations: React.StatelessComponent< > = ({ product, children, + onBulkProductVariantCreate, onBulkProductVariantDelete, onDelete, onImageDelete, @@ -112,31 +123,40 @@ const ProductUpdateOperations: React.StatelessComponent< - {(...bulkProductVariantDelete) => - children({ - bulkProductVariantDelete: getMutationProviderData( - ...bulkProductVariantDelete - ), - createProductImage: getMutationProviderData( - ...createProductImage - ), - deleteProduct: getMutationProviderData( - ...deleteProduct - ), - deleteProductImage: getMutationProviderData( - ...deleteProductImage - ), - reorderProductImages: getMutationProviderData( - ...reorderProductImages - ), - updateProduct: getMutationProviderData( - ...updateProduct - ), - updateSimpleProduct: getMutationProviderData( - ...updateSimpleProduct - ) - }) - } + {(...bulkProductVariantDelete) => ( + + {(...bulkProductVariantCreate) => + children({ + bulkProductVariantCreate: getMutationProviderData( + ...bulkProductVariantCreate + ), + bulkProductVariantDelete: getMutationProviderData( + ...bulkProductVariantDelete + ), + createProductImage: getMutationProviderData( + ...createProductImage + ), + deleteProduct: getMutationProviderData( + ...deleteProduct + ), + deleteProductImage: getMutationProviderData( + ...deleteProductImage + ), + reorderProductImages: getMutationProviderData( + ...reorderProductImages + ), + updateProduct: getMutationProviderData( + ...updateProduct + ), + updateSimpleProduct: getMutationProviderData( + ...updateSimpleProduct + ) + }) + } + + )} )} diff --git a/src/products/mutations.ts b/src/products/mutations.ts index ef7024d69..c612d576d 100644 --- a/src/products/mutations.ts +++ b/src/products/mutations.ts @@ -45,6 +45,10 @@ import { productBulkPublish, productBulkPublishVariables } from "./types/productBulkPublish"; +import { + ProductVariantBulkCreate, + ProductVariantBulkCreateVariables +} from "./types/ProductVariantBulkCreate"; import { ProductVariantBulkDelete, ProductVariantBulkDeleteVariables @@ -319,26 +323,8 @@ export const TypedVariantUpdateMutation = TypedMutation< export const variantCreateMutation = gql` ${fragmentVariant} - mutation VariantCreate( - $attributes: [AttributeValueInput]! - $costPrice: Decimal - $priceOverride: Decimal - $product: ID! - $sku: String - $quantity: Int - $trackInventory: Boolean! - ) { - productVariantCreate( - input: { - attributes: $attributes - costPrice: $costPrice - priceOverride: $priceOverride - product: $product - sku: $sku - quantity: $quantity - trackInventory: $trackInventory - } - ) { + mutation VariantCreate($input: ProductVariantCreateInput!) { + productVariantCreate(input: $input) { errors { field message @@ -458,6 +444,30 @@ export const TypedProductBulkPublishMutation = TypedMutation< productBulkPublishVariables >(productBulkPublishMutation); +export const ProductVariantBulkCreateMutation = gql` + mutation ProductVariantBulkCreate( + $id: ID! + $inputs: [ProductVariantBulkCreateInput]! + ) { + productVariantBulkCreate(product: $id, variants: $inputs) { + bulkProductErrors { + field + message + code + index + } + errors { + field + message + } + } + } +`; +export const TypedProductVariantBulkCreateMutation = TypedMutation< + ProductVariantBulkCreate, + ProductVariantBulkCreateVariables +>(ProductVariantBulkCreateMutation); + export const ProductVariantBulkDeleteMutation = gql` mutation ProductVariantBulkDelete($ids: [ID!]!) { productVariantBulkDelete(ids: $ids) { diff --git a/src/products/queries.ts b/src/products/queries.ts index fa60db7cb..626b6ff3a 100644 --- a/src/products/queries.ts +++ b/src/products/queries.ts @@ -260,6 +260,17 @@ const productDetailsQuery = gql` query ProductDetails($id: ID!) { product(id: $id) { ...Product + productType { + variantAttributes { + id + name + values { + id + name + slug + } + } + } } } `; diff --git a/src/products/types/ProductDetails.ts b/src/products/types/ProductDetails.ts index 5094afd59..3ed43c47a 100644 --- a/src/products/types/ProductDetails.ts +++ b/src/products/types/ProductDetails.ts @@ -139,11 +139,26 @@ export interface ProductDetails_product_variants { stockQuantity: number; } +export interface ProductDetails_product_productType_variantAttributes_values { + __typename: "AttributeValue"; + id: string; + name: string | null; + slug: string | null; +} + +export interface ProductDetails_product_productType_variantAttributes { + __typename: "Attribute"; + id: string; + name: string | null; + values: (ProductDetails_product_productType_variantAttributes_values | null)[] | null; +} + export interface ProductDetails_product_productType { __typename: "ProductType"; id: string; name: string; hasVariants: boolean; + variantAttributes: (ProductDetails_product_productType_variantAttributes | null)[] | null; } export interface ProductDetails_product { diff --git a/src/products/types/ProductVariantBulkCreate.ts b/src/products/types/ProductVariantBulkCreate.ts new file mode 100644 index 000000000..f0a0160ff --- /dev/null +++ b/src/products/types/ProductVariantBulkCreate.ts @@ -0,0 +1,38 @@ +/* tslint:disable */ +/* eslint-disable */ +// This file was automatically generated and should not be edited. + +import { ProductVariantBulkCreateInput, ProductErrorCode } from "./../../types/globalTypes"; + +// ==================================================== +// GraphQL mutation operation: ProductVariantBulkCreate +// ==================================================== + +export interface ProductVariantBulkCreate_productVariantBulkCreate_bulkProductErrors { + __typename: "BulkProductError"; + field: string | null; + message: string | null; + code: ProductErrorCode | null; + index: number | null; +} + +export interface ProductVariantBulkCreate_productVariantBulkCreate_errors { + __typename: "Error"; + field: string | null; + message: string | null; +} + +export interface ProductVariantBulkCreate_productVariantBulkCreate { + __typename: "ProductVariantBulkCreate"; + bulkProductErrors: ProductVariantBulkCreate_productVariantBulkCreate_bulkProductErrors[] | null; + errors: ProductVariantBulkCreate_productVariantBulkCreate_errors[] | null; +} + +export interface ProductVariantBulkCreate { + productVariantBulkCreate: ProductVariantBulkCreate_productVariantBulkCreate | null; +} + +export interface ProductVariantBulkCreateVariables { + id: string; + inputs: (ProductVariantBulkCreateInput | null)[]; +} diff --git a/src/products/types/VariantCreate.ts b/src/products/types/VariantCreate.ts index 4dcfdd5d1..b1d2783b1 100644 --- a/src/products/types/VariantCreate.ts +++ b/src/products/types/VariantCreate.ts @@ -2,7 +2,7 @@ /* eslint-disable */ // This file was automatically generated and should not be edited. -import { AttributeValueInput } from "./../../types/globalTypes"; +import { ProductVariantCreateInput } from "./../../types/globalTypes"; // ==================================================== // GraphQL mutation operation: VariantCreate @@ -122,11 +122,5 @@ export interface VariantCreate { } export interface VariantCreateVariables { - attributes: (AttributeValueInput | null)[]; - costPrice?: any | null; - priceOverride?: any | null; - product: string; - sku?: string | null; - quantity?: number | null; - trackInventory: boolean; + input: ProductVariantCreateInput; } diff --git a/src/products/urls.ts b/src/products/urls.ts index 5ebfe8d31..2f240496f 100644 --- a/src/products/urls.ts +++ b/src/products/urls.ts @@ -53,7 +53,7 @@ export const productListUrl = (params?: ProductListUrlQueryParams): string => export const productPath = (id: string) => urlJoin(productSection + id); export type ProductUrlDialog = "remove"; export type ProductUrlQueryParams = BulkAction & - Dialog<"remove" | "remove-variants">; + Dialog<"create-variants" | "remove" | "remove-variants">; export const productUrl = (id: string, params?: ProductUrlQueryParams) => productPath(encodeURIComponent(id)) + "?" + stringifyQs(params); diff --git a/src/products/views/ProductUpdate/ProductUpdate.tsx b/src/products/views/ProductUpdate/ProductUpdate.tsx index d86c58c25..8f5d3fffb 100644 --- a/src/products/views/ProductUpdate/ProductUpdate.tsx +++ b/src/products/views/ProductUpdate/ProductUpdate.tsx @@ -10,7 +10,10 @@ import { WindowTitle } from "@saleor/components/WindowTitle"; import useBulkActions from "@saleor/hooks/useBulkActions"; import useNavigator from "@saleor/hooks/useNavigator"; import useNotifier from "@saleor/hooks/useNotifier"; +import useShop from "@saleor/hooks/useShop"; import { commonMessages } from "@saleor/intl"; +import ProductVariantCreateDialog from "@saleor/products/components/ProductVariantCreateDialog/ProductVariantCreateDialog"; +import { ProductVariantBulkCreate } from "@saleor/products/types/ProductVariantBulkCreate"; import { DEFAULT_INITIAL_SEARCH_DATA } from "../../../config"; import SearchCategories from "../../../containers/SearchCategories"; import SearchCollections from "../../../containers/SearchCollections"; @@ -54,6 +57,7 @@ export const ProductUpdate: React.StatelessComponent = ({ params.ids ); const intl = useIntl(); + const shop = useShop(); const openModal = (action: ProductUrlDialog) => navigate( @@ -115,6 +119,15 @@ export const ProductUpdate: React.StatelessComponent = ({ const handleVariantAdd = () => navigate(productVariantAddUrl(id)); + const handleBulkProductVariantCreate = ( + data: ProductVariantBulkCreate + ) => { + if (data.productVariantBulkCreate.errors.length === 0) { + navigate(productUrl(id), true); + refetch(); + } + }; + const handleBulkProductVariantDelete = ( data: ProductVariantBulkDelete ) => { @@ -125,10 +138,19 @@ export const ProductUpdate: React.StatelessComponent = ({ } }; + const handleVariantCreatorOpen = () => + navigate( + productUrl(id, { + ...params, + action: "create-variants" + }) + ); + const product = data ? data.product : undefined; return ( = ({ onUpdate={handleUpdate} > {({ + bulkProductVariantCreate, bulkProductVariantDelete, createProductImage, deleteProduct, @@ -245,6 +268,7 @@ export const ProductUpdate: React.StatelessComponent = ({ onImageReorder={handleImageReorder} onSubmit={handleSubmit} onVariantAdd={handleVariantAdd} + onVariantsAdd={handleVariantCreatorOpen} onVariantShow={variantId => () => navigate( productVariantEditUrl(product.id, variantId) @@ -328,6 +352,37 @@ export const ProductUpdate: React.StatelessComponent = ({ /> + + data.product.basePrice.amount.toFixed(2) + )} + errors={maybe( + () => + bulkProductVariantCreate.opts.data + .productVariantBulkCreate.bulkProductErrors, + [] + )} + open={params.action === "create-variants"} + attributes={maybe( + () => data.product.productType.variantAttributes, + [] + )} + currencySymbol={maybe(() => shop.defaultCurrency)} + onClose={() => + navigate( + productUrl(id, { + ...params, + action: undefined + }) + ) + } + onSubmit={inputs => + bulkProductVariantCreate.mutate({ + id, + inputs + }) + } + /> ); }} diff --git a/src/products/views/ProductVariantCreate.tsx b/src/products/views/ProductVariantCreate.tsx index b78ca66a9..a8ac30785 100644 --- a/src/products/views/ProductVariantCreate.tsx +++ b/src/products/views/ProductVariantCreate.tsx @@ -58,18 +58,20 @@ export const ProductVariant: React.StatelessComponent = ({ ) => variantCreate({ variables: { - attributes: formData.attributes - .filter(attribute => attribute.value !== "") - .map(attribute => ({ - id: attribute.id, - values: [attribute.value] - })), - costPrice: decimal(formData.costPrice), - priceOverride: decimal(formData.priceOverride), - product: productId, - quantity: formData.quantity || null, - sku: formData.sku, - trackInventory: true + input: { + attributes: formData.attributes + .filter(attribute => attribute.value !== "") + .map(attribute => ({ + id: attribute.id, + values: [attribute.value] + })), + costPrice: decimal(formData.costPrice), + priceOverride: decimal(formData.priceOverride), + product: productId, + quantity: formData.quantity || null, + sku: formData.sku, + trackInventory: true + } } }); const handleVariantClick = (id: string) => diff --git a/src/shipping/components/ShippingZonesList/ShippingZonesList.tsx b/src/shipping/components/ShippingZonesList/ShippingZonesList.tsx index 56c2b6ff7..920d955b3 100644 --- a/src/shipping/components/ShippingZonesList/ShippingZonesList.tsx +++ b/src/shipping/components/ShippingZonesList/ShippingZonesList.tsx @@ -81,7 +81,7 @@ const ShippingZonesList = withStyles(styles, { name: "ShippingZonesList" })( diff --git a/src/storybook/__snapshots__/Stories.test.ts.snap b/src/storybook/__snapshots__/Stories.test.ts.snap index af213721e..40345b148 100644 --- a/src/storybook/__snapshots__/Stories.test.ts.snap +++ b/src/storybook/__snapshots__/Stories.test.ts.snap @@ -6050,7 +6050,7 @@ exports[`Storyshots Orders / OrderCustomer default 1`] = `

- Contact information + Contact Information

- Contact information + Contact Information

- Contact information + Contact Information

- Contact information + Contact Information

- Background image (optional) + Background Image (optional)
- Background image (optional) + Background Image (optional)
- Background image (optional) + Background Image (optional)
- Background image (optional) + Background Image (optional)
- Background image (optional) + Background Image (optional)
- Background image (optional) + Background Image (optional)
- Background image (optional) + Background Image (optional)
- Background image (optional) + Background Image (optional)
- Background image (optional) + Background Image (optional)
- Add collection + Add Collection
- Background image (optional) + Background Image (optional)
- Add collection + Add Collection
- Background image (optional) + Background Image (optional)
- Add customer + Add Customer
- Customer overview + Customer Overview
- Primary address + Primary Address
- Add customer + Add Customer
- Customer overview + Customer Overview
- Primary address + Primary Address
- Add customer + Add Customer
- Customer overview + Customer Overview
- Primary address + Primary Address
- Recent orders + Recent Orders
- Recent orders + Recent Orders
- Billing address + Billing Address

- Shipping address + Shipping Address

- Recent orders + Recent Orders
- Recent orders + Recent Orders
- Recent orders + Recent Orders
- Recent orders + Recent Orders
- Recent orders + Recent Orders
- Recent orders + Recent Orders
- Shipping address + Shipping Address

- Recent orders + Recent Orders
- Billing address + Billing Address

- Pricing + Discount Type
-
- - -
- USD + + + + - -
+ Percentage + + +
+
+
+
+
+ + Active Dates + +
+
+

-
- Time Frame -
-
- + +
+
+
+ +
+ + +
-
- -
- -
-
+ + + Set end date + +
@@ -38168,7 +38229,7 @@ exports[`Storyshots Views / Discounts / Sale create form errors 1`] = ` - Pricing + Discount Type
-
- - -
- USD + + + + - -
+ Percentage + + +
-

- Generic form error -

+
+
+
+
+ + Active Dates + +
+
+

-
- Time Frame -
-
- + +
+

+ Generic form error +

-

- Generic form error -

+ +
+ + +
+

+ Generic form error +

+
-
- -
- -
-

+ - Generic form error -

-
+ Set end date + +
@@ -38451,7 +38568,7 @@ exports[`Storyshots Views / Discounts / Sale create loading 1`] = ` - Pricing + Discount Type
-
- - -
- USD + + + + - -
+ Percentage + + +
+
+
+
+
+ + Active Dates + +
+
+

-
- Time Frame -
-
- + +
+
+
+ +
+ + +
-
- -
- -
-
+ + + Set end date + +
@@ -38721,7 +38902,7 @@ exports[`Storyshots Views / Discounts / Sale details collections 1`] = ` - Pricing + Discount Type
+
+
+ + +
+
+
+
+
+
+
+ + Value + +
+
+
+
+
-
- - % - - -
-
-
-
-
-
-
- Time Frame -
-
- -
- - -
-
-
- -
- - + %
@@ -39122,6 +39305,130 @@ exports[`Storyshots Views / Discounts / Sale details collections 1`] = `
+
+
+
+ + Active Dates + +
+
+
+
+
+
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+ +
+
- Pricing + Discount Type
+
+
+ + +
+
+
+
+
+
+
+ + Value + +
+
+
+
+
-
- - % - - -
-
-
-
-
-
-
- Time Frame -
-
- -
- - -
-
-
- -
- - + %
@@ -39714,6 +40023,130 @@ exports[`Storyshots Views / Discounts / Sale details default 1`] = `
+
+
+
+ + Active Dates + +
+
+
+
+
+
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+ +
+
- Pricing + Discount Type
+
+
+ + +
+
+
+
+
+
+
+ + Value + +
+
+
+
+
-
- - % - - -
+ %

-
-
-
- Time Frame -
-
- -
- - -
-

- Generic form error -

-
-
- -
- - -
-

- Generic form error -

-
-
+
+
+
+ + Active Dates + +
+
+
+
+
+
+
+ +
+ + +
+

+ Generic form error +

+
+
+ +
+ + +
+

+ Generic form error +

+
+
+ +
+
- Pricing + Discount Type
+
+
+ + +
+
+
+
+
+
+
+ + Value + +
+
+
+
+
-
- - USD - - -
-
-
-
-
-
-
- Time Frame -
-
- -
- - -
-
-
- -
- - + USD
@@ -40941,6 +41504,132 @@ exports[`Storyshots Views / Discounts / Sale details loading 1`] = `
+
+
+
+ + Active Dates + +
+
+
+
+
+
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+ +
+
- Pricing + Discount Type
+
+
+ + +
+
+
+
+
+
+
+ + Value + +
+
+
+
+
-
- - % - - -
-
-
-
-
-
-
- Time Frame -
-
- -
- - -
-
-
- -
- - + %
@@ -41833,6 +42524,130 @@ exports[`Storyshots Views / Discounts / Sale details products 1`] = `
+
+
+
+ + Active Dates + +
+
+
+
+
+
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+ +
+

-

-
+
-

`; +exports[`Storyshots Views / Products / Create multiple variants / summary default 1`] = ` +

+
+
+
+
+
+ + Select Values + +
+
+ + Prices and SKU + +
+
+ + Summary + +
+
+
+
+ You will create variants below +
+
+
+
+
+ Variant +
+
+ Price +
+
+ Inventory +
+
+ SKU +
+
+
+
+ + 100g + + + Arabica + + + Round + +
+
+
+
+ + + USD +
+
+
+
+
+
+ + +
+
+
+
+
+
+ + +
+
+
+
+ +
+
+
+
+ + 100g + + + Arabica + + + Polo + +
+
+
+
+ + + USD +
+
+
+
+
+
+ + +
+
+
+
+
+
+ + +
+
+
+
+ +
+
+
+
+ + 500g + + + Arabica + + + Round + +
+
+
+
+ + + USD +
+
+
+
+
+
+ + +
+
+
+
+
+
+ + +
+
+
+
+ +
+
+
+
+ + 500g + + + Arabica + + + Polo + +
+
+
+
+ + + USD +
+
+
+
+
+
+ + +
+
+
+
+
+
+ + +
+
+
+
+ +
+
+
+
+
+
+
+
+`; + +exports[`Storyshots Views / Products / Create multiple variants / summary errors 1`] = ` +
+
+
+
+
+
+ + Select Values + +
+
+ + Prices and SKU + +
+
+ + Summary + +
+
+
+
+ You will create variants below +
+
+
+
+
+ Variant +
+
+ Price +
+
+ Inventory +
+
+ SKU +
+
+
+
+ + 100g + + + Arabica + + + Round + +
+
+
+
+ + + USD +
+
+
+
+
+
+ + +
+
+
+
+
+
+ + +
+
+
+
+ +
+
+
+
+ + 100g + + + Arabica + + + Polo + +
+
+
+
+ + + USD +
+
+
+
+
+
+ + +
+
+
+
+
+
+ + +
+
+
+
+ +
+
+
+
+ + 500g + + + Arabica + + + Round + +
+
+
+
+ + + USD +
+
+
+
+
+
+ + +
+
+
+
+
+
+ + +
+
+
+
+ +
+
+
+
+ + 500g + + + Arabica + + + Polo + +
+
+
+
+ + + USD +
+
+
+
+
+
+ + +
+
+
+
+
+
+ + +
+

+ Duplicated SKU. +

+
+
+
+ +
+
+
+
+
+
+
+
+`; + +exports[`Storyshots Views / Products / Create multiple variants choose values 1`] = ` +
+
+
+
+
+
+ + Select Values + +
+
+ + Prices and SKU + +
+
+ + Summary + +
+
+
+
+ Box Size +
+
+
+ + + + +
+
+ Coffee Genre +
+
+
+ + +
+
+ Collar +
+
+
+ + + +
+
+
+
+
+
+`; + +exports[`Storyshots Views / Products / Create multiple variants interactive 1`] = ` +
+`; + +exports[`Storyshots Views / Products / Create multiple variants prices and SKU 1`] = ` +
+
+
+
+
+
+ + Select Values + +
+
+ + Prices and SKU + +
+
+ + Summary + +
+
+
+
+ Price +
+
+
+ +
+
+ +
+ + + USD +
+
+
+ +
+
+
+
+

+ Choose attribute +

+
+
+
+ +
+ +
+
+ Coffee Genre +
+ + +
+
+
+
+
+
+
+
+
+

+ Arabica +

+
+
+
+ +
+ + + USD +
+
+
+
+
+
+ Stock +
+
+
+ +
+
+ +
+ + +
+
+
+ +
+
+
+
+

+ Choose attribute +

+
+
+
+ +
+ +
+
+ Coffee Genre +
+ + +
+
+
+
+
+
+
+
+
+

+ Arabica +

+
+
+
+ +
+ + +
+
+
+
+
+
+
+
+
+`; + exports[`Storyshots Views / Products / Create product When loading 1`] = `
- Shipping by zone + Shipping By Zone
- Shipping by zone + Shipping By Zone
- Shipping by zone + Shipping By Zone
- Store information + Store Information
- Store information + Store Information
- Store information + Store Information
undefined, onVariantAdd: () => undefined, onVariantShow: () => undefined, + onVariantsAdd: () => undefined, placeholderImage, product, saveButtonBarState: "default", diff --git a/src/theme.ts b/src/theme.ts index c8d42b7a8..1ed2b9bfd 100644 --- a/src/theme.ts +++ b/src/theme.ts @@ -60,7 +60,7 @@ export default (colors: IThemeColors): Theme => }, flat: { "& span": { - color: colors.primary + color: colors.font.gray } }, flatPrimary: { @@ -106,7 +106,7 @@ export default (colors: IThemeColors): Theme => MuiFormControlLabel: { root: { display: "grid", - gridTemplateColumns: "50px 6fr" + gridTemplateColumns: "48px 1fr" } }, MuiFormLabel: { @@ -281,8 +281,7 @@ export default (colors: IThemeColors): Theme => "& fieldset": { "&&:not($error)": { borderColor: colors.input.border - }, - background: colors.background.paper + } }, "& legend": { display: "none" @@ -454,7 +453,8 @@ export default (colors: IThemeColors): Theme => ripple: { "&$rippleVisible": { backgroundColor: fade(colors.primary, 0.2) - } + }, + borderRadius: "100%" } } }, diff --git a/src/types/globalTypes.ts b/src/types/globalTypes.ts index 509193af8..eb0eafe33 100644 --- a/src/types/globalTypes.ts +++ b/src/types/globalTypes.ts @@ -193,6 +193,20 @@ export enum PermissionEnum { MANAGE_WEBHOOKS = "MANAGE_WEBHOOKS", } +export enum ProductErrorCode { + ALREADY_EXISTS = "ALREADY_EXISTS", + ATTRIBUTE_ALREADY_ASSIGNED = "ATTRIBUTE_ALREADY_ASSIGNED", + ATTRIBUTE_CANNOT_BE_ASSIGNED = "ATTRIBUTE_CANNOT_BE_ASSIGNED", + ATTRIBUTE_VARIANTS_DISABLED = "ATTRIBUTE_VARIANTS_DISABLED", + GRAPHQL_ERROR = "GRAPHQL_ERROR", + INVALID = "INVALID", + NOT_FOUND = "NOT_FOUND", + NOT_PRODUCTS_IMAGE = "NOT_PRODUCTS_IMAGE", + REQUIRED = "REQUIRED", + UNIQUE = "UNIQUE", + VARIANT_NO_DIGITAL_CONTENT = "VARIANT_NO_DIGITAL_CONTENT", +} + export enum ProductOrderField { DATE = "DATE", MINIMAL_PRICE = "MINIMAL_PRICE", @@ -624,6 +638,27 @@ export interface ProductTypeInput { taxCode?: string | null; } +export interface ProductVariantBulkCreateInput { + attributes: (AttributeValueInput | null)[]; + costPrice?: any | null; + priceOverride?: any | null; + sku: string; + quantity?: number | null; + trackInventory?: boolean | null; + weight?: any | null; +} + +export interface ProductVariantCreateInput { + attributes: (AttributeValueInput | null)[]; + costPrice?: any | null; + priceOverride?: any | null; + sku?: string | null; + quantity?: number | null; + trackInventory?: boolean | null; + weight?: any | null; + product: string; +} + export interface ProductVariantInput { attributes?: (AttributeValueInput | null)[] | null; costPrice?: any | null; diff --git a/webpack.config.js b/webpack.config.js index 55c8ce335..d2ccd8247 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -3,6 +3,7 @@ const path = require("path"); const webpack = require("webpack"); const TsconfigPathsPlugin = require("tsconfig-paths-webpack-plugin"); const HtmlWebpackPlugin = require("html-webpack-plugin"); +require('dotenv').config(); const resolve = path.resolve.bind(path, __dirname);