Merge pull request #390 from mirumee/add/warehouses

Add warehouses
This commit is contained in:
Marcin Gębala 2020-04-29 07:31:08 +02:00 committed by GitHub
commit 40b3791f0e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
228 changed files with 31311 additions and 12121 deletions

View file

@ -45,6 +45,7 @@ All notable, unreleased changes to this project will be documented in this file.
- Filter column ids before send it to GridAttributes operation - #476 by @gabmartinez
- Display Is Published column correctly in main Product Listing - #475 by @gabmartinez
- Add Permission Groups section - #406 by @krzysztofwolski
- Add warehouse management - #390 by @dominik-zeglen
## 2.0.0

View file

@ -1,8 +1,4 @@
{
"OrderCancelDialogButton": {
"context": "button",
"string": "Cancel Order"
},
"OrderCancelDialogHeader": {
"context": "dialog header",
"string": "Cancel Order"
@ -34,6 +30,9 @@
"configurationMenuTaxes": {
"string": "Manage how your store charges tax"
},
"configurationMenuWarehouses": {
"string": "Manage and update your warehouse information"
},
"configurationPluginsPages": {
"string": "View and update your plugins and their settings."
},
@ -206,10 +205,6 @@
"context": "vat not included in order price",
"string": "does not apply"
},
"prodictStockInventoryLabel": {
"context": "product stock",
"string": "Inventory"
},
"productStatusLabel": {
"context": "product",
"string": "Published"
@ -240,13 +235,17 @@
"context": "variant price",
"string": "Price"
},
"productVariantCreatePricesSetStockPlaceholder": {
"context": "variant stock",
"string": "Stock"
},
"productVariantCreatePricesStockInputLabel": {
"string": "Stock"
},
"productVariantCreatorStockSectionHeader": {
"context": "variant stock, header",
"string": "Stock"
},
"productVariantCreatorWarehouseSectionHeader": {
"context": "header",
"string": "Warehouses"
},
"productVariantPriceOptionalCostPriceField": {
"context": "optional field",
"string": "Optional"
@ -287,10 +286,18 @@
"context": "delete shipping zone",
"string": "Are you sure you want to delete {name}?"
},
"shippingZoneWarehouses_dot_autocomplete_dot_label": {
"context": "autocomplete select label",
"string": "Warehouse"
},
"siteSettingsMailingHelperText": {
"context": "helper text",
"string": "Mailing Configuration"
},
"src_dot_accept": {
"context": "button",
"string": "Accept"
},
"src_dot_attributes": {
"context": "attributes section name",
"string": "Attributes"
@ -1020,6 +1027,30 @@
"context": "pick columns to display",
"string": "{numberOfSelected} columns selected out of {numberOfTotal}"
},
"src_dot_components_dot_CompanyAddressInput_dot_1139500589": {
"string": "Country"
},
"src_dot_components_dot_CompanyAddressInput_dot_1271289966": {
"string": "Phone"
},
"src_dot_components_dot_CompanyAddressInput_dot_1363074570": {
"string": "Address line 1"
},
"src_dot_components_dot_CompanyAddressInput_dot_253031977": {
"string": "City"
},
"src_dot_components_dot_CompanyAddressInput_dot_2965971965": {
"string": "ZIP / Postal code"
},
"src_dot_components_dot_CompanyAddressInput_dot_3121963259": {
"string": "Address line 2"
},
"src_dot_components_dot_CompanyAddressInput_dot_3570415321": {
"string": "Company"
},
"src_dot_components_dot_CompanyAddressInput_dot_944851093": {
"string": "Country area"
},
"src_dot_components_dot_ConfirmButton_dot_2845142593": {
"context": "button",
"string": "Error"
@ -2191,6 +2222,10 @@
"src_dot_no": {
"string": "No"
},
"src_dot_ok": {
"context": "button",
"string": "OK"
},
"src_dot_optionalField": {
"context": "field is optional",
"string": "Optional"
@ -2211,19 +2246,18 @@
"context": "dialog header",
"string": "Cancel Orders"
},
"src_dot_orders_dot_components_dot_OrderBulkCancelDialog_dot_187921539": {
"context": "switch button",
"string": "Release all stock allocated to these orders"
},
"src_dot_orders_dot_components_dot_OrderBulkCancelDialog_dot_4224885638": {
"string": "{counter,plural,one{Are you sure you want to cancel this order?} other{Are you sure you want to cancel {displayQuantity} orders?}}"
},
"src_dot_orders_dot_components_dot_OrderCancelDialog_dot_3981375672": {
"string": "Are you sure you want to cancel order #{orderNumber}?"
"src_dot_orders_dot_components_dot_OrderCancelDialog_dot_4059738695": {
"string": "Cancelling this order will release unfulfilled stocks, so they can be bought by other customers. <b>Order will not be refunded when cancelling order - You need to do it manually.</b> Are you sure you want to cancel this order?"
},
"src_dot_orders_dot_components_dot_OrderCancelDialog_dot_944150063": {
"context": "switch button",
"string": "Release all stock allocated to this order"
"src_dot_orders_dot_components_dot_OrderCannotCancelOrderDialog_dot_1561587911": {
"context": "dialog header",
"string": "Saleor couldnt cancel order"
},
"src_dot_orders_dot_components_dot_OrderCannotCancelOrderDialog_dot_775268031": {
"string": "There are still fulfillments created for this order. Cancel the fulfillments first before you cancel the order."
},
"src_dot_orders_dot_components_dot_OrderCustomerNote_dot_1505053535": {
"string": "No notes from customer"
@ -2392,16 +2426,51 @@
"context": "button",
"string": "Finalize"
},
"src_dot_orders_dot_components_dot_OrderFulfillmentCancelDialog_dot_2569854889": {
"string": "Are you sure you want to cancel this fulfillment?"
"src_dot_orders_dot_components_dot_OrderFulfillPage_dot_1608534452": {
"context": "page header",
"string": "Order no. {orderNumber} - Add Fulfillment"
},
"src_dot_orders_dot_components_dot_OrderFulfillmentCancelDialog_dot_3515223857": {
"context": "switch button",
"string": "Restock items?"
"src_dot_orders_dot_components_dot_OrderFulfillPage_dot_2094985970": {
"context": "quantity of fulfilled products",
"string": "Quantity to fulfill"
},
"src_dot_orders_dot_components_dot_OrderFulfillmentCancelDialog_dot_675709443": {
"context": "button",
"string": "Cancel fulfillment"
"src_dot_orders_dot_components_dot_OrderFulfillPage_dot_2095687440": {
"context": "fulfill order, button",
"string": "Fulfill"
},
"src_dot_orders_dot_components_dot_OrderFulfillPage_dot_2588284040": {
"context": "no variant stock in warehouse",
"string": "No Stock"
},
"src_dot_orders_dot_components_dot_OrderFulfillPage_dot_3244948255": {
"context": "header",
"string": "Items ready to ship"
},
"src_dot_orders_dot_components_dot_OrderFulfillPage_dot_3620521256": {
"context": "page header",
"string": "Order"
},
"src_dot_orders_dot_components_dot_OrderFulfillPage_dot_4046223826": {
"string": "Product name"
},
"src_dot_orders_dot_components_dot_OrderFulfillPage_dot_4251997263": {
"context": "checkbox",
"string": "Send shipment details to customer"
},
"src_dot_orders_dot_components_dot_OrderFulfillPage_dot_580490159": {
"context": "page header with order number",
"string": "Order #{orderNumber}"
},
"src_dot_orders_dot_components_dot_OrderFulfillPage_dot_693960049": {
"context": "product's sku",
"string": "SKU"
},
"src_dot_orders_dot_components_dot_OrderFulfillmentCancelDialog_dot_1097287358": {
"string": "Are you sure you want to cancel fulfillment? Canceling a fulfillment will restock products at a selected warehouse."
},
"src_dot_orders_dot_components_dot_OrderFulfillmentCancelDialog_dot_46197273": {
"context": "select warehouse to restock items",
"string": "Select Warehouse"
},
"src_dot_orders_dot_components_dot_OrderFulfillmentCancelDialog_dot_732594284": {
"context": "dialog header",
@ -2456,9 +2525,17 @@
"context": "section header",
"string": "Fulfilled ({quantity})"
},
"src_dot_orders_dot_components_dot_OrderFulfillment_dot_662203348": {
"src_dot_orders_dot_components_dot_OrderFulfillment_dot_4039425374": {
"context": "cancelled fulfillment, section header",
"string": "Cancelled ({quantity})"
},
"src_dot_orders_dot_components_dot_OrderFulfillment_dot_732594284": {
"context": "button",
"string": "Cancel shipment"
"string": "Cancel Fulfillment"
},
"src_dot_orders_dot_components_dot_OrderFulfillment_dot_77179533": {
"context": "fulfillment group",
"string": "Fulfilled from: {warehouseName}"
},
"src_dot_orders_dot_components_dot_OrderFulfillment_dot_878013594": {
"context": "order line total price",
@ -2766,9 +2843,6 @@
"src_dot_orders_dot_views_dot_OrderDetails_dot_3367579693": {
"string": "Order successfully updated"
},
"src_dot_orders_dot_views_dot_OrderDetails_dot_4245651107": {
"string": "Items successfully fulfilled"
},
"src_dot_orders_dot_views_dot_OrderDetails_dot_580490159": {
"context": "window title",
"string": "Order #{orderNumber}"
@ -2800,16 +2874,21 @@
"context": "dialog content",
"string": "{counter,plural,one{Are you sure you want to delete this order draft?} other{Are you sure you want to delete {displayQuantity} order drafts?}}"
},
"src_dot_orders_dot_views_dot_OrderList_dot_1136302661": {
"string": "Orders cancelled"
"src_dot_orders_dot_views_dot_OrderFulfill_dot_1186021594": {
"context": "window title",
"string": "Fulfill Order #{orderNumber}"
},
"src_dot_orders_dot_views_dot_OrderFulfill_dot_1892156526": {
"context": "order fulfilled success message",
"string": "Fulfilled Items"
},
"src_dot_orders_dot_views_dot_OrderFulfill_dot_3754102871": {
"context": "window title",
"string": "Fulfill Order"
},
"src_dot_orders_dot_views_dot_OrderList_dot_1738939038": {
"string": "Order draft successfully created"
},
"src_dot_orders_dot_views_dot_OrderList_dot_3528672691": {
"context": "cancel orders, button",
"string": "Cancel"
},
"src_dot_pages": {
"context": "pages section name",
"string": "Pages"
@ -3520,13 +3599,35 @@
"src_dot_products_dot_components_dot_ProductPricing_dot_3015886868": {
"string": "Charge taxes for this item"
},
"src_dot_products_dot_components_dot_ProductStock_dot_1680952454": {
"context": "allocated product stock",
"string": "Allocated: {quantity}"
},
"src_dot_products_dot_components_dot_ProductStock_dot_2585918415": {
"src_dot_products_dot_components_dot_ProductStocks_dot_2585918415": {
"string": "SKU (Stock Keeping Unit)"
},
"src_dot_products_dot_components_dot_ProductStocks_dot_2622674857": {
"context": "tabel column header",
"string": "Warehouse Name"
},
"src_dot_products_dot_components_dot_ProductStocks_dot_2729628316": {
"context": "tabel column header",
"string": "Quantity Available"
},
"src_dot_products_dot_components_dot_ProductStocks_dot_2796503714": {
"context": "header",
"string": "Quantity"
},
"src_dot_products_dot_components_dot_ProductStocks_dot_3143542809": {
"string": "This product doesn't have any stock. You can add it <l>here</l>."
},
"src_dot_products_dot_components_dot_ProductStocks_dot_3482985373": {
"context": "button",
"string": "Edit Warehouses"
},
"src_dot_products_dot_components_dot_ProductStocks_dot_3633706025": {
"context": "product inventory, checkbox",
"string": "Track Inventory"
},
"src_dot_products_dot_components_dot_ProductStocks_dot_849869830": {
"string": "Active inventory tracking will automatically calculate changes of stock"
},
"src_dot_products_dot_components_dot_ProductUpdatePage_dot_1815688500": {
"context": "product",
"string": "since {date}"
@ -3546,81 +3647,6 @@
"context": "product attribute error",
"string": "This variant already exists"
},
"src_dot_products_dot_components_dot_ProductVariantCreateDialog_dot_1009678918": {
"context": "header",
"string": "You will create variants below"
},
"src_dot_products_dot_components_dot_ProductVariantCreateDialog_dot_1134347598": {
"context": "variant price",
"string": "Price"
},
"src_dot_products_dot_components_dot_ProductVariantCreateDialog_dot_168343345": {
"context": "variant attribute",
"string": "Attribute"
},
"src_dot_products_dot_components_dot_ProductVariantCreateDialog_dot_2478977538": {
"context": "attribute values, variant creation step",
"string": "Select Values"
},
"src_dot_products_dot_components_dot_ProductVariantCreateDialog_dot_2670525734": {
"context": "variant attribute",
"string": "Choose attribute"
},
"src_dot_products_dot_components_dot_ProductVariantCreateDialog_dot_2745385064": {
"context": "variant creation step",
"string": "Summary"
},
"src_dot_products_dot_components_dot_ProductVariantCreateDialog_dot_2783195765": {
"string": "Apply single price to all SKUs"
},
"src_dot_products_dot_components_dot_ProductVariantCreateDialog_dot_3387090508": {
"string": "Apply unique stock by attribute to each SKU"
},
"src_dot_products_dot_components_dot_ProductVariantCreateDialog_dot_3490038570": {
"context": "variant stock amount",
"string": "Inventory"
},
"src_dot_products_dot_components_dot_ProductVariantCreateDialog_dot_3528672691": {
"context": "button",
"string": "Cancel"
},
"src_dot_products_dot_components_dot_ProductVariantCreateDialog_dot_3570949907": {
"string": "Apply unique prices by attribute to each SKU"
},
"src_dot_products_dot_components_dot_ProductVariantCreateDialog_dot_3601538615": {
"string": "Apply single stock to all SKUs"
},
"src_dot_products_dot_components_dot_ProductVariantCreateDialog_dot_3673120330": {
"context": "button",
"string": "Next"
},
"src_dot_products_dot_components_dot_ProductVariantCreateDialog_dot_3841616483": {
"context": "variant stock, header",
"string": "Stock"
},
"src_dot_products_dot_components_dot_ProductVariantCreateDialog_dot_3922579741": {
"context": "dialog header",
"string": "Assign Attribute"
},
"src_dot_products_dot_components_dot_ProductVariantCreateDialog_dot_4120989039": {
"context": "create multiple variants, button",
"string": "Create"
},
"src_dot_products_dot_components_dot_ProductVariantCreateDialog_dot_693960049": {
"string": "SKU"
},
"src_dot_products_dot_components_dot_ProductVariantCreateDialog_dot_705096461": {
"context": "variant creation step",
"string": "Prices and SKU"
},
"src_dot_products_dot_components_dot_ProductVariantCreateDialog_dot_904693740": {
"context": "previous step, button",
"string": "Previous"
},
"src_dot_products_dot_components_dot_ProductVariantCreateDialog_dot_998917294": {
"context": "variant name",
"string": "Variant"
},
"src_dot_products_dot_components_dot_ProductVariantCreatePage_dot_2853608829": {
"context": "button",
"string": "Save variant"
@ -3629,6 +3655,96 @@
"context": "button",
"string": "Delete Variant"
},
"src_dot_products_dot_components_dot_ProductVariantCreatorPage_dot_1134347598": {
"context": "variant price",
"string": "Price"
},
"src_dot_products_dot_components_dot_ProductVariantCreatorPage_dot_1346828628": {
"string": "Selected values will be used to create variants for the configurable product."
},
"src_dot_products_dot_components_dot_ProductVariantCreatorPage_dot_168343345": {
"context": "variant attribute",
"string": "Attribute"
},
"src_dot_products_dot_components_dot_ProductVariantCreatorPage_dot_1808330403": {
"context": "variant attribute",
"string": "Select Attribute"
},
"src_dot_products_dot_components_dot_ProductVariantCreatorPage_dot_2002684673": {
"context": "page title",
"string": "Price and SKUs"
},
"src_dot_products_dot_components_dot_ProductVariantCreatorPage_dot_2020618004": {
"context": "variant stock, header",
"string": "Stock and Warehousing"
},
"src_dot_products_dot_components_dot_ProductVariantCreatorPage_dot_2158649399": {
"context": "product attribute values, page title",
"string": "Choose Values"
},
"src_dot_products_dot_components_dot_ProductVariantCreatorPage_dot_2478977538": {
"context": "attribute values, variant creation step",
"string": "Select Values"
},
"src_dot_products_dot_components_dot_ProductVariantCreatorPage_dot_2670525734": {
"context": "variant attribute",
"string": "Choose attribute"
},
"src_dot_products_dot_components_dot_ProductVariantCreatorPage_dot_2745385064": {
"context": "variant creation step",
"string": "Summary"
},
"src_dot_products_dot_components_dot_ProductVariantCreatorPage_dot_2783195765": {
"string": "Apply single price to all SKUs"
},
"src_dot_products_dot_components_dot_ProductVariantCreatorPage_dot_3387090508": {
"string": "Apply unique stock by attribute to each SKU"
},
"src_dot_products_dot_components_dot_ProductVariantCreatorPage_dot_3570949907": {
"string": "Apply unique prices by attribute to each SKU"
},
"src_dot_products_dot_components_dot_ProductVariantCreatorPage_dot_3601538615": {
"string": "Apply single stock to all SKUs"
},
"src_dot_products_dot_components_dot_ProductVariantCreatorPage_dot_3673120330": {
"context": "button",
"string": "Next"
},
"src_dot_products_dot_components_dot_ProductVariantCreatorPage_dot_3699389906": {
"string": "Here is the summary of variants that will be created. You can change prices, stocks an SKU for each one created."
},
"src_dot_products_dot_components_dot_ProductVariantCreatorPage_dot_3896519183": {
"string": "Skip stock for now"
},
"src_dot_products_dot_components_dot_ProductVariantCreatorPage_dot_4120989039": {
"context": "create multiple variants, button",
"string": "Create"
},
"src_dot_products_dot_components_dot_ProductVariantCreatorPage_dot_520980261": {
"context": "variant creator summary card header",
"string": "Created Variants"
},
"src_dot_products_dot_components_dot_ProductVariantCreatorPage_dot_551319747": {
"string": "Based on your selections we will create {numberOfProducts} products. Use this step to customize price and stocks for your new products"
},
"src_dot_products_dot_components_dot_ProductVariantCreatorPage_dot_693960049": {
"string": "SKU"
},
"src_dot_products_dot_components_dot_ProductVariantCreatorPage_dot_705096461": {
"context": "variant creation step",
"string": "Prices and SKU"
},
"src_dot_products_dot_components_dot_ProductVariantCreatorPage_dot_860603977": {
"string": "Based on your selections we will create 8 products. Use this step to customize price and stocks for your new products."
},
"src_dot_products_dot_components_dot_ProductVariantCreatorPage_dot_904693740": {
"context": "previous step, button",
"string": "Previous"
},
"src_dot_products_dot_components_dot_ProductVariantCreatorPage_dot_998917294": {
"context": "variant name",
"string": "Variant"
},
"src_dot_products_dot_components_dot_ProductVariantDeleteDialog_dot_1583616500": {
"context": "button",
"string": "Delete variant"
@ -3678,23 +3794,12 @@
"src_dot_products_dot_components_dot_ProductVariantPrice_dot_2238565650": {
"string": "Selling price override"
},
"src_dot_products_dot_components_dot_ProductVariantStock_dot_1680952454": {
"context": "variant allocated stock",
"string": "Allocated: {quantity}"
},
"src_dot_products_dot_components_dot_ProductVariantStock_dot_2585918415": {
"string": "SKU (Stock Keeping Unit)"
},
"src_dot_products_dot_components_dot_ProductVariantStock_dot_3490038570": {
"context": "product variant stock",
"string": "Inventory"
},
"src_dot_products_dot_components_dot_ProductVariantStock_dot_3841616483": {
"context": "product variant stock, section header",
"string": "Stock"
"src_dot_products_dot_components_dot_ProductVariants_dot_1001303107": {
"context": "product variant inventory",
"string": "Unavailable in all locations"
},
"src_dot_products_dot_components_dot_ProductVariants_dot_1033175132": {
"context": "product variant status",
"context": "product variant inventory",
"string": "Unavailable"
},
"src_dot_products_dot_components_dot_ProductVariants_dot_1134347598": {
@ -3705,32 +3810,55 @@
"context": "button",
"string": "Create variants"
},
"src_dot_products_dot_components_dot_ProductVariants_dot_1756106276": {
"context": "product variant status",
"string": "Status"
},
"src_dot_products_dot_components_dot_ProductVariants_dot_2153006789": {
"context": "section header",
"string": "Variants"
},
"src_dot_products_dot_components_dot_ProductVariants_dot_2157131639": {
"context": "product variant status",
"string": "Available"
"src_dot_products_dot_components_dot_ProductVariants_dot_2496096212": {
"context": "product variant inventory",
"string": "{numLocations,plural,one{{numAvailable} available at {numLocations} location} other{{numAvailable} available at {numLocations} locations}}"
},
"src_dot_products_dot_components_dot_ProductVariants_dot_277989856": {
"string": "Use variants for products that come in a variety of versions for example different sizes or colors"
},
"src_dot_products_dot_components_dot_ProductVariants_dot_3203451491": {
"context": "filtering option",
"string": "All Warehouses"
},
"src_dot_products_dot_components_dot_ProductVariants_dot_3284706946": {
"context": "variant stock status",
"string": "Available inventoty at:"
},
"src_dot_products_dot_components_dot_ProductVariants_dot_3490038570": {
"context": "product variant inventory status",
"string": "Inventory"
},
"src_dot_products_dot_components_dot_ProductVariants_dot_387066717": {
"context": "product variant inventory",
"string": "{stockQuantity,plural,other{{stockQuantity} available}}"
},
"src_dot_products_dot_components_dot_ProductVariants_dot_3989383405": {
"context": "button",
"string": "Create variant"
},
"src_dot_products_dot_components_dot_ProductVariants_dot_636461959": {
"context": "product variant name",
"string": "Name"
"src_dot_products_dot_components_dot_ProductVariants_dot_4017567521": {
"context": "product variant inventory",
"string": "Not stocked"
},
"src_dot_products_dot_components_dot_ProductVariants_dot_693960049": {
"string": "SKU"
},
"src_dot_products_dot_components_dot_ProductVariants_dot_998917294": {
"context": "product variant name",
"string": "Variant"
},
"src_dot_products_dot_components_dot_ProductWarehousesDialog_dot_3482985373": {
"context": "dialog header",
"string": "Edit Warehouses"
},
"src_dot_products_dot_components_dot_ProductWarehousesDialog_dot_4112406382": {
"string": "Select warehouses that stock selected product"
},
"src_dot_products_dot_views_dot_1542417144": {
"context": "window title",
"string": "Create Product"
@ -3811,6 +3939,14 @@
"context": "dialog header",
"string": "Delete Product"
},
"src_dot_products_dot_views_dot_ProductVariantCreator_dot_2292700443": {
"context": "success message",
"string": "Successfully created variants"
},
"src_dot_products_dot_views_dot_ProductVariantCreator_dot_830692292": {
"context": "window title",
"string": "Create Variants"
},
"src_dot_properties": {
"string": "Properties"
},
@ -4014,6 +4150,13 @@
"src_dot_shipping_dot_components_dot_ShippingWeightUnitForm_dot_549146363": {
"string": "Shipping Weight Unit"
},
"src_dot_shipping_dot_components_dot_ShippingZoneAddWarehouseDialog_dot_2622674857": {
"string": "Warehouse Name"
},
"src_dot_shipping_dot_components_dot_ShippingZoneAddWarehouseDialog_dot_3173942020": {
"context": "header, dialog",
"string": "Create New Warehouse"
},
"src_dot_shipping_dot_components_dot_ShippingZoneCountriesAssignDialog_dot_1003092716": {
"string": "Rest of the World"
},
@ -4184,6 +4327,21 @@
"context": "shipping method name",
"string": "Name"
},
"src_dot_shipping_dot_components_dot_ShippingZoneWarehouses_dot_1221560277": {
"context": "section header",
"string": "Warehouse"
},
"src_dot_shipping_dot_components_dot_ShippingZoneWarehouses_dot_2304484478": {
"context": "button",
"string": "Add New Warehouse"
},
"src_dot_shipping_dot_components_dot_ShippingZoneWarehouses_dot_3852102652": {
"string": "Select warehouse from which you will ship products for this shipping zone. This warehouse address will also be used to calculate taxes."
},
"src_dot_shipping_dot_components_dot_ShippingZoneWarehouses_dot_46197273": {
"context": "input placeholder",
"string": "Select Warehouse"
},
"src_dot_shipping_dot_components_dot_ShippingZonesListPage_dot_1325966144": {
"context": "header",
"string": "Shipping"
@ -4245,34 +4403,6 @@
"context": "site settings section name",
"string": "Site Settings"
},
"src_dot_siteSettings_dot_components_dot_SiteSettingsAddress_dot_1139500589": {
"string": "Country"
},
"src_dot_siteSettings_dot_components_dot_SiteSettingsAddress_dot_1271289966": {
"string": "Phone"
},
"src_dot_siteSettings_dot_components_dot_SiteSettingsAddress_dot_1363074570": {
"string": "Address line 1"
},
"src_dot_siteSettings_dot_components_dot_SiteSettingsAddress_dot_229184360": {
"context": "section header",
"string": "Store Information"
},
"src_dot_siteSettings_dot_components_dot_SiteSettingsAddress_dot_253031977": {
"string": "City"
},
"src_dot_siteSettings_dot_components_dot_SiteSettingsAddress_dot_2965971965": {
"string": "ZIP / Postal code"
},
"src_dot_siteSettings_dot_components_dot_SiteSettingsAddress_dot_3121963259": {
"string": "Address line 2"
},
"src_dot_siteSettings_dot_components_dot_SiteSettingsAddress_dot_3570415321": {
"string": "Company"
},
"src_dot_siteSettings_dot_components_dot_SiteSettingsAddress_dot_944851093": {
"string": "Country area"
},
"src_dot_siteSettings_dot_components_dot_SiteSettingsDetails_dot_1008586926": {
"string": "Name of your store is shown on tab in web browser"
},
@ -4359,6 +4489,10 @@
"src_dot_siteSettings_dot_components_dot_SiteSettingsPage_dot_1214877701": {
"string": "Authentication method defines additional ways that customers can log in to your ecommerce."
},
"src_dot_siteSettings_dot_components_dot_SiteSettingsPage_dot_229184360": {
"context": "section header",
"string": "Store Information"
},
"src_dot_siteSettings_dot_components_dot_SiteSettingsPage_dot_2768400497": {
"context": "section header",
"string": "Company Information"
@ -4980,6 +5114,10 @@
"context": "bulk variant create error",
"string": "SKUs must be unique"
},
"src_dot_utils_dot_errors_dot_slugUnique": {
"context": "error message",
"string": "Slug must be unique for each warehouse"
},
"src_dot_utils_dot_errors_dot_tooCommon": {
"string": "This password is too commonly used"
},
@ -5002,6 +5140,68 @@
"context": "vouchers section name",
"string": "Vouchers"
},
"src_dot_warehouses": {
"context": "warehouses section name",
"string": "Warehouses"
},
"src_dot_warehouses_dot_components_dot_WarehouseCreatePage_dot_1967111456": {
"context": "warehouse",
"string": "Address Information"
},
"src_dot_warehouses_dot_components_dot_WarehouseCreatePage_dot_934572530": {
"context": "header",
"string": "Create Warehouse"
},
"src_dot_warehouses_dot_components_dot_WarehouseDeleteDialog_dot_1355303260": {
"context": "dialog content",
"string": "Are you sure you want to delete {warehouseName}?"
},
"src_dot_warehouses_dot_components_dot_WarehouseDeleteDialog_dot_3857661071": {
"context": "dialog title",
"string": "Delete Warehouse"
},
"src_dot_warehouses_dot_components_dot_WarehouseDetailsPage_dot_1967111456": {
"context": "warehouse",
"string": "Address Information"
},
"src_dot_warehouses_dot_components_dot_WarehouseInfo_dot_2622674857": {
"string": "Warehouse Name"
},
"src_dot_warehouses_dot_components_dot_WarehouseListPage_dot_2304765290": {
"string": "Search Warehouse"
},
"src_dot_warehouses_dot_components_dot_WarehouseListPage_dot_3203451491": {
"context": "tab name",
"string": "All Warehouses"
},
"src_dot_warehouses_dot_components_dot_WarehouseListPage_dot_934572530": {
"context": "button",
"string": "Create Warehouse"
},
"src_dot_warehouses_dot_components_dot_WarehouseList_dot_1225932315": {
"string": "Shipping Zones"
},
"src_dot_warehouses_dot_components_dot_WarehouseList_dot_3787396469": {
"string": "No warehouses found"
},
"src_dot_warehouses_dot_components_dot_WarehouseList_dot_4190792473": {
"string": "Actions"
},
"src_dot_warehouses_dot_components_dot_WarehouseList_dot_636461959": {
"context": "warehouse",
"string": "Name"
},
"src_dot_warehouses_dot_components_dot_WarehouseZones_dot_1225932315": {
"context": "zones that warehouse sends to",
"string": "Shipping Zones"
},
"src_dot_warehouses_dot_components_dot_WarehouseZones_dot_2423245141": {
"string": "This warehouse has no shipping zones assigned."
},
"src_dot_warehouses_dot_views_dot_WarehouseCreate_dot_934572530": {
"context": "header",
"string": "Create Warehouse"
},
"src_dot_webhooks": {
"context": "webhooks section name",
"string": "Webhooks"

60
package-lock.json generated
View file

@ -16,56 +16,56 @@
}
},
"@apollo/react-common": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/@apollo/react-common/-/react-common-3.1.3.tgz",
"integrity": "sha512-Q7ZjDOeqjJf/AOGxUMdGxKF+JVClRXrYBGVq+SuVFqANRpd68MxtVV2OjCWavsFAN0eqYnRqRUrl7vtUCiJqeg==",
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/@apollo/react-common/-/react-common-3.1.4.tgz",
"integrity": "sha512-X5Kyro73bthWSCBJUC5XYQqMnG0dLWuDZmVkzog9dynovhfiVCV4kPSdgSIkqnb++cwCzOVuQ4rDKVwo2XRzQA==",
"requires": {
"ts-invariant": "^0.4.4",
"tslib": "^1.10.0"
}
},
"@apollo/react-components": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/@apollo/react-components/-/react-components-3.1.3.tgz",
"integrity": "sha512-H0l2JKDQMz+LkM93QK7j3ThbNXkWQCduN3s3eKxFN3Rdg7rXsrikJWvx2wQ868jmqy0VhwJbS1vYdRLdh114uQ==",
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/@apollo/react-components/-/react-components-3.1.5.tgz",
"integrity": "sha512-c82VyUuE9VBnJB7bnX+3dmwpIPMhyjMwyoSLyQWPHxz8jK4ak30XszJtqFf4eC4hwvvLYa+Ou6X73Q8V8e2/jg==",
"requires": {
"@apollo/react-common": "^3.1.3",
"@apollo/react-hooks": "^3.1.3",
"@apollo/react-common": "^3.1.4",
"@apollo/react-hooks": "^3.1.5",
"prop-types": "^15.7.2",
"ts-invariant": "^0.4.4",
"tslib": "^1.10.0"
}
},
"@apollo/react-hoc": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/@apollo/react-hoc/-/react-hoc-3.1.3.tgz",
"integrity": "sha512-oCPma0uBVPTcYTR5sOvtMbpaWll4xDBvYfKr6YkDorUcQVeNzFu1LK1kmQjJP64bKsaziKYji5ibFaeCnVptmA==",
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/@apollo/react-hoc/-/react-hoc-3.1.5.tgz",
"integrity": "sha512-jlZ2pvEnRevLa54H563BU0/xrYSgWQ72GksarxUzCHQW85nmn9wQln0kLBX7Ua7SBt9WgiuYQXQVechaaCulfQ==",
"requires": {
"@apollo/react-common": "^3.1.3",
"@apollo/react-components": "^3.1.3",
"@apollo/react-common": "^3.1.4",
"@apollo/react-components": "^3.1.5",
"hoist-non-react-statics": "^3.3.0",
"ts-invariant": "^0.4.4",
"tslib": "^1.10.0"
}
},
"@apollo/react-hooks": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/@apollo/react-hooks/-/react-hooks-3.1.3.tgz",
"integrity": "sha512-reIRO9xKdfi+B4gT/o/hnXuopUnm7WED/ru8VQydPw+C/KG/05Ssg1ZdxFKHa3oxwiTUIDnevtccIH35POanbA==",
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/@apollo/react-hooks/-/react-hooks-3.1.5.tgz",
"integrity": "sha512-y0CJ393DLxIIkksRup4nt+vSjxalbZBXnnXxYbviq/woj+zKa431zy0yT4LqyRKpFy9ahMIwxBnBwfwIoupqLQ==",
"requires": {
"@apollo/react-common": "^3.1.3",
"@apollo/react-common": "^3.1.4",
"@wry/equality": "^0.1.9",
"ts-invariant": "^0.4.4",
"tslib": "^1.10.0"
}
},
"@apollo/react-ssr": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/@apollo/react-ssr/-/react-ssr-3.1.3.tgz",
"integrity": "sha512-fUTmEYHxSTX1GA43B8vICxXXplpcEBnDwn0IgdAc3eG0p2YK97ZrJDRFCJ5vD7fyDZsrYhMf+rAI3sd+H2SS+A==",
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/@apollo/react-ssr/-/react-ssr-3.1.5.tgz",
"integrity": "sha512-wuLPkKlctNn3u8EU8rlECyktpOUCeekFfb0KhIKknpGY6Lza2Qu0bThx7D9MIbVEzhKadNNrzLcpk0Y8/5UuWg==",
"requires": {
"@apollo/react-common": "^3.1.3",
"@apollo/react-hooks": "^3.1.3",
"@apollo/react-common": "^3.1.4",
"@apollo/react-hooks": "^3.1.5",
"tslib": "^1.10.0"
}
},
@ -17593,15 +17593,15 @@
}
},
"react-apollo": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/react-apollo/-/react-apollo-3.1.3.tgz",
"integrity": "sha512-orCZNoAkgveaK5b75y7fw1MSqSHOU/Wuu9rRFOGmRQBSQVZjvV4DI+hj604rHmuN9+WDABxb5W48wTa0F/xNZQ==",
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/react-apollo/-/react-apollo-3.1.5.tgz",
"integrity": "sha512-xOxMqxORps+WHrUYbjVHPliviomefOpu5Sh35oO3osuOyPTxvrljdfTLGCggMhcXBsDljtS5Oy4g+ijWg3D4JQ==",
"requires": {
"@apollo/react-common": "^3.1.3",
"@apollo/react-components": "^3.1.3",
"@apollo/react-hoc": "^3.1.3",
"@apollo/react-hooks": "^3.1.3",
"@apollo/react-ssr": "^3.1.3"
"@apollo/react-common": "^3.1.4",
"@apollo/react-components": "^3.1.5",
"@apollo/react-hoc": "^3.1.5",
"@apollo/react-hooks": "^3.1.5",
"@apollo/react-ssr": "^3.1.5"
}
},
"react-clientside-effect": {

View file

@ -48,7 +48,7 @@
"moment-timezone": "^0.5.26",
"qs": "^6.9.0",
"react": "^16.12.0",
"react-apollo": "^3.1.3",
"react-apollo": "^3.1.4",
"react-dom": "^16.9.0",
"react-dropzone": "^8.2.0",
"react-error-boundary": "^1.2.5",

File diff suppressed because it is too large Load diff

View file

@ -172,7 +172,7 @@ export const CategoryDetails: React.FC<CategoryDetailsProps> = ({
disabled={loading}
errors={updateResult.data?.categoryUpdate.errors || []}
onAddCategory={() => navigate(categoryAddUrl(id))}
onAddProduct={() => navigate(productAddUrl)}
onAddProduct={() => navigate(productAddUrl())}
onBack={() =>
navigate(
maybe(

View file

@ -0,0 +1,197 @@
import { makeStyles } from "@material-ui/core/styles";
import TextField from "@material-ui/core/TextField";
import React from "react";
import { useIntl, IntlShape } from "react-intl";
import FormSpacer from "@saleor/components/FormSpacer";
import Grid from "@saleor/components/Grid";
import SingleAutocompleteSelectField, {
SingleAutocompleteChoiceType
} from "@saleor/components/SingleAutocompleteSelectField";
import { AddressTypeInput } from "@saleor/customers/types";
import { ChangeEvent } from "@saleor/hooks/useForm";
import getShopErrorMessage from "@saleor/utils/errors/shop";
import { getFormErrors } from "@saleor/utils/errors";
import { ShopErrorFragment } from "@saleor/siteSettings/types/ShopErrorFragment";
import { AccountErrorFragment } from "@saleor/customers/types/AccountErrorFragment";
import getAccountErrorMessage from "@saleor/utils/errors/account";
import getWarehouseErrorMessage from "@saleor/utils/errors/warehouse";
import { WarehouseErrorFragment } from "@saleor/warehouses/types/WarehouseErrorFragment";
export interface CompanyAddressFormProps {
countries: SingleAutocompleteChoiceType[];
data: AddressTypeInput;
displayCountry: string;
errors: Array<
AccountErrorFragment | ShopErrorFragment | WarehouseErrorFragment
>;
disabled: boolean;
onChange: (event: ChangeEvent) => void;
onCountryChange: (event: ChangeEvent) => void;
}
const useStyles = makeStyles(
{
root: {}
},
{ name: "CompanyAddressForm" }
);
function getErrorMessage(
err: AccountErrorFragment | ShopErrorFragment | WarehouseErrorFragment,
intl: IntlShape
): string {
switch (err?.__typename) {
case "AccountError":
return getAccountErrorMessage(err, intl);
case "WarehouseError":
return getWarehouseErrorMessage(err, intl);
default:
return getShopErrorMessage(err, intl);
}
}
const CompanyAddressForm: React.FC<CompanyAddressFormProps> = props => {
const {
countries,
data,
disabled,
displayCountry,
errors,
onChange,
onCountryChange
} = props;
const classes = useStyles(props);
const intl = useIntl();
const formFields = [
"companyName",
"streetAddress1",
"streetAddress2",
"city",
"postalCode",
"country",
"companyArea",
"phone"
];
const formErrors = getFormErrors(formFields, errors);
return (
<div className={classes.root}>
<TextField
disabled={disabled}
error={!!formErrors.companyName}
helperText={getErrorMessage(formErrors.companyName, intl)}
label={intl.formatMessage({
defaultMessage: "Company"
})}
name={"companyName" as keyof AddressTypeInput}
onChange={onChange}
value={data.companyName}
fullWidth
/>
<FormSpacer />
<TextField
disabled={disabled}
error={!!formErrors.streetAddress1}
helperText={getErrorMessage(formErrors.streetAddress1, intl)}
label={intl.formatMessage({
defaultMessage: "Address line 1"
})}
name={"streetAddress1" as keyof AddressTypeInput}
onChange={onChange}
value={data.streetAddress1}
fullWidth
/>
<FormSpacer />
<TextField
disabled={disabled}
error={!!formErrors.streetAddress2}
helperText={getErrorMessage(formErrors.streetAddress2, intl)}
label={intl.formatMessage({
defaultMessage: "Address line 2"
})}
name={"streetAddress2" as keyof AddressTypeInput}
onChange={onChange}
value={data.streetAddress2}
fullWidth
/>
<FormSpacer />
<Grid>
<TextField
disabled={disabled}
error={!!formErrors.city}
helperText={getErrorMessage(formErrors.city, intl)}
label={intl.formatMessage({
defaultMessage: "City"
})}
name={"city" as keyof AddressTypeInput}
onChange={onChange}
value={data.city}
fullWidth
/>
<TextField
disabled={disabled}
error={!!formErrors.postalCode}
helperText={getErrorMessage(formErrors.postalCode, intl)}
label={intl.formatMessage({
defaultMessage: "ZIP / Postal code"
})}
name={"postalCode" as keyof AddressTypeInput}
onChange={onChange}
value={data.postalCode}
fullWidth
/>
</Grid>
<FormSpacer />
<Grid>
<SingleAutocompleteSelectField
disabled={disabled}
displayValue={displayCountry}
error={!!formErrors.country}
helperText={getErrorMessage(formErrors.country, intl)}
label={intl.formatMessage({
defaultMessage: "Country"
})}
name={"country" as keyof AddressTypeInput}
onChange={onCountryChange}
value={data.country}
choices={countries}
InputProps={{
inputProps: {
autocomplete: "plsdontautocomplete" // Somehow it shuts it down
}
}}
/>
<TextField
disabled={disabled}
error={!!formErrors.countryArea}
helperText={getErrorMessage(formErrors.countryArea, intl)}
label={intl.formatMessage({
defaultMessage: "Country area"
})}
name={"countryArea" as keyof AddressTypeInput}
onChange={onChange}
value={data.countryArea}
fullWidth
/>
</Grid>
<FormSpacer />
<TextField
disabled={disabled}
error={!!formErrors.phone}
fullWidth
helperText={getErrorMessage(formErrors.phone, intl)}
label={intl.formatMessage({
defaultMessage: "Phone"
})}
name={"phone" as keyof AddressTypeInput}
value={data.phone}
onChange={onChange}
/>
</div>
);
};
CompanyAddressForm.displayName = "CompanyAddressForm";
export default CompanyAddressForm;

View file

@ -0,0 +1,40 @@
import Card from "@material-ui/core/Card";
import CardContent from "@material-ui/core/CardContent";
import { makeStyles } from "@material-ui/core/styles";
import React from "react";
import classNames from "classnames";
import CardTitle from "../CardTitle";
import CompanyAddressForm, {
CompanyAddressFormProps
} from "./CompanyAddressForm";
interface CompanyAddressInputProps extends CompanyAddressFormProps {
className?: string;
header: string;
}
const useStyles = makeStyles(
{
root: {
overflow: "visible"
}
},
{ name: "CompanyAddressInput" }
);
const CompanyAddressInput: React.FC<CompanyAddressInputProps> = props => {
const { className, header, ...formProps } = props;
const classes = useStyles(props);
return (
<Card className={classNames(classes.root, className)}>
<CardTitle title={header} />
<CardContent>
<CompanyAddressForm {...formProps} />
</CardContent>
</Card>
);
};
CompanyAddressInput.displayName = "CompanyAddressInput";
export default CompanyAddressInput;

View file

@ -0,0 +1,2 @@
export { default } from "./CompanyAddressInput";
export * from "./CompanyAddressInput";

View file

@ -25,6 +25,7 @@ export const ControlledCheckbox: React.FC<ControlledCheckboxProps> = ({
control={
<Checkbox
checked={checked}
disabled={disabled}
name={name}
disableClickPropagation
onChange={() => onChange({ target: { name, value: !checked } })}

View file

@ -0,0 +1,26 @@
import { storiesOf } from "@storybook/react";
import React from "react";
import { countries } from "@saleor/fixtures";
import CardDecorator from "@saleor/storybook/CardDecorator";
import Decorator from "@saleor/storybook/Decorator";
import Form from "../Form";
import LinkChoice, { LinkChoiceProps } from "./LinkChoice";
const suggestions = countries.map(c => ({ label: c.name, value: c.code }));
const props: Omit<LinkChoiceProps, "value" | "onChange"> = {
choices: suggestions.slice(0, 10),
name: "country"
};
storiesOf("Generics / Link with choices", module)
.addDecorator(CardDecorator)
.addDecorator(Decorator)
.add("default", () => (
<Form initial={{ country: suggestions[1].value }}>
{({ change, data }) => (
<LinkChoice {...props} value={data.country} onChange={change} />
)}
</Form>
));

View file

@ -0,0 +1,159 @@
import React from "react";
import ClickAwayListener from "@material-ui/core/ClickAwayListener";
import Paper from "@material-ui/core/Paper";
import MenuItem from "@material-ui/core/MenuItem";
import makeStyles from "@material-ui/core/styles/makeStyles";
import Popper from "@material-ui/core/Popper";
import { fade } from "@material-ui/core/styles/colorManipulator";
import classNames from "classnames";
import { codes } from "keycode";
import ArrowDropdown from "@saleor/icons/ArrowDropdown";
import { FormChange } from "@saleor/hooks/useForm";
import { SingleAutocompleteChoiceType } from "../SingleAutocompleteSelectField";
import Link from "../Link";
const useStyles = makeStyles(
theme => ({
arrow: {
position: "relative",
top: 6,
transition: theme.transitions.duration.short + "ms"
},
highlighted: {
background: theme.palette.background.default
},
menuItem: {
"&:not(:last-of-type)": {
marginBottom: theme.spacing()
}
},
paper: {
padding: theme.spacing()
},
popper: {
boxShadow: `0px 5px 10px 0 ${fade(theme.palette.common.black, 0.05)}`,
marginTop: theme.spacing(1),
zIndex: 2
},
root: {
"&:focus": {
textDecoration: "underline"
},
outline: 0,
position: "relative"
},
rotate: {
transform: "rotate(180deg)"
}
}),
{
name: "LinkChoice"
}
);
export interface LinkChoiceProps {
className?: string;
choices: SingleAutocompleteChoiceType[];
name?: string;
value: string;
onChange: FormChange;
}
const LinkChoice: React.FC<LinkChoiceProps> = ({
className,
choices,
name,
value,
onChange
}) => {
const classes = useStyles({});
const [open, setOpen] = React.useState(false);
const anchor = React.useRef<HTMLInputElement>(null);
const current = choices.find(c => c.value === value);
const [highlightedIndex, setHighlightedIndex] = React.useState(0);
const handleChange = (value: string) => {
setOpen(false);
onChange({
target: {
name,
value
}
});
};
const handleKeyPress = (event: React.KeyboardEvent<HTMLSpanElement>) => {
switch (event.keyCode) {
case codes.down:
setHighlightedIndex(
highlightedIndex => (highlightedIndex + 1) % choices.length
);
break;
case codes.up:
setHighlightedIndex(highlightedIndex =>
highlightedIndex === 0
? choices.length - 1
: (highlightedIndex - 1) % choices.length
);
break;
case codes.enter:
if (open) {
handleChange(choices[highlightedIndex].value);
} else {
setOpen(true);
}
break;
}
};
return (
<span
className={classNames(classes.root, className)}
ref={anchor}
onKeyDown={handleKeyPress}
tabIndex={0}
>
<Link onClick={() => setOpen(open => !open)}>{current.label}</Link>
<ArrowDropdown
className={classNames(classes.arrow, {
[classes.rotate]: open
})}
color="primary"
/>
<Popper
className={classes.popper}
open={open}
anchorEl={anchor.current}
transition
disablePortal
placement="bottom-start"
>
<ClickAwayListener
onClickAway={() => setOpen(false)}
mouseEvent="onClick"
>
<Paper className={classes.paper}>
{choices.map((choice, choiceIndex) => (
<MenuItem
className={classNames(classes.menuItem, {
[classes.highlighted]: highlightedIndex === choiceIndex
})}
selected={choice.value === value}
key={choice.value}
onClick={() => handleChange(choice.value)}
data-tc="select-option"
>
{choice.label}
</MenuItem>
))}
</Paper>
</ClickAwayListener>
</Popper>
</span>
);
};
LinkChoice.displayName = "LinkChoice";
export default LinkChoice;

View file

@ -0,0 +1,2 @@
export { default } from "./LinkChoice";
export * from "./LinkChoice";

View file

@ -12,7 +12,8 @@ import Debounce, { DebounceProps } from "@saleor/components/Debounce";
import ArrowDropdownIcon from "@saleor/icons/ArrowDropdown";
import { FetchMoreProps } from "@saleor/types";
import MultiAutocompleteSelectFieldContent, {
MultiAutocompleteChoiceType
MultiAutocompleteChoiceType,
MultiAutocompleteActionType
} from "./MultiAutocompleteSelectFieldContent";
const useStyles = makeStyles(
@ -71,6 +72,7 @@ const useStyles = makeStyles(
export interface MultiAutocompleteSelectFieldProps
extends Partial<FetchMoreProps> {
add?: MultiAutocompleteActionType;
allowCustomValues?: boolean;
displayValues: MultiAutocompleteChoiceType[];
error?: boolean;
@ -91,6 +93,7 @@ const DebounceAutocomplete: React.ComponentType<DebounceProps<
const MultiAutocompleteSelectFieldComponent: React.FC<MultiAutocompleteSelectFieldProps> = props => {
const {
add,
allowCustomValues,
choices,
displayValues,
@ -129,6 +132,7 @@ const MultiAutocompleteSelectFieldComponent: React.FC<MultiAutocompleteSelectFie
itemToString={() => ""}
>
{({
closeMenu,
getInputProps,
getItemProps,
isOpen,
@ -166,6 +170,13 @@ const MultiAutocompleteSelectFieldComponent: React.FC<MultiAutocompleteSelectFie
/>
{isOpen && (!!inputValue || !!choices.length) && (
<MultiAutocompleteSelectFieldContent
add={{
...add,
onClick: () => {
add.onClick();
closeMenu();
}
}}
choices={choices.filter(
choice => !value.includes(choice.value)
)}

View file

@ -7,6 +7,7 @@ import { FormattedMessage } from "react-intl";
import chevronDown from "@assets/images/ChevronDown.svg";
import CircularProgress from "@material-ui/core/CircularProgress";
import MenuItem from "@material-ui/core/MenuItem";
import Typography from "@material-ui/core/Typography";
import Paper from "@material-ui/core/Paper";
import { makeStyles } from "@material-ui/core/styles";
import AddIcon from "@material-ui/icons/Add";
@ -22,6 +23,10 @@ const menuItemHeight = 46;
const maxMenuItems = 5;
const offset = 24;
export interface MultiAutocompleteActionType {
label: string;
onClick: () => void;
}
export interface MultiAutocompleteChoiceType {
label: string;
value: any;
@ -29,6 +34,7 @@ export interface MultiAutocompleteChoiceType {
}
export interface MultiAutocompleteSelectFieldContentProps
extends Partial<FetchMoreProps> {
add?: MultiAutocompleteActionType;
choices: MultiAutocompleteChoiceType[];
displayCustomValue: boolean;
displayValues: MultiAutocompleteChoiceType[];
@ -39,6 +45,14 @@ export interface MultiAutocompleteSelectFieldContentProps
const useStyles = makeStyles(
theme => ({
add: {
background: theme.palette.background.default,
border: `1px solid ${theme.palette.divider}`,
borderRadius: "100%",
height: 24,
margin: theme.spacing(),
width: 24
},
addIcon: {
height: 24,
margin: 9,
@ -100,6 +114,7 @@ const useStyles = makeStyles(
gridColumnGap: theme.spacing(1),
gridTemplateColumns: "30px 1fr",
height: "auto",
marginBottom: theme.spacing(0.5),
padding: 0,
whiteSpace: "normal"
},
@ -130,10 +145,11 @@ const useStyles = makeStyles(
function getChoiceIndex(
index: number,
displayValues: MultiAutocompleteChoiceType[],
displayCustomValue: boolean
displayCustomValue: boolean,
add: boolean
) {
let choiceIndex = index;
if (displayCustomValue) {
if (add || displayCustomValue) {
choiceIndex += 2;
}
if (displayValues.length > 0) {
@ -145,6 +161,7 @@ function getChoiceIndex(
const MultiAutocompleteSelectFieldContent: React.FC<MultiAutocompleteSelectFieldContentProps> = props => {
const {
add,
choices,
displayCustomValue,
displayValues,
@ -156,6 +173,10 @@ const MultiAutocompleteSelectFieldContent: React.FC<MultiAutocompleteSelectField
onFetchMore
} = props;
if (!!add && !!displayCustomValue) {
throw new Error("Add and custom value cannot be displayed simultaneously");
}
const classes = useStyles(props);
const anchor = React.useRef<HTMLDivElement>();
const scrollPosition = useElementScroll(anchor);
@ -183,6 +204,20 @@ const MultiAutocompleteSelectFieldContent: React.FC<MultiAutocompleteSelectField
displayValues.length > 0 ||
displayCustomValue ? (
<>
{add && (
<MenuItem
className={classes.menuItem}
component="div"
{...getItemProps({
item: inputValue
})}
data-tc="multiautocomplete-select-option-add"
onClick={add.onClick}
>
<AddIcon color="primary" className={classes.addIcon} />
<Typography color="primary">{add.label}</Typography>
</MenuItem>
)}
{displayCustomValue && (
<MenuItem
className={classes.menuItem}
@ -233,7 +268,8 @@ const MultiAutocompleteSelectFieldContent: React.FC<MultiAutocompleteSelectField
const choiceIndex = getChoiceIndex(
index,
displayValues,
displayCustomValue
displayCustomValue,
!!add
);
return (

View file

@ -46,7 +46,7 @@ export function searchInCommands(
{
label: intl.formatMessage(messages.createProduct),
onClick: () => {
navigate(productAddUrl);
navigate(productAddUrl());
return false;
}
},

View file

@ -21,6 +21,7 @@ import { staffListUrl } from "@saleor/staff/urls";
import { countryListUrl } from "@saleor/taxes/urls";
import { languageListUrl } from "@saleor/translations/urls";
import { webhookListUrl } from "@saleor/webhooks/urls";
import { warehouseListUrl } from "@saleor/warehouses/urls";
import { QuickSearchActionInput } from "../../types";
interface View {
@ -116,6 +117,10 @@ function searchInViews(
{
label: intl.formatMessage(sectionNames.webhooks),
url: webhookListUrl()
},
{
label: intl.formatMessage(sectionNames.warehouses),
url: warehouseListUrl()
}
];

View file

@ -28,7 +28,7 @@ interface PageHeaderProps {
children?: React.ReactNode;
className?: string;
inline?: boolean;
title?: string;
title?: React.ReactNode;
}
const PageHeader: React.FC<PageHeaderProps> = props => {

View file

@ -69,6 +69,7 @@ const Story: React.FC<Partial<
};
const contentProps: SingleAutocompleteSelectFieldContentProps = {
add: undefined,
choices: suggestions.slice(0, 10),
displayCustomValue: false,
emptyOption: false,
@ -88,6 +89,15 @@ storiesOf("Generics / Select with autocomplete", module)
.add("default", () => (
<SingleAutocompleteSelectFieldContent {...contentProps} />
))
.add("with add", () => (
<SingleAutocompleteSelectFieldContent
{...contentProps}
add={{
label: "Add New Collection",
onClick: () => undefined
}}
/>
))
.add("can load more", () => (
<SingleAutocompleteSelectFieldContent {...contentProps} hasMore={true} />
))

View file

@ -10,7 +10,8 @@ import { FetchMoreProps } from "@saleor/types";
import ArrowDropdownIcon from "../../icons/ArrowDropdown";
import Debounce, { DebounceProps } from "../Debounce";
import SingleAutocompleteSelectFieldContent, {
SingleAutocompleteChoiceType
SingleAutocompleteChoiceType,
SingleAutocompleteActionType
} from "./SingleAutocompleteSelectFieldContent";
const useStyles = makeStyles(
@ -25,6 +26,7 @@ const useStyles = makeStyles(
export interface SingleAutocompleteSelectFieldProps
extends Partial<FetchMoreProps> {
add?: SingleAutocompleteActionType;
error?: boolean;
name: string;
displayValue: string;
@ -47,6 +49,7 @@ const DebounceAutocomplete: React.ComponentType<DebounceProps<
const SingleAutocompleteSelectFieldComponent: React.FC<SingleAutocompleteSelectFieldProps> = props => {
const {
add,
allowCustomValues,
choices,
disabled,
@ -144,6 +147,15 @@ const SingleAutocompleteSelectFieldComponent: React.FC<SingleAutocompleteSelectF
/>
{isOpen && (!!inputValue || !!choices.length) && (
<SingleAutocompleteSelectFieldContent
add={
!!add && {
...add,
onClick: () => {
add.onClick();
closeMenu();
}
}
}
choices={choices}
displayCustomValue={displayCustomValue}
emptyOption={emptyOption}

View file

@ -1,5 +1,6 @@
import CircularProgress from "@material-ui/core/CircularProgress";
import MenuItem from "@material-ui/core/MenuItem";
import Add from "@material-ui/icons/Add";
import Paper from "@material-ui/core/Paper";
import { makeStyles } from "@material-ui/core/styles";
import Typography from "@material-ui/core/Typography";
@ -24,8 +25,13 @@ export interface SingleAutocompleteChoiceType {
label: string;
value: any;
}
export interface SingleAutocompleteActionType {
label: string;
onClick: () => void;
}
export interface SingleAutocompleteSelectFieldContentProps
extends Partial<FetchMoreProps> {
add?: SingleAutocompleteActionType;
choices: SingleAutocompleteChoiceType[];
displayCustomValue: boolean;
emptyOption: boolean;
@ -38,6 +44,14 @@ export interface SingleAutocompleteSelectFieldContentProps
const useStyles = makeStyles(
theme => ({
add: {
background: theme.palette.background.default,
border: `1px solid ${theme.palette.divider}`,
borderRadius: "100%",
height: 24,
marginRight: theme.spacing(),
width: 24
},
arrowContainer: {
position: "relative"
},
@ -96,23 +110,23 @@ const useStyles = makeStyles(
function getChoiceIndex(
index: number,
emptyValue: boolean,
customValue: boolean
customValue: boolean,
add: boolean
) {
let choiceIndex = index;
if (emptyValue) {
choiceIndex += 1;
}
if (customValue) {
if (customValue || add) {
choiceIndex += 2;
}
return choiceIndex;
}
const SingleAutocompleteSelectFieldContent: React.FC<
SingleAutocompleteSelectFieldContentProps
> = props => {
const SingleAutocompleteSelectFieldContent: React.FC<SingleAutocompleteSelectFieldContentProps> = props => {
const {
add,
choices,
displayCustomValue,
emptyOption,
@ -125,6 +139,10 @@ const SingleAutocompleteSelectFieldContent: React.FC<
onFetchMore
} = props;
if (!!add && !!displayCustomValue) {
throw new Error("Add and custom value cannot be displayed simultaneously");
}
const classes = useStyles(props);
const anchor = React.useRef<HTMLDivElement>();
const scrollPosition = useElementScroll(anchor);
@ -164,6 +182,20 @@ const SingleAutocompleteSelectFieldContent: React.FC<
</Typography>
</MenuItem>
)}
{add && (
<MenuItem
className={classes.menuItem}
component="div"
{...getItemProps({
item: inputValue
})}
data-tc="singleautocomplete-select-option-add"
onClick={add.onClick}
>
<Add color="primary" className={classes.add} />
<Typography color="primary">{add.label}</Typography>
</MenuItem>
)}
{displayCustomValue && (
<MenuItem
className={classes.menuItem}
@ -184,14 +216,15 @@ const SingleAutocompleteSelectFieldContent: React.FC<
/>
</MenuItem>
)}
{choices.length > 0 && displayCustomValue && (
{choices.length > 0 && (!!add || displayCustomValue) && (
<Hr className={classes.hr} />
)}
{choices.map((suggestion, index) => {
const choiceIndex = getChoiceIndex(
index,
emptyOption,
displayCustomValue
displayCustomValue,
!!add
);
return (

View file

@ -34,6 +34,7 @@ export interface AppListViewSettings {
[ListViews.STAFF_MEMBERS_LIST]: ListSettings;
[ListViews.PERMISSION_GROUP_LIST]: ListSettings;
[ListViews.VOUCHER_LIST]: ListSettings;
[ListViews.WAREHOUSE_LIST]: ListSettings;
[ListViews.WEBHOOK_LIST]: ListSettings;
}
export const defaultListSettings: AppListViewSettings = {
@ -80,6 +81,9 @@ export const defaultListSettings: AppListViewSettings = {
[ListViews.VOUCHER_LIST]: {
rowNumber: PAGINATE_BY
},
[ListViews.WAREHOUSE_LIST]: {
rowNumber: PAGINATE_BY
},
[ListViews.WEBHOOK_LIST]: {
rowNumber: PAGINATE_BY
}

View file

@ -31,6 +31,8 @@ import { permissionGroupListUrl } from "@saleor/permissionGroups/urls";
import { taxSection } from "@saleor/taxes/urls";
import { PermissionEnum } from "@saleor/types/globalTypes";
import { webhookListUrl } from "@saleor/webhooks/urls";
import Warehouses from "@saleor/icons/Warehouses";
import { warehouseSection } from "@saleor/warehouses/urls";
import ConfigurationPage, { MenuSection } from "./ConfigurationPage";
export function createConfigurationMenu(intl: IntlShape): MenuSection[] {
@ -67,16 +69,6 @@ export function createConfigurationMenu(intl: IntlShape): MenuSection[] {
defaultMessage: "Product Settings"
}),
menuItems: [
{
description: intl.formatMessage({
defaultMessage: "Manage how you ship out orders",
id: "configurationMenuShipping"
}),
icon: <ShippingMethods fontSize="inherit" viewBox="0 0 44 44" />,
permission: PermissionEnum.MANAGE_SHIPPING,
title: intl.formatMessage(sectionNames.shipping),
url: shippingZonesListUrl()
},
{
description: intl.formatMessage({
defaultMessage: "Manage how your store charges tax",
@ -117,6 +109,33 @@ export function createConfigurationMenu(intl: IntlShape): MenuSection[] {
}
]
},
{
label: intl.formatMessage({
defaultMessage: "Product Settings"
}),
menuItems: [
{
description: intl.formatMessage({
defaultMessage: "Manage how you ship out orders",
id: "configurationMenuShipping"
}),
icon: <ShippingMethods fontSize="inherit" viewBox="0 0 44 44" />,
permission: PermissionEnum.MANAGE_SHIPPING,
title: intl.formatMessage(sectionNames.shipping),
url: shippingZonesListUrl()
},
{
description: intl.formatMessage({
defaultMessage: "Manage and update your warehouse information",
id: "configurationMenuWarehouses"
}),
icon: <Warehouses fontSize="inherit" viewBox="0 0 44 44" />,
permission: PermissionEnum.MANAGE_PRODUCTS,
title: intl.formatMessage(sectionNames.warehouses),
url: warehouseSection
}
]
},
{
label: intl.formatMessage({
defaultMessage: "Miscellaneous"

View file

@ -485,3 +485,23 @@ export const adminUserPermissions: User_userPermissions[] = [
name: "Manage customers."
}
];
export const address = {
__typename: "Address" as "Address",
city: "Port Danielshire",
cityArea: "",
companyName: "",
country: {
__typename: "CountryDisplay" as "CountryDisplay",
code: "SE",
country: "Szwecja"
},
countryArea: "",
firstName: "Elizabeth",
id: "QWRkcmVzczoy",
lastName: "Vaughn",
phone: "",
postalCode: "52203",
streetAddress1: "419 Ruiz Orchard Apt. 199",
streetAddress2: "0238 Cremin Freeway"
};

View file

@ -18,8 +18,8 @@ export interface UseFormsetOutput<TData = object, TValue = any> {
set: (data: FormsetData<TData, TValue>) => void;
}
function useFormset<TData = object, TValue = any>(
initial: FormsetData<TData>
): UseFormsetOutput<TData> {
initial: FormsetData<TData, TValue>
): UseFormsetOutput<TData, TValue> {
const [data, setData] = useStateFromProps<FormsetData<TData, TValue>>(
initial || []
);

View file

@ -1,3 +1,4 @@
import { useEffect } from "react";
import useLocalStorage from "@saleor/hooks/useLocalStorage";
import { AppListViewSettings, defaultListSettings } from "./../config";
import { ListSettings, ListViews } from "./../types";
@ -14,6 +15,15 @@ export default function useListSettings<TColumns extends string = string>(
defaultListSettings
);
useEffect(() => {
if (settings[listName] === undefined) {
setListSettings(settings => ({
...settings,
[listName]: defaultListSettings[listName]
}));
}
}, []);
const updateListSettings = (key: keyof ListSettings, value: any) =>
setListSettings(settings => ({
...settings,

61
src/hooks/useWizard.ts Normal file
View file

@ -0,0 +1,61 @@
import { useState } from "react";
export interface UseWizardActions<T> {
next: () => void;
prev: () => void;
set: (step: T) => void;
}
export interface UseWizardOpts<T> {
onTransition: (prevStep: T, nextStep: T) => void;
}
export type UseWizard<T> = [T, UseWizardActions<T>];
function useWizard<T>(
initial: T,
steps: T[],
opts?: UseWizardOpts<T>
): UseWizard<T> {
const [stepIndex, setStepIndex] = useState(steps.indexOf(initial));
function goToStep(nextStepIndex) {
if (typeof opts?.onTransition === "function") {
opts.onTransition(steps[stepIndex], steps[nextStepIndex]);
}
setStepIndex(nextStepIndex);
}
function next() {
if (stepIndex === steps.length - 1) {
console.error("This is the last step");
} else {
goToStep(stepIndex + 1);
}
}
function prev() {
if (stepIndex === 0) {
console.error("This is the first step");
} else {
goToStep(stepIndex - 1);
}
}
function set(step: T) {
const newStepIndex = steps.findIndex(s => s === step);
if (newStepIndex === -1) {
console.error("Step does not exist");
} else {
goToStep(newStepIndex);
}
}
return [
steps[stepIndex],
{
next,
prev,
set
}
];
}
export default useWizard;

16
src/icons/Warehouses.tsx Normal file
View file

@ -0,0 +1,16 @@
import createSvgIcon from "@material-ui/icons/utils/createSvgIcon";
import React from "react";
const Warehouses = createSvgIcon(
<>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M5.23438 15.2344H34.7656V38.4375H38.2812V12.3121L19.9219 1.80048L1.5625 12.3121V38.4375H5.23438V15.2344ZM6.79688 40H0V11.4062L19.9219 0L39.8438 11.4062V40H33.2031V16.7969H6.79688V40ZM23.8281 9.84375H16.1719V11.7188H23.8281V9.84375ZM14.6094 8.28125V13.2812H25.3906V8.28125H14.6094ZM15.8594 32.3438H21.6406V38.4375H15.8594V32.3438ZM21.6406 30.7812V23.125H30.5469V30.7812V32.3438V40H23.2031H21.6406H14.2969V30.7812H21.6406ZM28.9844 32.3438H23.2031V38.4375H28.9844V32.3438ZM28.9844 30.7812V24.6875H23.2031V30.7812H28.9844Z"
fill="#06847B"
/>
</>,
"Warehouses"
);
export default Warehouses;

View file

@ -55,6 +55,8 @@ import TaxesSection from "./taxes";
import TranslationsSection from "./translations";
import { PermissionEnum } from "./types/globalTypes";
import WebhooksSection from "./webhooks";
import { warehouseSection } from "./warehouses/urls";
import WarehouseSection from "./warehouses";
interface ResponseError extends ErrorResponse {
networkError?: Error & {
@ -263,6 +265,11 @@ const Routes: React.FC = () => {
path={serviceSection}
component={ServiceSection}
/>
<SectionRoute
permissions={[PermissionEnum.MANAGE_PRODUCTS]}
path={warehouseSection}
component={WarehouseSection}
/>
{createConfigurationMenu(intl).filter(menu =>
menu.menuItems.map(item =>
hasPermission(item.permission, user)

View file

@ -81,6 +81,10 @@ export const commonMessages = defineMessages({
});
export const buttonMessages = defineMessages({
accept: {
defaultMessage: "Accept",
description: "button"
},
back: {
defaultMessage: "Back",
description: "button"
@ -117,6 +121,10 @@ export const buttonMessages = defineMessages({
defaultMessage: "Manage",
description: "button"
},
ok: {
defaultMessage: "OK",
description: "button"
},
remove: {
defaultMessage: "Remove",
description: "button"
@ -224,6 +232,10 @@ export const sectionNames = defineMessages({
defaultMessage: "Vouchers",
description: "vouchers section name"
},
warehouses: {
defaultMessage: "Warehouses",
description: "warehouses section name"
},
webhooks: {
defaultMessage: "Webhooks",
description: "webhooks section name"

View file

@ -4,14 +4,13 @@ import { FormattedMessage, useIntl } from "react-intl";
import ActionDialog from "@saleor/components/ActionDialog";
import { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton";
import ControlledCheckbox from "@saleor/components/ControlledCheckbox";
export interface OrderBulkCancelDialogProps {
confirmButtonState: ConfirmButtonTransitionState;
numberOfOrders: string;
open: boolean;
onClose: () => void;
onConfirm: (restock: boolean) => void;
onConfirm: () => void;
}
const OrderBulkCancelDialog: React.FC<OrderBulkCancelDialogProps> = ({
@ -22,7 +21,6 @@ const OrderBulkCancelDialog: React.FC<OrderBulkCancelDialogProps> = ({
onConfirm
}) => {
const intl = useIntl();
const [restock, setRestock] = React.useState(true);
return (
<ActionDialog
@ -34,7 +32,7 @@ const OrderBulkCancelDialog: React.FC<OrderBulkCancelDialogProps> = ({
description: "dialog header"
})}
onClose={onClose}
onConfirm={() => onConfirm(restock)}
onConfirm={onConfirm}
>
<DialogContentText>
<FormattedMessage
@ -45,15 +43,6 @@ const OrderBulkCancelDialog: React.FC<OrderBulkCancelDialogProps> = ({
}}
/>
</DialogContentText>
<ControlledCheckbox
checked={restock}
label={intl.formatMessage({
defaultMessage: "Release all stock allocated to these orders",
description: "switch button"
})}
name="restock"
onChange={event => setRestock(event.target.value)}
/>
</ActionDialog>
);
};

View file

@ -4,45 +4,25 @@ import DialogActions from "@material-ui/core/DialogActions";
import DialogContent from "@material-ui/core/DialogContent";
import DialogContentText from "@material-ui/core/DialogContentText";
import DialogTitle from "@material-ui/core/DialogTitle";
import { makeStyles } from "@material-ui/core/styles";
import React from "react";
import { FormattedMessage, useIntl } from "react-intl";
import ConfirmButton, {
ConfirmButtonTransitionState
} from "@saleor/components/ConfirmButton";
import ControlledCheckbox from "@saleor/components/ControlledCheckbox";
import Form from "@saleor/components/Form";
import { buttonMessages } from "@saleor/intl";
import { OrderErrorFragment } from "@saleor/orders/types/OrderErrorFragment";
import FormSpacer from "@saleor/components/FormSpacer";
import getOrderErrorMessage from "@saleor/utils/errors/order";
import useModalDialogErrors from "@saleor/hooks/useModalDialogErrors";
export interface FormData {
restock: boolean;
}
const useStyles = makeStyles(
theme => ({
deleteButton: {
"&:hover": {
backgroundColor: theme.palette.error.main
},
backgroundColor: theme.palette.error.main,
color: theme.palette.error.contrastText
}
}),
{ name: "OrderCancelDialog" }
);
export interface OrderCancelDialogProps {
confirmButtonState: ConfirmButtonTransitionState;
errors: OrderErrorFragment[];
number: string;
open: boolean;
onClose?();
onSubmit(data: FormData);
onClose: () => void;
onSubmit: () => void;
}
const OrderCancelDialog: React.FC<OrderCancelDialogProps> = props => {
@ -55,76 +35,52 @@ const OrderCancelDialog: React.FC<OrderCancelDialogProps> = props => {
onClose
} = props;
const classes = useStyles(props);
const intl = useIntl();
const errors = useModalDialogErrors(apiErrors, open);
return (
<Dialog onClose={onClose} open={open}>
<Form
initial={{
restock: true
}}
onSubmit={onSubmit}
>
{({ data, change }) => (
<Dialog onClose={onClose} open={open} maxWidth="sm">
<DialogTitle>
<FormattedMessage
defaultMessage="Cancel Order"
description="dialog header"
id="OrderCancelDialogHeader"
/>
</DialogTitle>
<DialogContent>
<DialogContentText>
<FormattedMessage
defaultMessage="Cancelling this order will release unfulfilled stocks, so they can be bought by other customers. <b>Order will not be refunded when cancelling order - You need to do it manually.</b> Are you sure you want to cancel this order?"
values={{
b: (...chunks) => <b>{chunks}</b>,
orderNumber
}}
/>
</DialogContentText>
{errors.length > 0 && (
<>
<DialogTitle>
<FormattedMessage
defaultMessage="Cancel Order"
description="dialog header"
id="OrderCancelDialogHeader"
/>
</DialogTitle>
<DialogContent>
<DialogContentText>
<FormattedMessage
defaultMessage="Are you sure you want to cancel order #{orderNumber}?"
values={{
orderNumber
}}
/>
<ControlledCheckbox
checked={data.restock}
label={intl.formatMessage({
defaultMessage: "Release all stock allocated to this order",
description: "switch button"
})}
name="restock"
onChange={change}
/>
<FormSpacer />
{errors.map(err => (
<DialogContentText color="error">
{getOrderErrorMessage(err, intl)}
</DialogContentText>
{errors.length > 0 && (
<>
<FormSpacer />
{errors.map(err => (
<DialogContentText color="error">
{getOrderErrorMessage(err, intl)}
</DialogContentText>
))}
</>
)}
</DialogContent>
<DialogActions>
<Button onClick={onClose}>
<FormattedMessage {...buttonMessages.back} />
</Button>
<ConfirmButton
transitionState={confirmButtonState}
className={classes.deleteButton}
variant="contained"
type="submit"
>
<FormattedMessage
defaultMessage="Cancel Order"
description="button"
id="OrderCancelDialogButton"
/>
</ConfirmButton>
</DialogActions>
))}
</>
)}
</Form>
</DialogContent>
<DialogActions>
<Button onClick={onClose}>
<FormattedMessage {...buttonMessages.back} />
</Button>
<ConfirmButton
onClick={onSubmit}
transitionState={confirmButtonState}
variant="contained"
type="submit"
>
<FormattedMessage {...buttonMessages.accept} />
</ConfirmButton>
</DialogActions>
</Dialog>
);
};

View file

@ -0,0 +1,57 @@
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 DialogContentText from "@material-ui/core/DialogContentText";
import DialogTitle from "@material-ui/core/DialogTitle";
import makeStyles from "@material-ui/core/styles/makeStyles";
import React from "react";
import { FormattedMessage } from "react-intl";
import { buttonMessages } from "@saleor/intl";
import { DialogProps } from "@saleor/types";
const useStyles = makeStyles(
theme => ({
button: {
backgroundColor: theme.palette.error.main
}
}),
{
name: "OrderCannotCancelOrderDialog"
}
);
const OrderCannotCancelOrderDialog: React.FC<DialogProps> = ({
open,
onClose
}) => {
const classes = useStyles({});
return (
<Dialog onClose={onClose} open={open} maxWidth="sm">
<DialogTitle>
<FormattedMessage
defaultMessage="Saleor couldnt cancel order"
description="dialog header"
/>
</DialogTitle>
<DialogContent>
<DialogContentText>
<FormattedMessage defaultMessage="There are still fulfillments created for this order. Cancel the fulfillments first before you cancel the order." />
</DialogContentText>
</DialogContent>
<DialogActions>
<Button
variant="contained"
className={classes.button}
onClick={onClose}
>
<FormattedMessage {...buttonMessages.ok} />
</Button>
</DialogActions>
</Dialog>
);
};
OrderCannotCancelOrderDialog.displayName = "OrderCannotCancelOrderDialog";
export default OrderCannotCancelOrderDialog;

View file

@ -0,0 +1,2 @@
export { default } from "./OrderCannotCancelOrderDialog";
export * from "./OrderCannotCancelOrderDialog";

View file

@ -0,0 +1,44 @@
import { storiesOf } from "@storybook/react";
import React from "react";
import Decorator from "@saleor/storybook/Decorator";
import { warehouseList } from "@saleor/warehouses/fixtures";
import { OrderErrorCode } from "@saleor/types/globalTypes";
import OrderFulfillPage, { OrderFulfillPageProps } from "./OrderFulfillPage";
import { orderToFulfill } from "./fixtures";
const props: OrderFulfillPageProps = {
disabled: false,
errors: [],
onBack: () => undefined,
onSubmit: () => undefined,
order: orderToFulfill,
saveButtonBar: "default",
warehouses: warehouseList
};
storiesOf("Views / Orders / Fulfill order", module)
.addDecorator(Decorator)
.add("default", () => <OrderFulfillPage {...props} />)
.add("loading", () => (
<OrderFulfillPage
{...props}
disabled={true}
order={undefined}
warehouses={undefined}
/>
))
.add("error", () => (
<OrderFulfillPage
{...props}
errors={[
{
__typename: "OrderError",
code: OrderErrorCode.INSUFFICIENT_STOCK,
field: null,
orderLine: orderToFulfill.lines[0].id,
warehouse: warehouseList[0].id
}
]}
/>
));

View file

@ -0,0 +1,413 @@
import React from "react";
import { FormattedMessage, useIntl } from "react-intl";
import Card from "@material-ui/core/Card";
import CardActions from "@material-ui/core/CardActions";
import TableBody from "@material-ui/core/TableBody";
import TableCell from "@material-ui/core/TableCell";
import TableHead from "@material-ui/core/TableHead";
import TableRow from "@material-ui/core/TableRow";
import TextField from "@material-ui/core/TextField";
import classNames from "classnames";
import Typography from "@material-ui/core/Typography";
import useFormset, { FormsetData } from "@saleor/hooks/useFormset";
import {
OrderFulfillStockInput,
OrderErrorCode
} from "@saleor/types/globalTypes";
import { WarehouseFragment } from "@saleor/warehouses/types/WarehouseFragment";
import TableCellAvatar from "@saleor/components/TableCellAvatar";
import Container from "@saleor/components/Container";
import PageHeader from "@saleor/components/PageHeader";
import SaveButtonBar from "@saleor/components/SaveButtonBar";
import { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton";
import Form from "@saleor/components/Form";
import {
OrderFulfillData_order,
OrderFulfillData_order_lines
} from "@saleor/orders/types/OrderFulfillData";
import CardTitle from "@saleor/components/CardTitle";
import ResponsiveTable from "@saleor/components/ResponsiveTable";
import makeStyles from "@material-ui/core/styles/makeStyles";
import { update } from "@saleor/utils/lists";
import ControlledCheckbox from "@saleor/components/ControlledCheckbox";
import { renderCollection } from "@saleor/misc";
import Skeleton from "@saleor/components/Skeleton";
import AppHeader from "@saleor/components/AppHeader";
import { FulfillOrder_orderFulfill_errors } from "@saleor/orders/types/FulfillOrder";
const useStyles = makeStyles(
theme => ({
actionBar: {
flexDirection: "row",
paddingLeft: theme.spacing(2) + 2
},
colName: {
width: 250
},
colQuantity: {
width: 210
},
colQuantityContent: {
alignItems: "center",
display: "inline-flex"
},
colQuantityHeader: {
textAlign: "right"
},
colQuantityTotal: {
textAlign: "right",
width: 180
},
colSku: {
textAlign: "right",
width: 150
},
error: {
color: theme.palette.error.main
},
full: {
fontWeight: 600
},
quantityInnerInput: {
padding: "16px 0 14px 12px"
},
quantityInput: {
width: 100
},
remainingQuantity: {
marginLeft: theme.spacing()
},
table: {
"&&": {
tableLayout: "fixed"
}
}
}),
{ name: "OrderFulfillPage" }
);
interface OrderFulfillFormData {
sendInfo: boolean;
}
interface OrderFulfillSubmitData extends OrderFulfillFormData {
items: FormsetData<null, OrderFulfillStockInput[]>;
}
export interface OrderFulfillPageProps {
disabled: boolean;
errors: FulfillOrder_orderFulfill_errors[];
order: OrderFulfillData_order;
saveButtonBar: ConfirmButtonTransitionState;
warehouses: WarehouseFragment[];
onBack: () => void;
onSubmit: (data: OrderFulfillSubmitData) => void;
}
const initialFormData: OrderFulfillFormData = {
sendInfo: true
};
function getRemainingQuantity(line: OrderFulfillData_order_lines): number {
return line.quantity - line.quantityFulfilled;
}
const OrderFulfillPage: React.FC<OrderFulfillPageProps> = ({
disabled,
errors,
order,
saveButtonBar,
warehouses,
onBack,
onSubmit
}) => {
const intl = useIntl();
const classes = useStyles({});
const { change: formsetChange, data: formsetData } = useFormset<
null,
OrderFulfillStockInput[]
>(
order?.lines
.filter(line => getRemainingQuantity(line) > 0)
.map(line => ({
data: null,
id: line.id,
label: line.variant.attributes
.map(attribute =>
attribute.values
.map(attributeValue => attributeValue.name)
.join(" , ")
)
.join(" / "),
value: line.variant.stocks.map(stock => ({
quantity: 0,
warehouse: stock.warehouse.id
}))
}))
);
const handleSubmit = (formData: OrderFulfillFormData) =>
onSubmit({
...formData,
items: formsetData
});
return (
<Container>
<AppHeader onBack={onBack}>
{order?.number
? intl.formatMessage(
{
defaultMessage: "Order #{orderNumber}",
description: "page header with order number"
},
{
orderNumber: order.number
}
)
: intl.formatMessage({
defaultMessage: "Order",
description: "page header"
})}
</AppHeader>
<PageHeader
title={intl.formatMessage(
{
defaultMessage: "Order no. {orderNumber} - Add Fulfillment",
description: "page header"
},
{
orderNumber: order?.number
}
)}
/>
<Form initial={initialFormData} onSubmit={handleSubmit}>
{({ change, data, submit }) => (
<>
<Card>
<CardTitle
title={intl.formatMessage({
defaultMessage: "Items ready to ship",
description: "header"
})}
/>
<ResponsiveTable className={classes.table}>
<TableHead>
<TableRow>
<TableCell className={classes.colName}>
<FormattedMessage defaultMessage="Product name" />
</TableCell>
<TableCell className={classes.colSku}>
<FormattedMessage
defaultMessage="SKU"
description="product's sku"
/>
</TableCell>
{warehouses?.map(warehouse => (
<TableCell
key={warehouse.id}
className={classNames(
classes.colQuantity,
classes.colQuantityHeader
)}
>
{warehouse.name}
</TableCell>
))}
<TableCell className={classes.colQuantityTotal}>
<FormattedMessage
defaultMessage="Quantity to fulfill"
description="quantity of fulfilled products"
/>
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{renderCollection(
order?.lines.filter(line => getRemainingQuantity(line) > 0),
(line, lineIndex) => {
if (!line) {
return (
<TableRow>
<TableCellAvatar className={classes.colName}>
<Skeleton />
</TableCellAvatar>
<TableCell className={classes.colSku}>
<Skeleton />
</TableCell>
{warehouses?.map(warehouse => (
<TableCell
className={classes.colQuantity}
key={warehouse.id}
>
<Skeleton />
</TableCell>
))}
<TableCell className={classes.colQuantityTotal}>
{" "}
<Skeleton />
</TableCell>
</TableRow>
);
}
const remainingQuantity = getRemainingQuantity(line);
const quantityToFulfill = formsetData[
lineIndex
].value.reduce(
(quantityToFulfill, lineInput) =>
quantityToFulfill + (lineInput.quantity || 0),
0
);
const overfulfill = remainingQuantity < quantityToFulfill;
return (
<TableRow key={line.id}>
<TableCellAvatar
className={classes.colName}
thumbnail={line?.thumbnail?.url}
>
{line.productName}
<Typography color="textSecondary" variant="caption">
{line.variant.attributes
.map(attribute =>
attribute.values
.map(attributeValue => attributeValue.name)
.join(", ")
)
.join(" / ")}
</Typography>
</TableCellAvatar>
<TableCell className={classes.colSku}>
{line.variant.sku}
</TableCell>
{warehouses?.map(warehouse => {
const warehouseStock = line.variant.stocks.find(
stock => stock.warehouse.id === warehouse.id
);
const formsetStock = formsetData[
lineIndex
].value.find(
line => line.warehouse === warehouse.id
);
if (!warehouseStock) {
return (
<TableCell
className={classNames(
classes.colQuantity,
classes.error
)}
>
<FormattedMessage
defaultMessage="No Stock"
description="no variant stock in warehouse"
/>
</TableCell>
);
}
const availableQuantity =
warehouseStock.quantity -
warehouseStock.quantityAllocated;
return (
<TableCell className={classes.colQuantity}>
<div className={classes.colQuantityContent}>
<TextField
type="number"
inputProps={{
className: classes.quantityInnerInput,
max: warehouseStock.quantity,
min: 0,
style: { textAlign: "right" }
}}
className={classes.quantityInput}
value={formsetStock.quantity}
onChange={event =>
formsetChange(
line.id,
update(
{
quantity: parseInt(
event.target.value,
10
),
warehouse: warehouse.id
},
formsetData[lineIndex].value,
(a, b) => a.warehouse === b.warehouse
)
)
}
error={
overfulfill ||
formsetStock.quantity >
availableQuantity ||
!!errors?.find(
err =>
err.warehouse === warehouse.id &&
err.orderLine === line.id &&
err.code ===
OrderErrorCode.INSUFFICIENT_STOCK
)
}
/>
<div className={classes.remainingQuantity}>
/ {availableQuantity}
</div>
</div>
</TableCell>
);
})}
<TableCell className={classes.colQuantityTotal}>
<span
className={classNames({
[classes.error]: overfulfill,
[classes.full]:
remainingQuantity <= quantityToFulfill
})}
>
{quantityToFulfill}
</span>{" "}
/ {remainingQuantity}
</TableCell>
</TableRow>
);
}
)}
</TableBody>
</ResponsiveTable>
<CardActions className={classes.actionBar}>
<ControlledCheckbox
checked={data.sendInfo}
label={intl.formatMessage({
defaultMessage: "Send shipment details to customer",
description: "checkbox"
})}
name="sendInfo"
onChange={change}
/>
</CardActions>
</Card>
<SaveButtonBar
disabled={disabled}
labels={{
save: intl.formatMessage({
defaultMessage: "Fulfill",
description: "fulfill order, button"
})
}}
state={saveButtonBar}
onSave={submit}
onCancel={onBack}
/>
</>
)}
</Form>
</Container>
);
};
OrderFulfillPage.displayName = "OrderFulfillPage";
export default OrderFulfillPage;

View file

@ -0,0 +1,172 @@
/* eslint-disable sort-keys */
import { OrderFulfillData_order } from "@saleor/orders/types/OrderFulfillData";
import { warehouseList } from "@saleor/warehouses/fixtures";
import * as placeholderImage from "@assets/images/sample-product.jpg";
export const orderToFulfill: OrderFulfillData_order = {
__typename: "Order",
id: "T3JkZXI6Mg==",
lines: [
{
__typename: "OrderLine",
id: "T3JkZXJMaW5lOjQ=",
isShippingRequired: true,
productName: "T-Shirt",
quantity: 3,
quantityFulfilled: 1,
variant: {
__typename: "ProductVariant",
id: "UHJvZHVjdFZhcmlhbnQ6Mjk2",
name: "S",
sku: "62783187",
attributes: [
{
__typename: "SelectedAttribute",
values: [
{
__typename: "AttributeValue",
id: "QXR0cmlidXRlVmFsdWU6MzY=",
name: "S"
}
]
}
],
stocks: [
{
__typename: "Stock",
id: "U3RvY2s6NTIy",
warehouse: warehouseList[0],
quantity: 1217,
quantityAllocated: 10
},
{
__typename: "Stock",
id: "U3RvY2s6NTIx",
warehouse: warehouseList[1],
quantity: 1217,
quantityAllocated: 20
},
{
__typename: "Stock",
id: "U3RvY2s6NTIz",
warehouse: warehouseList[2],
quantity: 1217,
quantityAllocated: 4
},
{
__typename: "Stock",
id: "U3RvY2s6NTI0",
warehouse: warehouseList[3],
quantity: 1220,
quantityAllocated: 7
}
]
},
thumbnail: {
__typename: "Image",
url: placeholderImage
}
},
{
__typename: "OrderLine",
id: "T3JkZXJMaW5lOjU=",
isShippingRequired: true,
productName: "Lemon Juice",
quantity: 4,
quantityFulfilled: 0,
variant: {
__typename: "ProductVariant",
id: "UHJvZHVjdFZhcmlhbnQ6MTgx",
name: "2.5l",
sku: "998323583",
attributes: [
{
__typename: "SelectedAttribute",
values: [
{
__typename: "AttributeValue",
id: "QXR0cmlidXRlVmFsdWU6NjE=",
name: "2.5l"
}
]
}
],
stocks: [
{
__typename: "Stock",
id: "U3RvY2s6NTI=",
warehouse: warehouseList[1],
quantity: 760,
quantityAllocated: 2
},
{
__typename: "Stock",
id: "U3RvY2s6NTE=",
warehouse: warehouseList[2],
quantity: 760,
quantityAllocated: 33
},
{
__typename: "Stock",
id: "U3RvY2s6NTM=",
warehouse: warehouseList[3],
quantity: 760,
quantityAllocated: 4
}
]
},
thumbnail: {
__typename: "Image",
url: placeholderImage
}
},
{
__typename: "OrderLine",
id: "T3JkZXJMaW5lOjY=",
isShippingRequired: true,
productName: "Orange Juice",
quantity: 3,
quantityFulfilled: 2,
variant: {
__typename: "ProductVariant",
id: "UHJvZHVjdFZhcmlhbnQ6MTgy",
name: "5l",
sku: "998323584",
attributes: [
{
__typename: "SelectedAttribute",
values: [
{
__typename: "AttributeValue",
id: "QXR0cmlidXRlVmFsdWU6NjI=",
name: "5l"
}
]
}
],
stocks: [
{
__typename: "Stock",
id: "U3RvY2s6NTc=",
warehouse: warehouseList[0],
quantity: 587,
quantityAllocated: 0
},
{
__typename: "Stock",
id: "U3RvY2s6NTY=",
warehouse: warehouseList[2],
quantity: 587,
quantityAllocated: 1
}
]
},
thumbnail: {
__typename: "Image",
url: placeholderImage
}
}
],
number: "9123"
};

View file

@ -0,0 +1,2 @@
export * from "./OrderFulfillPage";
export { default } from "./OrderFulfillPage";

View file

@ -9,6 +9,7 @@ import TableRow from "@material-ui/core/TableRow";
import Typography from "@material-ui/core/Typography";
import React from "react";
import { FormattedMessage, useIntl } from "react-intl";
import classNames from "classnames";
import CardMenu from "@saleor/components/CardMenu";
import CardTitle from "@saleor/components/CardTitle";
@ -19,7 +20,7 @@ import StatusLabel from "@saleor/components/StatusLabel";
import TableCellAvatar, {
AVATAR_MARGIN
} from "@saleor/components/TableCellAvatar";
import { maybe, renderCollection } from "../../../misc";
import { maybe, renderCollection, getStringOrPlaceholder } from "../../../misc";
import { FulfillmentStatus } from "../../../types/globalTypes";
import { OrderDetails_order_fulfillments } from "../../types/OrderDetails";
@ -46,7 +47,15 @@ const useStyles = makeStyles(
textAlign: "right",
width: 120
},
infoLabel: {
display: "inline-block"
},
infoLabelWithMargin: {
marginBottom: theme.spacing()
},
infoRow: {
padding: theme.spacing(2, 3)
},
orderNumber: {
display: "inline",
marginLeft: theme.spacing(1)
@ -68,7 +77,7 @@ interface OrderFulfillmentProps {
onTrackingCodeAdd: () => void;
}
const numberOfColumns = 3;
const numberOfColumns = 4;
const OrderFulfillment: React.FC<OrderFulfillmentProps> = props => {
const {
@ -107,8 +116,8 @@ const OrderFulfillment: React.FC<OrderFulfillmentProps> = props => {
)
: intl.formatMessage(
{
defaultMessage: "Fulfilled ({quantity})",
description: "section header"
defaultMessage: "Cancelled ({quantity})",
description: "cancelled fulfillment, section header"
},
{
quantity
@ -135,7 +144,7 @@ const OrderFulfillment: React.FC<OrderFulfillmentProps> = props => {
menuItems={[
{
label: intl.formatMessage({
defaultMessage: "Cancel shipment",
defaultMessage: "Cancel Fulfillment",
description: "button"
}),
onSelect: onOrderFulfillmentCancel
@ -216,18 +225,47 @@ const OrderFulfillment: React.FC<OrderFulfillmentProps> = props => {
</TableCell>
</TableRow>
))}
{maybe(() => fulfillment.trackingNumber) && (
<TableRow>
<TableCell colSpan={numberOfColumns}>
<TableRow>
<TableCell className={classes.infoRow} colSpan={numberOfColumns}>
<Typography color="textSecondary" variant="body2">
<FormattedMessage
defaultMessage="Tracking Number: {trackingNumber}"
defaultMessage="Fulfilled from: {warehouseName}"
description="fulfillment group"
values={{
trackingNumber: fulfillment.trackingNumber
warehouseName: (
<Typography
className={classNames(classes.infoLabel, {
[classes.infoLabelWithMargin]: !!fulfillment?.trackingNumber
})}
color="textPrimary"
variant="body2"
>
{getStringOrPlaceholder(fulfillment?.warehouse?.name)}
</Typography>
)
}}
/>
</TableCell>
</TableRow>
)}
</Typography>
<Typography color="textSecondary" variant="body2">
{fulfillment?.trackingNumber && (
<FormattedMessage
defaultMessage="Tracking Number: {trackingNumber}"
values={{
trackingNumber: (
<Typography
className={classes.infoLabel}
color="textPrimary"
variant="body2"
>
{fulfillment.trackingNumber}
</Typography>
)
}}
/>
)}
</Typography>
</TableCell>
</TableRow>
</TableBody>
</ResponsiveTable>
{status === FulfillmentStatus.FULFILLED && !fulfillment.trackingNumber && (

View file

@ -11,25 +11,29 @@ import { FormattedMessage, useIntl } from "react-intl";
import ConfirmButton, {
ConfirmButtonTransitionState
} from "@saleor/components/ConfirmButton";
import { ControlledCheckbox } from "@saleor/components/ControlledCheckbox";
import Form from "@saleor/components/Form";
import { buttonMessages } from "@saleor/intl";
import { OrderErrorFragment } from "@saleor/orders/types/OrderErrorFragment";
import FormSpacer from "@saleor/components/FormSpacer";
import getOrderErrorMessage from "@saleor/utils/errors/order";
import { WarehouseFragment } from "@saleor/warehouses/types/WarehouseFragment";
import SingleAutocompleteSelectField from "@saleor/components/SingleAutocompleteSelectField";
import createSingleAutocompleteSelectHandler from "@saleor/utils/handlers/singleAutocompleteSelectChangeHandler";
export interface FormData {
restock: boolean;
export interface OrderFulfillmentCancelDialogFormData {
warehouseId: string;
}
const useStyles = makeStyles(
theme => ({
deleteButton: {
"&:hover": {
backgroundColor: theme.palette.error.main
},
backgroundColor: theme.palette.error.main,
color: theme.palette.error.contrastText
enableOverflow: {
overflow: "visible"
},
paragraph: {
marginBottom: theme.spacing(2)
},
selectCcontainer: {
width: "60%"
}
}),
{ name: "OrderFulfillmentCancelDialog" }
@ -39,69 +43,99 @@ export interface OrderFulfillmentCancelDialogProps {
confirmButtonState: ConfirmButtonTransitionState;
errors: OrderErrorFragment[];
open: boolean;
warehouses: WarehouseFragment[];
onClose();
onConfirm(data: FormData);
onConfirm(data: OrderFulfillmentCancelDialogFormData);
}
const OrderFulfillmentCancelDialog: React.FC<OrderFulfillmentCancelDialogProps> = props => {
const { confirmButtonState, errors, open, onConfirm, onClose } = props;
const {
confirmButtonState,
errors,
open,
warehouses,
onConfirm,
onClose
} = props;
const classes = useStyles(props);
const intl = useIntl();
const [displayValue, setDisplayValue] = React.useState("");
const choices = warehouses?.map(warehouse => ({
label: warehouse.name,
value: warehouse.id
}));
return (
<Dialog onClose={onClose} open={open} fullWidth maxWidth="xs">
<Form initial={{ restock: true }} onSubmit={onConfirm}>
{({ change, data, submit }) => (
<>
<DialogTitle>
<FormattedMessage
defaultMessage="Cancel Fulfillment"
description="dialog header"
/>
</DialogTitle>
<DialogContent>
<DialogContentText>
<FormattedMessage defaultMessage="Are you sure you want to cancel this fulfillment?" />
</DialogContentText>
<ControlledCheckbox
checked={data.restock}
label={intl.formatMessage({
defaultMessage: "Restock items?",
description: "switch button"
})}
name="restock"
onChange={change}
/>
{errors.length > 0 && (
<>
<FormSpacer />
{errors.map(err => (
<DialogContentText color="error">
{getOrderErrorMessage(err, intl)}
</DialogContentText>
))}
</>
)}
</DialogContent>
<DialogActions>
<Button onClick={onClose}>
<FormattedMessage {...buttonMessages.back} />
</Button>
<ConfirmButton
transitionState={confirmButtonState}
className={classes.deleteButton}
variant="contained"
onClick={submit}
>
<Dialog
classes={{
paper: classes.enableOverflow
}}
onClose={onClose}
open={open}
fullWidth
maxWidth="sm"
>
<Form initial={{ warehouseId: null }} onSubmit={onConfirm}>
{({ change, data: formData, submit }) => {
const handleChange = createSingleAutocompleteSelectHandler(
change,
setDisplayValue,
choices
);
return (
<>
<DialogTitle>
<FormattedMessage
defaultMessage="Cancel fulfillment"
description="button"
defaultMessage="Cancel Fulfillment"
description="dialog header"
/>
</ConfirmButton>
</DialogActions>
</>
)}
</DialogTitle>
<DialogContent className={classes.enableOverflow}>
<DialogContentText className={classes.paragraph}>
<FormattedMessage defaultMessage="Are you sure you want to cancel fulfillment? Canceling a fulfillment will restock products at a selected warehouse." />
</DialogContentText>
<div className={classes.selectCcontainer}>
<SingleAutocompleteSelectField
choices={choices}
displayValue={displayValue}
label={intl.formatMessage({
defaultMessage: "Select Warehouse",
description: "select warehouse to restock items"
})}
name="warehouseId"
value={formData.warehouseId}
onChange={handleChange}
/>
</div>
{errors.length > 0 && (
<>
<FormSpacer />
{errors.map(err => (
<DialogContentText color="error">
{getOrderErrorMessage(err, intl)}
</DialogContentText>
))}
</>
)}
</DialogContent>
<DialogActions>
<Button onClick={onClose}>
<FormattedMessage {...buttonMessages.back} />
</Button>
<ConfirmButton
disabled={formData.warehouseId === null}
transitionState={confirmButtonState}
variant="contained"
onClick={submit}
>
<FormattedMessage {...buttonMessages.accept} />
</ConfirmButton>
</DialogActions>
</>
);
}}
</Form>
</Dialog>
);

View file

@ -54,7 +54,7 @@ const useStyles = makeStyles(
width: 120
},
quantityInput: {
width: "4rem"
width: 100
},
remainingQuantity: {
marginLeft: theme.spacing(),

View file

@ -3,16 +3,15 @@ import TableBody from "@material-ui/core/TableBody";
import TableCell from "@material-ui/core/TableCell";
import TableFooter from "@material-ui/core/TableFooter";
import TableRow from "@material-ui/core/TableRow";
import TableHead from "@material-ui/core/TableHead";
import React from "react";
import { FormattedMessage, useIntl } from "react-intl";
import Checkbox from "@saleor/components/Checkbox";
import { DateTime } from "@saleor/components/Date";
import Money from "@saleor/components/Money";
import ResponsiveTable from "@saleor/components/ResponsiveTable";
import Skeleton from "@saleor/components/Skeleton";
import StatusLabel from "@saleor/components/StatusLabel";
import TableHead from "@saleor/components/TableHead";
import TablePagination from "@saleor/components/TablePagination";
import {
maybe,
@ -20,7 +19,7 @@ import {
transformOrderStatus,
transformPaymentStatus
} from "@saleor/misc";
import { ListActions, ListProps, SortPage } from "@saleor/types";
import { ListProps, SortPage } from "@saleor/types";
import { OrderListUrlSortField } from "@saleor/orders/urls";
import TableCellHeader from "@saleor/components/TableCellHeader";
import { getArrowDirection } from "@saleor/utils/sort";
@ -59,14 +58,11 @@ const useStyles = makeStyles(
{ name: "OrderList" }
);
interface OrderListProps
extends ListProps,
ListActions,
SortPage<OrderListUrlSortField> {
interface OrderListProps extends ListProps, SortPage<OrderListUrlSortField> {
orders: OrderList_orders_edges_node[];
}
const numberOfColumns = 7;
const numberOfColumns = 6;
export const OrderList: React.FC<OrderListProps> = props => {
const {
@ -79,12 +75,7 @@ export const OrderList: React.FC<OrderListProps> = props => {
onUpdateListSettings,
onRowClick,
onSort,
isChecked,
selected,
sort,
toggle,
toggleAll,
toolbar
sort
} = props;
const classes = useStyles(props);
@ -99,14 +90,7 @@ export const OrderList: React.FC<OrderListProps> = props => {
: undefined;
return (
<ResponsiveTable>
<TableHead
colSpan={numberOfColumns}
selected={selected}
disabled={disabled}
items={orders}
toggleAll={toggleAll}
toolbar={toolbar}
>
<TableHead>
<TableCellHeader
direction={
sort.sort === OrderListUrlSortField.number
@ -206,84 +190,67 @@ export const OrderList: React.FC<OrderListProps> = props => {
<TableBody>
{renderCollection(
orderList,
order => {
const isSelected = order ? isChecked(order.id) : false;
return (
<TableRow
hover={!!order}
className={!!order ? classes.link : undefined}
onClick={order ? onRowClick(order.id) : undefined}
key={order ? order.id : "skeleton"}
selected={isSelected}
>
<TableCell padding="checkbox">
<Checkbox
checked={isSelected}
disabled={disabled}
disableClickPropagation
onChange={() => toggle(order.id)}
/>
</TableCell>
<TableCell className={classes.colNumber}>
{maybe(() => order.number) ? (
"#" + order.number
) : (
<Skeleton />
)}
</TableCell>
<TableCell className={classes.colDate}>
{maybe(() => order.created) ? (
<DateTime date={order.created} />
) : (
<Skeleton />
)}
</TableCell>
<TableCell className={classes.colCustomer}>
{maybe(() => order.billingAddress) ? (
<>
{order.billingAddress.firstName}
&nbsp;
{order.billingAddress.lastName}
</>
) : maybe(() => order.userEmail) !== undefined ? (
order.userEmail
) : (
<Skeleton />
)}
</TableCell>
<TableCell className={classes.colPayment}>
{maybe(() => order.paymentStatus.status) !== undefined ? (
order.paymentStatus.status === null ? null : (
<StatusLabel
status={order.paymentStatus.status}
label={order.paymentStatus.localized}
/>
)
) : (
<Skeleton />
)}
</TableCell>
<TableCell className={classes.colFulfillment}>
{maybe(() => order.status) ? (
order => (
<TableRow
hover={!!order}
className={!!order ? classes.link : undefined}
onClick={order ? onRowClick(order.id) : undefined}
key={order ? order.id : "skeleton"}
>
<TableCell className={classes.colNumber}>
{maybe(() => order.number) ? "#" + order.number : <Skeleton />}
</TableCell>
<TableCell className={classes.colDate}>
{maybe(() => order.created) ? (
<DateTime date={order.created} />
) : (
<Skeleton />
)}
</TableCell>
<TableCell className={classes.colCustomer}>
{maybe(() => order.billingAddress) ? (
<>
{order.billingAddress.firstName}
&nbsp;
{order.billingAddress.lastName}
</>
) : maybe(() => order.userEmail) !== undefined ? (
order.userEmail
) : (
<Skeleton />
)}
</TableCell>
<TableCell className={classes.colPayment}>
{maybe(() => order.paymentStatus.status) !== undefined ? (
order.paymentStatus.status === null ? null : (
<StatusLabel
status={order.status.status}
label={order.status.localized}
status={order.paymentStatus.status}
label={order.paymentStatus.localized}
/>
) : (
<Skeleton />
)}
</TableCell>
<TableCell className={classes.colTotal}>
{maybe(() => order.total.gross) ? (
<Money money={order.total.gross} />
) : (
<Skeleton />
)}
</TableCell>
</TableRow>
);
},
)
) : (
<Skeleton />
)}
</TableCell>
<TableCell className={classes.colFulfillment}>
{maybe(() => order.status) ? (
<StatusLabel
status={order.status.status}
label={order.status.localized}
/>
) : (
<Skeleton />
)}
</TableCell>
<TableCell className={classes.colTotal}>
{maybe(() => order.total.gross) ? (
<Money money={order.total.gross} />
) : (
<Skeleton />
)}
</TableCell>
</TableRow>
),
() => (
<TableRow>
<TableCell colSpan={numberOfColumns}>

View file

@ -7,12 +7,7 @@ import { FormattedMessage, useIntl } from "react-intl";
import Container from "@saleor/components/Container";
import PageHeader from "@saleor/components/PageHeader";
import { sectionNames } from "@saleor/intl";
import {
FilterPageProps,
ListActions,
PageListProps,
SortPage
} from "@saleor/types";
import { FilterPageProps, PageListProps, SortPage } from "@saleor/types";
import { OrderListUrlSortField } from "@saleor/orders/urls";
import FilterBar from "@saleor/components/FilterBar";
import { OrderList_orders_edges_node } from "../../types/OrderList";
@ -25,7 +20,6 @@ import {
export interface OrderListPageProps
extends PageListProps,
ListActions,
FilterPageProps<OrderFilterKeys, OrderListFilterOpts>,
SortPage<OrderListUrlSortField> {
orders: OrderList_orders_edges_node[];

View file

@ -6,7 +6,6 @@ import {
TypedOrderAddNoteMutation,
TypedOrderCancelMutation,
TypedOrderCaptureMutation,
TypedOrderCreateFulfillmentMutation,
TypedOrderDraftCancelMutation,
TypedOrderDraftFinalizeMutation,
TypedOrderDraftUpdateMutation,
@ -24,10 +23,6 @@ import {
import { OrderAddNote, OrderAddNoteVariables } from "../types/OrderAddNote";
import { OrderCancel, OrderCancelVariables } from "../types/OrderCancel";
import { OrderCapture, OrderCaptureVariables } from "../types/OrderCapture";
import {
OrderCreateFulfillment,
OrderCreateFulfillmentVariables
} from "../types/OrderCreateFulfillment";
import {
OrderDraftCancel,
OrderDraftCancelVariables
@ -80,10 +75,6 @@ interface OrderOperationsProps {
OrderCancel,
OrderCancelVariables
>;
orderCreateFulfillment: PartialMutationProviderOutput<
OrderCreateFulfillment,
OrderCreateFulfillmentVariables
>;
orderFulfillmentCancel: PartialMutationProviderOutput<
OrderFulfillmentCancel,
OrderFulfillmentCancelVariables
@ -139,7 +130,6 @@ interface OrderOperationsProps {
>;
}) => React.ReactNode;
onOrderFulfillmentCancel: (data: OrderFulfillmentCancel) => void;
onOrderFulfillmentCreate: (data: OrderCreateFulfillment) => void;
onOrderFulfillmentUpdate: (data: OrderFulfillmentUpdateTracking) => void;
onOrderCancel: (data: OrderCancel) => void;
onOrderVoid: (data: OrderVoid) => void;
@ -160,7 +150,6 @@ interface OrderOperationsProps {
const OrderOperations: React.FC<OrderOperationsProps> = ({
children,
onDraftUpdate,
onOrderFulfillmentCreate,
onNoteAdd,
onOrderCancel,
onOrderLinesAdd,
@ -185,151 +174,140 @@ const OrderOperations: React.FC<OrderOperationsProps> = ({
{(...paymentCapture) => (
<TypedOrderRefundMutation onCompleted={onPaymentRefund}>
{(...paymentRefund) => (
<TypedOrderCreateFulfillmentMutation
onCompleted={onOrderFulfillmentCreate}
>
{(...createFulfillment) => (
<TypedOrderAddNoteMutation onCompleted={onNoteAdd}>
{(...addNote) => (
<TypedOrderUpdateMutation onCompleted={onUpdate}>
{(...update) => (
<TypedOrderDraftUpdateMutation
onCompleted={onDraftUpdate}
<TypedOrderAddNoteMutation onCompleted={onNoteAdd}>
{(...addNote) => (
<TypedOrderUpdateMutation onCompleted={onUpdate}>
{(...update) => (
<TypedOrderDraftUpdateMutation
onCompleted={onDraftUpdate}
>
{(...updateDraft) => (
<TypedOrderShippingMethodUpdateMutation
onCompleted={onShippingMethodUpdate}
>
{(...updateDraft) => (
<TypedOrderShippingMethodUpdateMutation
onCompleted={onShippingMethodUpdate}
{(...updateShippingMethod) => (
<TypedOrderLineDeleteMutation
onCompleted={onOrderLineDelete}
>
{(...updateShippingMethod) => (
<TypedOrderLineDeleteMutation
onCompleted={onOrderLineDelete}
{(...deleteOrderLine) => (
<TypedOrderLinesAddMutation
onCompleted={onOrderLinesAdd}
>
{(...deleteOrderLine) => (
<TypedOrderLinesAddMutation
onCompleted={onOrderLinesAdd}
{(...addOrderLine) => (
<TypedOrderLineUpdateMutation
onCompleted={onOrderLineUpdate}
>
{(...addOrderLine) => (
<TypedOrderLineUpdateMutation
onCompleted={onOrderLineUpdate}
{(...updateOrderLine) => (
<TypedOrderFulfillmentCancelMutation
onCompleted={
onOrderFulfillmentCancel
}
>
{(...updateOrderLine) => (
<TypedOrderFulfillmentCancelMutation
{(...cancelFulfillment) => (
<TypedOrderFulfillmentUpdateTrackingMutation
onCompleted={
onOrderFulfillmentCancel
onOrderFulfillmentUpdate
}
>
{(...cancelFulfillment) => (
<TypedOrderFulfillmentUpdateTrackingMutation
{(
...updateTrackingNumber
) => (
<TypedOrderDraftFinalizeMutation
onCompleted={
onOrderFulfillmentUpdate
onDraftFinalize
}
>
{(
...updateTrackingNumber
) => (
<TypedOrderDraftFinalizeMutation
{(...finalizeDraft) => (
<TypedOrderDraftCancelMutation
onCompleted={
onDraftFinalize
onDraftCancel
}
>
{(
...finalizeDraft
...cancelDraft
) => (
<TypedOrderDraftCancelMutation
<TypedOrderMarkAsPaidMutation
onCompleted={
onDraftCancel
onOrderMarkAsPaid
}
>
{(
...cancelDraft
) => (
<TypedOrderMarkAsPaidMutation
onCompleted={
onOrderMarkAsPaid
}
>
{(
...markAsPaid
) =>
children({
orderAddNote: getMutationProviderData(
...addNote
),
orderCancel: getMutationProviderData(
...orderCancel
),
orderDraftCancel: getMutationProviderData(
...cancelDraft
),
orderDraftFinalize: getMutationProviderData(
...finalizeDraft
),
orderDraftUpdate: getMutationProviderData(
...updateDraft
),
orderFulfillmentCancel: getMutationProviderData(
...cancelFulfillment
),
orderFulfillmentUpdateTracking: getMutationProviderData(
...updateTrackingNumber
),
orderLineDelete: getMutationProviderData(
...deleteOrderLine
),
orderLineUpdate: getMutationProviderData(
...updateOrderLine
),
orderLinesAdd: getMutationProviderData(
...addOrderLine
),
orderPaymentCapture: getMutationProviderData(
...paymentCapture
),
orderPaymentMarkAsPaid: getMutationProviderData(
...markAsPaid
) =>
children({
orderAddNote: getMutationProviderData(
...addNote
),
orderCancel: getMutationProviderData(
...orderCancel
),
orderCreateFulfillment: getMutationProviderData(
...createFulfillment
),
orderDraftCancel: getMutationProviderData(
...cancelDraft
),
orderDraftFinalize: getMutationProviderData(
...finalizeDraft
),
orderDraftUpdate: getMutationProviderData(
...updateDraft
),
orderFulfillmentCancel: getMutationProviderData(
...cancelFulfillment
),
orderFulfillmentUpdateTracking: getMutationProviderData(
...updateTrackingNumber
),
orderLineDelete: getMutationProviderData(
...deleteOrderLine
),
orderLineUpdate: getMutationProviderData(
...updateOrderLine
),
orderLinesAdd: getMutationProviderData(
...addOrderLine
),
orderPaymentCapture: getMutationProviderData(
...paymentCapture
),
orderPaymentMarkAsPaid: getMutationProviderData(
...markAsPaid
),
orderPaymentRefund: getMutationProviderData(
...paymentRefund
),
orderShippingMethodUpdate: getMutationProviderData(
...updateShippingMethod
),
orderUpdate: getMutationProviderData(
...update
),
orderVoid: getMutationProviderData(
...orderVoid
)
})
}
</TypedOrderMarkAsPaidMutation>
)}
</TypedOrderDraftCancelMutation>
),
orderPaymentRefund: getMutationProviderData(
...paymentRefund
),
orderShippingMethodUpdate: getMutationProviderData(
...updateShippingMethod
),
orderUpdate: getMutationProviderData(
...update
),
orderVoid: getMutationProviderData(
...orderVoid
)
})
}
</TypedOrderMarkAsPaidMutation>
)}
</TypedOrderDraftFinalizeMutation>
</TypedOrderDraftCancelMutation>
)}
</TypedOrderFulfillmentUpdateTrackingMutation>
</TypedOrderDraftFinalizeMutation>
)}
</TypedOrderFulfillmentCancelMutation>
</TypedOrderFulfillmentUpdateTrackingMutation>
)}
</TypedOrderLineUpdateMutation>
</TypedOrderFulfillmentCancelMutation>
)}
</TypedOrderLinesAddMutation>
</TypedOrderLineUpdateMutation>
)}
</TypedOrderLineDeleteMutation>
</TypedOrderLinesAddMutation>
)}
</TypedOrderShippingMethodUpdateMutation>
</TypedOrderLineDeleteMutation>
)}
</TypedOrderDraftUpdateMutation>
</TypedOrderShippingMethodUpdateMutation>
)}
</TypedOrderUpdateMutation>
</TypedOrderDraftUpdateMutation>
)}
</TypedOrderAddNoteMutation>
</TypedOrderUpdateMutation>
)}
</TypedOrderCreateFulfillmentMutation>
</TypedOrderAddNoteMutation>
)}
</TypedOrderRefundMutation>
)}

View file

@ -1,5 +1,6 @@
import { SearchCustomers_search_edges_node } from "@saleor/searches/types/SearchCustomers";
import { MessageDescriptor } from "react-intl";
import { warehouseList } from "@saleor/warehouses/fixtures";
import { transformOrderStatus, transformPaymentStatus } from "../misc";
import {
FulfillmentStatus,
@ -865,7 +866,8 @@ export const order = (placeholder: string): OrderDetails_order => ({
}
],
status: FulfillmentStatus.FULFILLED,
trackingNumber: ""
trackingNumber: "",
warehouse: warehouseList[1]
},
{
__typename: "Fulfillment",
@ -905,7 +907,8 @@ export const order = (placeholder: string): OrderDetails_order => ({
}
],
status: FulfillmentStatus.FULFILLED,
trackingNumber: ""
trackingNumber: "01nn12399su12nndfsy",
warehouse: warehouseList[0]
}
],
id: "T3JkZXI6OQ==",

View file

@ -14,9 +14,11 @@ import {
orderPath,
OrderUrlQueryParams,
OrderDraftListUrlSortField,
OrderListUrlSortField
OrderListUrlSortField,
orderFulfillPath
} from "./urls";
import OrderDetailsComponent from "./views/OrderDetails";
import OrderFulfillComponent from "./views/OrderFulfill";
import OrderDraftListComponent from "./views/OrderDraftList";
import OrderListComponent from "./views/OrderList";
@ -57,6 +59,10 @@ const OrderDetails: React.FC<RouteComponentProps<any>> = ({
);
};
const OrderFulfill: React.FC<RouteComponentProps<any>> = ({ match }) => (
<OrderFulfillComponent orderId={decodeURIComponent(match.params.id)} />
);
const Component = () => {
const intl = useIntl();
@ -66,6 +72,7 @@ const Component = () => {
<Switch>
<Route exact path={orderDraftListPath} component={OrderDraftList} />
<Route exact path={orderListPath} component={OrderList} />
<Route path={orderFulfillPath(":id")} component={OrderFulfill} />
<Route path={orderPath(":id")} component={OrderDetails} />
</Switch>
</>

View file

@ -8,20 +8,13 @@ import {
fragmentOrderEvent
} from "./queries";
import { OrderAddNote, OrderAddNoteVariables } from "./types/OrderAddNote";
import {
OrderBulkCancel,
OrderBulkCancelVariables
} from "./types/OrderBulkCancel";
import { OrderCancel, OrderCancelVariables } from "./types/OrderCancel";
import { OrderCapture, OrderCaptureVariables } from "./types/OrderCapture";
import {
OrderCreateFulfillment,
OrderCreateFulfillmentVariables
} from "./types/OrderCreateFulfillment";
import {
OrderDraftBulkCancel,
OrderDraftBulkCancelVariables
} from "./types/OrderDraftBulkCancel";
import { FulfillOrder, FulfillOrderVariables } from "./types/FulfillOrder";
import {
OrderDraftCancel,
OrderDraftCancelVariables
@ -74,8 +67,8 @@ export const orderErrorFragment = gql`
const orderCancelMutation = gql`
${fragmentOrderDetails}
${orderErrorFragment}
mutation OrderCancel($id: ID!, $restock: Boolean!) {
orderCancel(id: $id, restock: $restock) {
mutation OrderCancel($id: ID!) {
orderCancel(id: $id) {
errors: orderErrors {
...OrderErrorFragment
}
@ -90,21 +83,6 @@ export const TypedOrderCancelMutation = TypedMutation<
OrderCancelVariables
>(orderCancelMutation);
const orderBulkCancelMutation = gql`
${orderErrorFragment}
mutation OrderBulkCancel($ids: [ID]!, $restock: Boolean!) {
orderBulkCancel(ids: $ids, restock: $restock) {
errors: orderErrors {
...OrderErrorFragment
}
}
}
`;
export const TypedOrderBulkCancelMutation = TypedMutation<
OrderBulkCancel,
OrderBulkCancelVariables
>(orderBulkCancelMutation);
const orderDraftCancelMutation = gql`
${fragmentOrderDetails}
${orderErrorFragment}
@ -234,28 +212,6 @@ export const TypedOrderCaptureMutation = TypedMutation<
OrderCaptureVariables
>(orderCaptureMutation);
const orderCreateFulfillmentMutation = gql`
${fragmentOrderDetails}
${orderErrorFragment}
mutation OrderCreateFulfillment(
$order: ID!
$input: FulfillmentCreateInput!
) {
orderFulfillmentCreate(order: $order, input: $input) {
errors: orderErrors {
...OrderErrorFragment
}
order {
...OrderDetailsFragment
}
}
}
`;
export const TypedOrderCreateFulfillmentMutation = TypedMutation<
OrderCreateFulfillment,
OrderCreateFulfillmentVariables
>(orderCreateFulfillmentMutation);
const orderFulfillmentUpdateTrackingMutation = gql`
${fragmentOrderDetails}
${orderErrorFragment}
@ -477,3 +433,24 @@ export const TypedOrderLineUpdateMutation = TypedMutation<
OrderLineUpdate,
OrderLineUpdateVariables
>(orderLineUpdateMutation);
const fulfillOrder = gql`
${fragmentOrderDetails}
${orderErrorFragment}
mutation FulfillOrder($orderId: ID!, $input: OrderFulfillInput!) {
orderFulfill(order: $orderId, input: $input) {
errors: orderErrors {
...OrderErrorFragment
warehouse
orderLine
}
order {
...OrderDetailsFragment
}
}
}
`;
export const useOrderFulfill = makeMutation<
FulfillOrder,
FulfillOrderVariables
>(fulfillOrder);

View file

@ -13,6 +13,10 @@ import {
SearchOrderVariant as SearchOrderVariantType,
SearchOrderVariantVariables
} from "./types/SearchOrderVariant";
import {
OrderFulfillData,
OrderFulfillDataVariables
} from "./types/OrderFulfillData";
export const fragmentOrderEvent = gql`
fragment OrderEventFragment on OrderEvent {
@ -73,11 +77,32 @@ export const fragmentOrderLine = gql`
}
}
`;
export const fulfillmentFragment = gql`
${fragmentOrderLine}
fragment FulfillmentFragment on Fulfillment {
id
lines {
id
quantity
orderLine {
...OrderLineFragment
}
}
fulfillmentOrder
status
trackingNumber
warehouse {
id
name
}
}
`;
export const fragmentOrderDetails = gql`
${fragmentAddress}
${fragmentOrderEvent}
${fragmentOrderLine}
${fulfillmentFragment}
fragment OrderDetailsFragment on Order {
id
billingAddress {
@ -90,17 +115,7 @@ export const fragmentOrderDetails = gql`
...OrderEventFragment
}
fulfillments {
id
lines {
id
quantity
orderLine {
...OrderLineFragment
}
}
fulfillmentOrder
status
trackingNumber
...FulfillmentFragment
}
lines {
...OrderLineFragment
@ -327,3 +342,45 @@ export const useOrderVariantSearch = makeTopLevelSearch<
SearchOrderVariantType,
SearchOrderVariantVariables
>(searchOrderVariant);
const orderFulfillData = gql`
query OrderFulfillData($orderId: ID!) {
order(id: $orderId) {
id
lines {
id
isShippingRequired
productName
quantity
quantityFulfilled
variant {
id
name
sku
attributes {
values {
id
name
}
}
stocks {
id
warehouse {
id
}
quantity
quantityAllocated
}
}
thumbnail(size: 64) {
url
}
}
number
}
}
`;
export const useOrderFulfillData = makeQuery<
OrderFulfillData,
OrderFulfillDataVariables
>(orderFulfillData);

View file

@ -0,0 +1,291 @@
/* tslint:disable */
/* eslint-disable */
// This file was automatically generated and should not be edited.
import { OrderFulfillInput, OrderErrorCode, OrderEventsEmailsEnum, OrderEventsEnum, FulfillmentStatus, PaymentChargeStatusEnum, OrderStatus, OrderAction } from "./../../types/globalTypes";
// ====================================================
// GraphQL mutation operation: FulfillOrder
// ====================================================
export interface FulfillOrder_orderFulfill_errors {
__typename: "OrderError";
code: OrderErrorCode;
field: string | null;
warehouse: string | null;
orderLine: string | null;
}
export interface FulfillOrder_orderFulfill_order_billingAddress_country {
__typename: "CountryDisplay";
code: string;
country: string;
}
export interface FulfillOrder_orderFulfill_order_billingAddress {
__typename: "Address";
city: string;
cityArea: string;
companyName: string;
country: FulfillOrder_orderFulfill_order_billingAddress_country;
countryArea: string;
firstName: string;
id: string;
lastName: string;
phone: string | null;
postalCode: string;
streetAddress1: string;
streetAddress2: string;
}
export interface FulfillOrder_orderFulfill_order_events_user {
__typename: "User";
id: string;
email: string;
}
export interface FulfillOrder_orderFulfill_order_events {
__typename: "OrderEvent";
id: string;
amount: number | null;
date: any | null;
email: string | null;
emailType: OrderEventsEmailsEnum | null;
message: string | null;
quantity: number | null;
type: OrderEventsEnum | null;
user: FulfillOrder_orderFulfill_order_events_user | null;
}
export interface FulfillOrder_orderFulfill_order_fulfillments_lines_orderLine_unitPrice_gross {
__typename: "Money";
amount: number;
currency: string;
}
export interface FulfillOrder_orderFulfill_order_fulfillments_lines_orderLine_unitPrice_net {
__typename: "Money";
amount: number;
currency: string;
}
export interface FulfillOrder_orderFulfill_order_fulfillments_lines_orderLine_unitPrice {
__typename: "TaxedMoney";
gross: FulfillOrder_orderFulfill_order_fulfillments_lines_orderLine_unitPrice_gross;
net: FulfillOrder_orderFulfill_order_fulfillments_lines_orderLine_unitPrice_net;
}
export interface FulfillOrder_orderFulfill_order_fulfillments_lines_orderLine_thumbnail {
__typename: "Image";
url: string;
}
export interface FulfillOrder_orderFulfill_order_fulfillments_lines_orderLine {
__typename: "OrderLine";
id: string;
isShippingRequired: boolean;
productName: string;
productSku: string;
quantity: number;
quantityFulfilled: number;
unitPrice: FulfillOrder_orderFulfill_order_fulfillments_lines_orderLine_unitPrice | null;
thumbnail: FulfillOrder_orderFulfill_order_fulfillments_lines_orderLine_thumbnail | null;
}
export interface FulfillOrder_orderFulfill_order_fulfillments_lines {
__typename: "FulfillmentLine";
id: string;
quantity: number;
orderLine: FulfillOrder_orderFulfill_order_fulfillments_lines_orderLine | null;
}
export interface FulfillOrder_orderFulfill_order_fulfillments_warehouse {
__typename: "Warehouse";
id: string;
name: string;
}
export interface FulfillOrder_orderFulfill_order_fulfillments {
__typename: "Fulfillment";
id: string;
lines: (FulfillOrder_orderFulfill_order_fulfillments_lines | null)[] | null;
fulfillmentOrder: number;
status: FulfillmentStatus;
trackingNumber: string;
warehouse: FulfillOrder_orderFulfill_order_fulfillments_warehouse | null;
}
export interface FulfillOrder_orderFulfill_order_lines_unitPrice_gross {
__typename: "Money";
amount: number;
currency: string;
}
export interface FulfillOrder_orderFulfill_order_lines_unitPrice_net {
__typename: "Money";
amount: number;
currency: string;
}
export interface FulfillOrder_orderFulfill_order_lines_unitPrice {
__typename: "TaxedMoney";
gross: FulfillOrder_orderFulfill_order_lines_unitPrice_gross;
net: FulfillOrder_orderFulfill_order_lines_unitPrice_net;
}
export interface FulfillOrder_orderFulfill_order_lines_thumbnail {
__typename: "Image";
url: string;
}
export interface FulfillOrder_orderFulfill_order_lines {
__typename: "OrderLine";
id: string;
isShippingRequired: boolean;
productName: string;
productSku: string;
quantity: number;
quantityFulfilled: number;
unitPrice: FulfillOrder_orderFulfill_order_lines_unitPrice | null;
thumbnail: FulfillOrder_orderFulfill_order_lines_thumbnail | null;
}
export interface FulfillOrder_orderFulfill_order_shippingAddress_country {
__typename: "CountryDisplay";
code: string;
country: string;
}
export interface FulfillOrder_orderFulfill_order_shippingAddress {
__typename: "Address";
city: string;
cityArea: string;
companyName: string;
country: FulfillOrder_orderFulfill_order_shippingAddress_country;
countryArea: string;
firstName: string;
id: string;
lastName: string;
phone: string | null;
postalCode: string;
streetAddress1: string;
streetAddress2: string;
}
export interface FulfillOrder_orderFulfill_order_shippingMethod {
__typename: "ShippingMethod";
id: string;
}
export interface FulfillOrder_orderFulfill_order_shippingPrice_gross {
__typename: "Money";
amount: number;
currency: string;
}
export interface FulfillOrder_orderFulfill_order_shippingPrice {
__typename: "TaxedMoney";
gross: FulfillOrder_orderFulfill_order_shippingPrice_gross;
}
export interface FulfillOrder_orderFulfill_order_subtotal_gross {
__typename: "Money";
amount: number;
currency: string;
}
export interface FulfillOrder_orderFulfill_order_subtotal {
__typename: "TaxedMoney";
gross: FulfillOrder_orderFulfill_order_subtotal_gross;
}
export interface FulfillOrder_orderFulfill_order_total_gross {
__typename: "Money";
amount: number;
currency: string;
}
export interface FulfillOrder_orderFulfill_order_total_tax {
__typename: "Money";
amount: number;
currency: string;
}
export interface FulfillOrder_orderFulfill_order_total {
__typename: "TaxedMoney";
gross: FulfillOrder_orderFulfill_order_total_gross;
tax: FulfillOrder_orderFulfill_order_total_tax;
}
export interface FulfillOrder_orderFulfill_order_totalAuthorized {
__typename: "Money";
amount: number;
currency: string;
}
export interface FulfillOrder_orderFulfill_order_totalCaptured {
__typename: "Money";
amount: number;
currency: string;
}
export interface FulfillOrder_orderFulfill_order_user {
__typename: "User";
id: string;
email: string;
}
export interface FulfillOrder_orderFulfill_order_availableShippingMethods_price {
__typename: "Money";
amount: number;
currency: string;
}
export interface FulfillOrder_orderFulfill_order_availableShippingMethods {
__typename: "ShippingMethod";
id: string;
name: string;
price: FulfillOrder_orderFulfill_order_availableShippingMethods_price | null;
}
export interface FulfillOrder_orderFulfill_order {
__typename: "Order";
id: string;
billingAddress: FulfillOrder_orderFulfill_order_billingAddress | null;
canFinalize: boolean;
created: any;
customerNote: string;
events: (FulfillOrder_orderFulfill_order_events | null)[] | null;
fulfillments: (FulfillOrder_orderFulfill_order_fulfillments | null)[];
lines: (FulfillOrder_orderFulfill_order_lines | null)[];
number: string | null;
paymentStatus: PaymentChargeStatusEnum | null;
shippingAddress: FulfillOrder_orderFulfill_order_shippingAddress | null;
shippingMethod: FulfillOrder_orderFulfill_order_shippingMethod | null;
shippingMethodName: string | null;
shippingPrice: FulfillOrder_orderFulfill_order_shippingPrice | null;
status: OrderStatus;
subtotal: FulfillOrder_orderFulfill_order_subtotal | null;
total: FulfillOrder_orderFulfill_order_total | null;
actions: (OrderAction | null)[];
totalAuthorized: FulfillOrder_orderFulfill_order_totalAuthorized | null;
totalCaptured: FulfillOrder_orderFulfill_order_totalCaptured | null;
user: FulfillOrder_orderFulfill_order_user | null;
userEmail: string | null;
availableShippingMethods: (FulfillOrder_orderFulfill_order_availableShippingMethods | null)[] | null;
}
export interface FulfillOrder_orderFulfill {
__typename: "OrderFulfill";
errors: FulfillOrder_orderFulfill_errors[];
order: FulfillOrder_orderFulfill_order | null;
}
export interface FulfillOrder {
orderFulfill: FulfillOrder_orderFulfill | null;
}
export interface FulfillOrderVariables {
orderId: string;
input: OrderFulfillInput;
}

View file

@ -0,0 +1,67 @@
/* tslint:disable */
/* eslint-disable */
// This file was automatically generated and should not be edited.
import { FulfillmentStatus } from "./../../types/globalTypes";
// ====================================================
// GraphQL fragment: FulfillmentFragment
// ====================================================
export interface FulfillmentFragment_lines_orderLine_unitPrice_gross {
__typename: "Money";
amount: number;
currency: string;
}
export interface FulfillmentFragment_lines_orderLine_unitPrice_net {
__typename: "Money";
amount: number;
currency: string;
}
export interface FulfillmentFragment_lines_orderLine_unitPrice {
__typename: "TaxedMoney";
gross: FulfillmentFragment_lines_orderLine_unitPrice_gross;
net: FulfillmentFragment_lines_orderLine_unitPrice_net;
}
export interface FulfillmentFragment_lines_orderLine_thumbnail {
__typename: "Image";
url: string;
}
export interface FulfillmentFragment_lines_orderLine {
__typename: "OrderLine";
id: string;
isShippingRequired: boolean;
productName: string;
productSku: string;
quantity: number;
quantityFulfilled: number;
unitPrice: FulfillmentFragment_lines_orderLine_unitPrice | null;
thumbnail: FulfillmentFragment_lines_orderLine_thumbnail | null;
}
export interface FulfillmentFragment_lines {
__typename: "FulfillmentLine";
id: string;
quantity: number;
orderLine: FulfillmentFragment_lines_orderLine | null;
}
export interface FulfillmentFragment_warehouse {
__typename: "Warehouse";
id: string;
name: string;
}
export interface FulfillmentFragment {
__typename: "Fulfillment";
id: string;
lines: (FulfillmentFragment_lines | null)[] | null;
fulfillmentOrder: number;
status: FulfillmentStatus;
trackingNumber: string;
warehouse: FulfillmentFragment_warehouse | null;
}

View file

@ -1,29 +0,0 @@
/* tslint:disable */
/* eslint-disable */
// This file was automatically generated and should not be edited.
import { OrderErrorCode } from "./../../types/globalTypes";
// ====================================================
// GraphQL mutation operation: OrderBulkCancel
// ====================================================
export interface OrderBulkCancel_orderBulkCancel_errors {
__typename: "OrderError";
code: OrderErrorCode;
field: string | null;
}
export interface OrderBulkCancel_orderBulkCancel {
__typename: "OrderBulkCancel";
errors: OrderBulkCancel_orderBulkCancel_errors[];
}
export interface OrderBulkCancel {
orderBulkCancel: OrderBulkCancel_orderBulkCancel | null;
}
export interface OrderBulkCancelVariables {
ids: (string | null)[];
restock: boolean;
}

View file

@ -97,6 +97,12 @@ export interface OrderCancel_orderCancel_order_fulfillments_lines {
orderLine: OrderCancel_orderCancel_order_fulfillments_lines_orderLine | null;
}
export interface OrderCancel_orderCancel_order_fulfillments_warehouse {
__typename: "Warehouse";
id: string;
name: string;
}
export interface OrderCancel_orderCancel_order_fulfillments {
__typename: "Fulfillment";
id: string;
@ -104,6 +110,7 @@ export interface OrderCancel_orderCancel_order_fulfillments {
fulfillmentOrder: number;
status: FulfillmentStatus;
trackingNumber: string;
warehouse: OrderCancel_orderCancel_order_fulfillments_warehouse | null;
}
export interface OrderCancel_orderCancel_order_lines_unitPrice_gross {
@ -278,5 +285,4 @@ export interface OrderCancel {
export interface OrderCancelVariables {
id: string;
restock: boolean;
}

View file

@ -97,6 +97,12 @@ export interface OrderCapture_orderCapture_order_fulfillments_lines {
orderLine: OrderCapture_orderCapture_order_fulfillments_lines_orderLine | null;
}
export interface OrderCapture_orderCapture_order_fulfillments_warehouse {
__typename: "Warehouse";
id: string;
name: string;
}
export interface OrderCapture_orderCapture_order_fulfillments {
__typename: "Fulfillment";
id: string;
@ -104,6 +110,7 @@ export interface OrderCapture_orderCapture_order_fulfillments {
fulfillmentOrder: number;
status: FulfillmentStatus;
trackingNumber: string;
warehouse: OrderCapture_orderCapture_order_fulfillments_warehouse | null;
}
export interface OrderCapture_orderCapture_order_lines_unitPrice_gross {

View file

@ -1,282 +0,0 @@
/* tslint:disable */
/* eslint-disable */
// This file was automatically generated and should not be edited.
import { FulfillmentCreateInput, OrderErrorCode, OrderEventsEmailsEnum, OrderEventsEnum, FulfillmentStatus, PaymentChargeStatusEnum, OrderStatus, OrderAction } from "./../../types/globalTypes";
// ====================================================
// GraphQL mutation operation: OrderCreateFulfillment
// ====================================================
export interface OrderCreateFulfillment_orderFulfillmentCreate_errors {
__typename: "OrderError";
code: OrderErrorCode;
field: string | null;
}
export interface OrderCreateFulfillment_orderFulfillmentCreate_order_billingAddress_country {
__typename: "CountryDisplay";
code: string;
country: string;
}
export interface OrderCreateFulfillment_orderFulfillmentCreate_order_billingAddress {
__typename: "Address";
city: string;
cityArea: string;
companyName: string;
country: OrderCreateFulfillment_orderFulfillmentCreate_order_billingAddress_country;
countryArea: string;
firstName: string;
id: string;
lastName: string;
phone: string | null;
postalCode: string;
streetAddress1: string;
streetAddress2: string;
}
export interface OrderCreateFulfillment_orderFulfillmentCreate_order_events_user {
__typename: "User";
id: string;
email: string;
}
export interface OrderCreateFulfillment_orderFulfillmentCreate_order_events {
__typename: "OrderEvent";
id: string;
amount: number | null;
date: any | null;
email: string | null;
emailType: OrderEventsEmailsEnum | null;
message: string | null;
quantity: number | null;
type: OrderEventsEnum | null;
user: OrderCreateFulfillment_orderFulfillmentCreate_order_events_user | null;
}
export interface OrderCreateFulfillment_orderFulfillmentCreate_order_fulfillments_lines_orderLine_unitPrice_gross {
__typename: "Money";
amount: number;
currency: string;
}
export interface OrderCreateFulfillment_orderFulfillmentCreate_order_fulfillments_lines_orderLine_unitPrice_net {
__typename: "Money";
amount: number;
currency: string;
}
export interface OrderCreateFulfillment_orderFulfillmentCreate_order_fulfillments_lines_orderLine_unitPrice {
__typename: "TaxedMoney";
gross: OrderCreateFulfillment_orderFulfillmentCreate_order_fulfillments_lines_orderLine_unitPrice_gross;
net: OrderCreateFulfillment_orderFulfillmentCreate_order_fulfillments_lines_orderLine_unitPrice_net;
}
export interface OrderCreateFulfillment_orderFulfillmentCreate_order_fulfillments_lines_orderLine_thumbnail {
__typename: "Image";
url: string;
}
export interface OrderCreateFulfillment_orderFulfillmentCreate_order_fulfillments_lines_orderLine {
__typename: "OrderLine";
id: string;
isShippingRequired: boolean;
productName: string;
productSku: string;
quantity: number;
quantityFulfilled: number;
unitPrice: OrderCreateFulfillment_orderFulfillmentCreate_order_fulfillments_lines_orderLine_unitPrice | null;
thumbnail: OrderCreateFulfillment_orderFulfillmentCreate_order_fulfillments_lines_orderLine_thumbnail | null;
}
export interface OrderCreateFulfillment_orderFulfillmentCreate_order_fulfillments_lines {
__typename: "FulfillmentLine";
id: string;
quantity: number;
orderLine: OrderCreateFulfillment_orderFulfillmentCreate_order_fulfillments_lines_orderLine | null;
}
export interface OrderCreateFulfillment_orderFulfillmentCreate_order_fulfillments {
__typename: "Fulfillment";
id: string;
lines: (OrderCreateFulfillment_orderFulfillmentCreate_order_fulfillments_lines | null)[] | null;
fulfillmentOrder: number;
status: FulfillmentStatus;
trackingNumber: string;
}
export interface OrderCreateFulfillment_orderFulfillmentCreate_order_lines_unitPrice_gross {
__typename: "Money";
amount: number;
currency: string;
}
export interface OrderCreateFulfillment_orderFulfillmentCreate_order_lines_unitPrice_net {
__typename: "Money";
amount: number;
currency: string;
}
export interface OrderCreateFulfillment_orderFulfillmentCreate_order_lines_unitPrice {
__typename: "TaxedMoney";
gross: OrderCreateFulfillment_orderFulfillmentCreate_order_lines_unitPrice_gross;
net: OrderCreateFulfillment_orderFulfillmentCreate_order_lines_unitPrice_net;
}
export interface OrderCreateFulfillment_orderFulfillmentCreate_order_lines_thumbnail {
__typename: "Image";
url: string;
}
export interface OrderCreateFulfillment_orderFulfillmentCreate_order_lines {
__typename: "OrderLine";
id: string;
isShippingRequired: boolean;
productName: string;
productSku: string;
quantity: number;
quantityFulfilled: number;
unitPrice: OrderCreateFulfillment_orderFulfillmentCreate_order_lines_unitPrice | null;
thumbnail: OrderCreateFulfillment_orderFulfillmentCreate_order_lines_thumbnail | null;
}
export interface OrderCreateFulfillment_orderFulfillmentCreate_order_shippingAddress_country {
__typename: "CountryDisplay";
code: string;
country: string;
}
export interface OrderCreateFulfillment_orderFulfillmentCreate_order_shippingAddress {
__typename: "Address";
city: string;
cityArea: string;
companyName: string;
country: OrderCreateFulfillment_orderFulfillmentCreate_order_shippingAddress_country;
countryArea: string;
firstName: string;
id: string;
lastName: string;
phone: string | null;
postalCode: string;
streetAddress1: string;
streetAddress2: string;
}
export interface OrderCreateFulfillment_orderFulfillmentCreate_order_shippingMethod {
__typename: "ShippingMethod";
id: string;
}
export interface OrderCreateFulfillment_orderFulfillmentCreate_order_shippingPrice_gross {
__typename: "Money";
amount: number;
currency: string;
}
export interface OrderCreateFulfillment_orderFulfillmentCreate_order_shippingPrice {
__typename: "TaxedMoney";
gross: OrderCreateFulfillment_orderFulfillmentCreate_order_shippingPrice_gross;
}
export interface OrderCreateFulfillment_orderFulfillmentCreate_order_subtotal_gross {
__typename: "Money";
amount: number;
currency: string;
}
export interface OrderCreateFulfillment_orderFulfillmentCreate_order_subtotal {
__typename: "TaxedMoney";
gross: OrderCreateFulfillment_orderFulfillmentCreate_order_subtotal_gross;
}
export interface OrderCreateFulfillment_orderFulfillmentCreate_order_total_gross {
__typename: "Money";
amount: number;
currency: string;
}
export interface OrderCreateFulfillment_orderFulfillmentCreate_order_total_tax {
__typename: "Money";
amount: number;
currency: string;
}
export interface OrderCreateFulfillment_orderFulfillmentCreate_order_total {
__typename: "TaxedMoney";
gross: OrderCreateFulfillment_orderFulfillmentCreate_order_total_gross;
tax: OrderCreateFulfillment_orderFulfillmentCreate_order_total_tax;
}
export interface OrderCreateFulfillment_orderFulfillmentCreate_order_totalAuthorized {
__typename: "Money";
amount: number;
currency: string;
}
export interface OrderCreateFulfillment_orderFulfillmentCreate_order_totalCaptured {
__typename: "Money";
amount: number;
currency: string;
}
export interface OrderCreateFulfillment_orderFulfillmentCreate_order_user {
__typename: "User";
id: string;
email: string;
}
export interface OrderCreateFulfillment_orderFulfillmentCreate_order_availableShippingMethods_price {
__typename: "Money";
amount: number;
currency: string;
}
export interface OrderCreateFulfillment_orderFulfillmentCreate_order_availableShippingMethods {
__typename: "ShippingMethod";
id: string;
name: string;
price: OrderCreateFulfillment_orderFulfillmentCreate_order_availableShippingMethods_price | null;
}
export interface OrderCreateFulfillment_orderFulfillmentCreate_order {
__typename: "Order";
id: string;
billingAddress: OrderCreateFulfillment_orderFulfillmentCreate_order_billingAddress | null;
canFinalize: boolean;
created: any;
customerNote: string;
events: (OrderCreateFulfillment_orderFulfillmentCreate_order_events | null)[] | null;
fulfillments: (OrderCreateFulfillment_orderFulfillmentCreate_order_fulfillments | null)[];
lines: (OrderCreateFulfillment_orderFulfillmentCreate_order_lines | null)[];
number: string | null;
paymentStatus: PaymentChargeStatusEnum | null;
shippingAddress: OrderCreateFulfillment_orderFulfillmentCreate_order_shippingAddress | null;
shippingMethod: OrderCreateFulfillment_orderFulfillmentCreate_order_shippingMethod | null;
shippingMethodName: string | null;
shippingPrice: OrderCreateFulfillment_orderFulfillmentCreate_order_shippingPrice | null;
status: OrderStatus;
subtotal: OrderCreateFulfillment_orderFulfillmentCreate_order_subtotal | null;
total: OrderCreateFulfillment_orderFulfillmentCreate_order_total | null;
actions: (OrderAction | null)[];
totalAuthorized: OrderCreateFulfillment_orderFulfillmentCreate_order_totalAuthorized | null;
totalCaptured: OrderCreateFulfillment_orderFulfillmentCreate_order_totalCaptured | null;
user: OrderCreateFulfillment_orderFulfillmentCreate_order_user | null;
userEmail: string | null;
availableShippingMethods: (OrderCreateFulfillment_orderFulfillmentCreate_order_availableShippingMethods | null)[] | null;
}
export interface OrderCreateFulfillment_orderFulfillmentCreate {
__typename: "FulfillmentCreate";
errors: OrderCreateFulfillment_orderFulfillmentCreate_errors[];
order: OrderCreateFulfillment_orderFulfillmentCreate_order | null;
}
export interface OrderCreateFulfillment {
orderFulfillmentCreate: OrderCreateFulfillment_orderFulfillmentCreate | null;
}
export interface OrderCreateFulfillmentVariables {
order: string;
input: FulfillmentCreateInput;
}

View file

@ -91,6 +91,12 @@ export interface OrderDetails_order_fulfillments_lines {
orderLine: OrderDetails_order_fulfillments_lines_orderLine | null;
}
export interface OrderDetails_order_fulfillments_warehouse {
__typename: "Warehouse";
id: string;
name: string;
}
export interface OrderDetails_order_fulfillments {
__typename: "Fulfillment";
id: string;
@ -98,6 +104,7 @@ export interface OrderDetails_order_fulfillments {
fulfillmentOrder: number;
status: FulfillmentStatus;
trackingNumber: string;
warehouse: OrderDetails_order_fulfillments_warehouse | null;
}
export interface OrderDetails_order_lines_unitPrice_gross {

View file

@ -91,6 +91,12 @@ export interface OrderDetailsFragment_fulfillments_lines {
orderLine: OrderDetailsFragment_fulfillments_lines_orderLine | null;
}
export interface OrderDetailsFragment_fulfillments_warehouse {
__typename: "Warehouse";
id: string;
name: string;
}
export interface OrderDetailsFragment_fulfillments {
__typename: "Fulfillment";
id: string;
@ -98,6 +104,7 @@ export interface OrderDetailsFragment_fulfillments {
fulfillmentOrder: number;
status: FulfillmentStatus;
trackingNumber: string;
warehouse: OrderDetailsFragment_fulfillments_warehouse | null;
}
export interface OrderDetailsFragment_lines_unitPrice_gross {

View file

@ -97,6 +97,12 @@ export interface OrderDraftCancel_draftOrderDelete_order_fulfillments_lines {
orderLine: OrderDraftCancel_draftOrderDelete_order_fulfillments_lines_orderLine | null;
}
export interface OrderDraftCancel_draftOrderDelete_order_fulfillments_warehouse {
__typename: "Warehouse";
id: string;
name: string;
}
export interface OrderDraftCancel_draftOrderDelete_order_fulfillments {
__typename: "Fulfillment";
id: string;
@ -104,6 +110,7 @@ export interface OrderDraftCancel_draftOrderDelete_order_fulfillments {
fulfillmentOrder: number;
status: FulfillmentStatus;
trackingNumber: string;
warehouse: OrderDraftCancel_draftOrderDelete_order_fulfillments_warehouse | null;
}
export interface OrderDraftCancel_draftOrderDelete_order_lines_unitPrice_gross {

View file

@ -97,6 +97,12 @@ export interface OrderDraftFinalize_draftOrderComplete_order_fulfillments_lines
orderLine: OrderDraftFinalize_draftOrderComplete_order_fulfillments_lines_orderLine | null;
}
export interface OrderDraftFinalize_draftOrderComplete_order_fulfillments_warehouse {
__typename: "Warehouse";
id: string;
name: string;
}
export interface OrderDraftFinalize_draftOrderComplete_order_fulfillments {
__typename: "Fulfillment";
id: string;
@ -104,6 +110,7 @@ export interface OrderDraftFinalize_draftOrderComplete_order_fulfillments {
fulfillmentOrder: number;
status: FulfillmentStatus;
trackingNumber: string;
warehouse: OrderDraftFinalize_draftOrderComplete_order_fulfillments_warehouse | null;
}
export interface OrderDraftFinalize_draftOrderComplete_order_lines_unitPrice_gross {

View file

@ -97,6 +97,12 @@ export interface OrderDraftUpdate_draftOrderUpdate_order_fulfillments_lines {
orderLine: OrderDraftUpdate_draftOrderUpdate_order_fulfillments_lines_orderLine | null;
}
export interface OrderDraftUpdate_draftOrderUpdate_order_fulfillments_warehouse {
__typename: "Warehouse";
id: string;
name: string;
}
export interface OrderDraftUpdate_draftOrderUpdate_order_fulfillments {
__typename: "Fulfillment";
id: string;
@ -104,6 +110,7 @@ export interface OrderDraftUpdate_draftOrderUpdate_order_fulfillments {
fulfillmentOrder: number;
status: FulfillmentStatus;
trackingNumber: string;
warehouse: OrderDraftUpdate_draftOrderUpdate_order_fulfillments_warehouse | null;
}
export interface OrderDraftUpdate_draftOrderUpdate_order_lines_unitPrice_gross {

View file

@ -0,0 +1,71 @@
/* tslint:disable */
/* eslint-disable */
// This file was automatically generated and should not be edited.
// ====================================================
// GraphQL query operation: OrderFulfillData
// ====================================================
export interface OrderFulfillData_order_lines_variant_attributes_values {
__typename: "AttributeValue";
id: string;
name: string | null;
}
export interface OrderFulfillData_order_lines_variant_attributes {
__typename: "SelectedAttribute";
values: (OrderFulfillData_order_lines_variant_attributes_values | null)[];
}
export interface OrderFulfillData_order_lines_variant_stocks_warehouse {
__typename: "Warehouse";
id: string;
}
export interface OrderFulfillData_order_lines_variant_stocks {
__typename: "Stock";
id: string;
warehouse: OrderFulfillData_order_lines_variant_stocks_warehouse;
quantity: number;
quantityAllocated: number;
}
export interface OrderFulfillData_order_lines_variant {
__typename: "ProductVariant";
id: string;
name: string;
sku: string;
attributes: OrderFulfillData_order_lines_variant_attributes[];
stocks: (OrderFulfillData_order_lines_variant_stocks | null)[] | null;
}
export interface OrderFulfillData_order_lines_thumbnail {
__typename: "Image";
url: string;
}
export interface OrderFulfillData_order_lines {
__typename: "OrderLine";
id: string;
isShippingRequired: boolean;
productName: string;
quantity: number;
quantityFulfilled: number;
variant: OrderFulfillData_order_lines_variant | null;
thumbnail: OrderFulfillData_order_lines_thumbnail | null;
}
export interface OrderFulfillData_order {
__typename: "Order";
id: string;
lines: (OrderFulfillData_order_lines | null)[];
number: string | null;
}
export interface OrderFulfillData {
order: OrderFulfillData_order | null;
}
export interface OrderFulfillDataVariables {
orderId: string;
}

View file

@ -97,6 +97,12 @@ export interface OrderFulfillmentCancel_orderFulfillmentCancel_order_fulfillment
orderLine: OrderFulfillmentCancel_orderFulfillmentCancel_order_fulfillments_lines_orderLine | null;
}
export interface OrderFulfillmentCancel_orderFulfillmentCancel_order_fulfillments_warehouse {
__typename: "Warehouse";
id: string;
name: string;
}
export interface OrderFulfillmentCancel_orderFulfillmentCancel_order_fulfillments {
__typename: "Fulfillment";
id: string;
@ -104,6 +110,7 @@ export interface OrderFulfillmentCancel_orderFulfillmentCancel_order_fulfillment
fulfillmentOrder: number;
status: FulfillmentStatus;
trackingNumber: string;
warehouse: OrderFulfillmentCancel_orderFulfillmentCancel_order_fulfillments_warehouse | null;
}
export interface OrderFulfillmentCancel_orderFulfillmentCancel_order_lines_unitPrice_gross {

View file

@ -97,6 +97,12 @@ export interface OrderFulfillmentUpdateTracking_orderFulfillmentUpdateTracking_o
orderLine: OrderFulfillmentUpdateTracking_orderFulfillmentUpdateTracking_order_fulfillments_lines_orderLine | null;
}
export interface OrderFulfillmentUpdateTracking_orderFulfillmentUpdateTracking_order_fulfillments_warehouse {
__typename: "Warehouse";
id: string;
name: string;
}
export interface OrderFulfillmentUpdateTracking_orderFulfillmentUpdateTracking_order_fulfillments {
__typename: "Fulfillment";
id: string;
@ -104,6 +110,7 @@ export interface OrderFulfillmentUpdateTracking_orderFulfillmentUpdateTracking_o
fulfillmentOrder: number;
status: FulfillmentStatus;
trackingNumber: string;
warehouse: OrderFulfillmentUpdateTracking_orderFulfillmentUpdateTracking_order_fulfillments_warehouse | null;
}
export interface OrderFulfillmentUpdateTracking_orderFulfillmentUpdateTracking_order_lines_unitPrice_gross {

View file

@ -97,6 +97,12 @@ export interface OrderLineDelete_draftOrderLineDelete_order_fulfillments_lines {
orderLine: OrderLineDelete_draftOrderLineDelete_order_fulfillments_lines_orderLine | null;
}
export interface OrderLineDelete_draftOrderLineDelete_order_fulfillments_warehouse {
__typename: "Warehouse";
id: string;
name: string;
}
export interface OrderLineDelete_draftOrderLineDelete_order_fulfillments {
__typename: "Fulfillment";
id: string;
@ -104,6 +110,7 @@ export interface OrderLineDelete_draftOrderLineDelete_order_fulfillments {
fulfillmentOrder: number;
status: FulfillmentStatus;
trackingNumber: string;
warehouse: OrderLineDelete_draftOrderLineDelete_order_fulfillments_warehouse | null;
}
export interface OrderLineDelete_draftOrderLineDelete_order_lines_unitPrice_gross {

View file

@ -97,6 +97,12 @@ export interface OrderLineUpdate_draftOrderLineUpdate_order_fulfillments_lines {
orderLine: OrderLineUpdate_draftOrderLineUpdate_order_fulfillments_lines_orderLine | null;
}
export interface OrderLineUpdate_draftOrderLineUpdate_order_fulfillments_warehouse {
__typename: "Warehouse";
id: string;
name: string;
}
export interface OrderLineUpdate_draftOrderLineUpdate_order_fulfillments {
__typename: "Fulfillment";
id: string;
@ -104,6 +110,7 @@ export interface OrderLineUpdate_draftOrderLineUpdate_order_fulfillments {
fulfillmentOrder: number;
status: FulfillmentStatus;
trackingNumber: string;
warehouse: OrderLineUpdate_draftOrderLineUpdate_order_fulfillments_warehouse | null;
}
export interface OrderLineUpdate_draftOrderLineUpdate_order_lines_unitPrice_gross {

View file

@ -97,6 +97,12 @@ export interface OrderLinesAdd_draftOrderLinesCreate_order_fulfillments_lines {
orderLine: OrderLinesAdd_draftOrderLinesCreate_order_fulfillments_lines_orderLine | null;
}
export interface OrderLinesAdd_draftOrderLinesCreate_order_fulfillments_warehouse {
__typename: "Warehouse";
id: string;
name: string;
}
export interface OrderLinesAdd_draftOrderLinesCreate_order_fulfillments {
__typename: "Fulfillment";
id: string;
@ -104,6 +110,7 @@ export interface OrderLinesAdd_draftOrderLinesCreate_order_fulfillments {
fulfillmentOrder: number;
status: FulfillmentStatus;
trackingNumber: string;
warehouse: OrderLinesAdd_draftOrderLinesCreate_order_fulfillments_warehouse | null;
}
export interface OrderLinesAdd_draftOrderLinesCreate_order_lines_unitPrice_gross {

View file

@ -97,6 +97,12 @@ export interface OrderMarkAsPaid_orderMarkAsPaid_order_fulfillments_lines {
orderLine: OrderMarkAsPaid_orderMarkAsPaid_order_fulfillments_lines_orderLine | null;
}
export interface OrderMarkAsPaid_orderMarkAsPaid_order_fulfillments_warehouse {
__typename: "Warehouse";
id: string;
name: string;
}
export interface OrderMarkAsPaid_orderMarkAsPaid_order_fulfillments {
__typename: "Fulfillment";
id: string;
@ -104,6 +110,7 @@ export interface OrderMarkAsPaid_orderMarkAsPaid_order_fulfillments {
fulfillmentOrder: number;
status: FulfillmentStatus;
trackingNumber: string;
warehouse: OrderMarkAsPaid_orderMarkAsPaid_order_fulfillments_warehouse | null;
}
export interface OrderMarkAsPaid_orderMarkAsPaid_order_lines_unitPrice_gross {

View file

@ -97,6 +97,12 @@ export interface OrderRefund_orderRefund_order_fulfillments_lines {
orderLine: OrderRefund_orderRefund_order_fulfillments_lines_orderLine | null;
}
export interface OrderRefund_orderRefund_order_fulfillments_warehouse {
__typename: "Warehouse";
id: string;
name: string;
}
export interface OrderRefund_orderRefund_order_fulfillments {
__typename: "Fulfillment";
id: string;
@ -104,6 +110,7 @@ export interface OrderRefund_orderRefund_order_fulfillments {
fulfillmentOrder: number;
status: FulfillmentStatus;
trackingNumber: string;
warehouse: OrderRefund_orderRefund_order_fulfillments_warehouse | null;
}
export interface OrderRefund_orderRefund_order_lines_unitPrice_gross {

View file

@ -97,6 +97,12 @@ export interface OrderVoid_orderVoid_order_fulfillments_lines {
orderLine: OrderVoid_orderVoid_order_fulfillments_lines_orderLine | null;
}
export interface OrderVoid_orderVoid_order_fulfillments_warehouse {
__typename: "Warehouse";
id: string;
name: string;
}
export interface OrderVoid_orderVoid_order_fulfillments {
__typename: "Fulfillment";
id: string;
@ -104,6 +110,7 @@ export interface OrderVoid_orderVoid_order_fulfillments {
fulfillmentOrder: number;
status: FulfillmentStatus;
trackingNumber: string;
warehouse: OrderVoid_orderVoid_order_fulfillments_warehouse | null;
}
export interface OrderVoid_orderVoid_order_lines_unitPrice_gross {

View file

@ -97,10 +97,14 @@ export type OrderUrlDialog =
| "edit-shipping"
| "edit-shipping-address"
| "finalize"
| "fulfill"
| "mark-paid"
| "refund"
| "void";
export type OrderUrlQueryParams = Dialog<OrderUrlDialog> & SingleAction;
export const orderUrl = (id: string, params?: OrderUrlQueryParams) =>
orderPath(encodeURIComponent(id)) + "?" + stringifyQs(params);
export const orderFulfillPath = (id: string) =>
urlJoin(orderPath(id), "fulfill");
export const orderFulfillUrl = (id: string) =>
orderFulfillPath(encodeURIComponent(id));

View file

@ -7,7 +7,6 @@ import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandl
import { OrderAddNote } from "../../types/OrderAddNote";
import { OrderCancel } from "../../types/OrderCancel";
import { OrderCapture } from "../../types/OrderCapture";
import { OrderCreateFulfillment } from "../../types/OrderCreateFulfillment";
import { OrderDraftCancel } from "../../types/OrderDraftCancel";
import { OrderDraftFinalize } from "../../types/OrderDraftFinalize";
import { OrderDraftUpdate } from "../../types/OrderDraftUpdate";
@ -31,7 +30,6 @@ interface OrderDetailsMessages {
handleNoteAdd: (data: OrderAddNote) => void;
handleOrderCancel: (data: OrderCancel) => void;
handleOrderFulfillmentCancel: (data: OrderFulfillmentCancel) => void;
handleOrderFulfillmentCreate: (data: OrderCreateFulfillment) => void;
handleOrderFulfillmentUpdate: (
data: OrderFulfillmentUpdateTracking
) => void;
@ -86,17 +84,6 @@ export const OrderDetailsMessages: React.FC<OrderDetailsMessages> = ({
closeModal();
}
};
const handleOrderFulfillmentCreate = (data: OrderCreateFulfillment) => {
const errs = data.orderFulfillmentCreate?.errors;
if (errs.length === 0) {
pushMessage({
text: intl.formatMessage({
defaultMessage: "Items successfully fulfilled"
})
});
closeModal();
}
};
const handleOrderMarkAsPaid = (data: OrderMarkAsPaid) => {
const errs = data.orderMarkAsPaid?.errors;
if (errs.length === 0) {
@ -256,7 +243,6 @@ export const OrderDetailsMessages: React.FC<OrderDetailsMessages> = ({
handleNoteAdd,
handleOrderCancel,
handleOrderFulfillmentCancel,
handleOrderFulfillmentCreate,
handleOrderFulfillmentUpdate,
handleOrderLineDelete,
handleOrderLineUpdate,

View file

@ -8,6 +8,8 @@ import useUser from "@saleor/hooks/useUser";
import useCustomerSearch from "@saleor/searches/useCustomerSearch";
import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers";
import NotFoundPage from "@saleor/components/NotFoundPage";
import { useWarehouseList } from "@saleor/warehouses/queries";
import OrderCannotCancelOrderDialog from "@saleor/orders/components/OrderCannotCancelOrderDialog";
import { customerUrl } from "../../../customers/urls";
import {
maybe,
@ -15,7 +17,7 @@ import {
getStringOrPlaceholder
} from "../../../misc";
import { productUrl } from "../../../products/urls";
import { OrderStatus } from "../../../types/globalTypes";
import { OrderStatus, FulfillmentStatus } from "../../../types/globalTypes";
import OrderAddressEditDialog from "../../components/OrderAddressEditDialog";
import OrderCancelDialog from "../../components/OrderCancelDialog";
import OrderDetailsPage from "../../components/OrderDetailsPage";
@ -25,7 +27,6 @@ import OrderDraftFinalizeDialog, {
} from "../../components/OrderDraftFinalizeDialog";
import OrderDraftPage from "../../components/OrderDraftPage";
import OrderFulfillmentCancelDialog from "../../components/OrderFulfillmentCancelDialog";
import OrderFulfillmentDialog from "../../components/OrderFulfillmentDialog";
import OrderFulfillmentTrackingDialog from "../../components/OrderFulfillmentTrackingDialog";
import OrderMarkAsPaidDialog from "../../components/OrderMarkAsPaidDialog/OrderMarkAsPaidDialog";
import OrderPaymentDialog from "../../components/OrderPaymentDialog";
@ -39,7 +40,8 @@ import {
orderListUrl,
orderUrl,
OrderUrlQueryParams,
OrderUrlDialog
OrderUrlDialog,
orderFulfillUrl
} from "../../urls";
import { OrderDetailsMessages } from "./OrderDetailsMessages";
@ -95,6 +97,12 @@ export const OrderDetails: React.FC<OrderDetailsProps> = ({ id, params }) => {
} = useOrderVariantSearch({
variables: DEFAULT_INITIAL_SEARCH_DATA
});
const warehouses = useWarehouseList({
displayLoader: true,
variables: {
first: 30
}
});
const intl = useIntl();
const [openModal, closeModal] = createDialogActionHandlers<
@ -118,9 +126,6 @@ export const OrderDetails: React.FC<OrderDetailsProps> = ({ id, params }) => {
{orderMessages => (
<OrderOperations
order={id}
onOrderFulfillmentCreate={
orderMessages.handleOrderFulfillmentCreate
}
onNoteAdd={orderMessages.handleNoteAdd}
onOrderCancel={orderMessages.handleOrderCancel}
onOrderVoid={orderMessages.handleOrderVoid}
@ -147,7 +152,6 @@ export const OrderDetails: React.FC<OrderDetailsProps> = ({ id, params }) => {
{({
orderAddNote,
orderCancel,
orderCreateFulfillment,
orderDraftUpdate,
orderLinesAdd,
orderLineDelete,
@ -194,7 +198,7 @@ export const OrderDetails: React.FC<OrderDetailsProps> = ({ id, params }) => {
)}
userPermissions={user?.userPermissions || []}
onOrderCancel={() => openModal("cancel")}
onOrderFulfill={() => openModal("fulfill")}
onOrderFulfill={() => navigate(orderFulfillUrl(id))}
onFulfillmentCancel={fulfillmentId =>
navigate(
orderUrl(id, {
@ -226,6 +230,17 @@ export const OrderDetails: React.FC<OrderDetailsProps> = ({ id, params }) => {
navigate(customerUrl(order.user.id))
}
/>
<OrderCannotCancelOrderDialog
onClose={closeModal}
open={
params.action === "cancel" &&
order?.fulfillments.some(
fulfillment =>
fulfillment.status ===
FulfillmentStatus.FULFILLED
)
}
/>
<OrderCancelDialog
confirmButtonState={orderCancel.opts.status}
errors={
@ -234,10 +249,9 @@ export const OrderDetails: React.FC<OrderDetailsProps> = ({ id, params }) => {
number={order?.number}
open={params.action === "cancel"}
onClose={closeModal}
onSubmit={variables =>
onSubmit={() =>
orderCancel.mutate({
id,
...variables
id
})
}
/>
@ -298,38 +312,6 @@ export const OrderDetails: React.FC<OrderDetailsProps> = ({ id, params }) => {
})
}
/>
<OrderFulfillmentDialog
confirmButtonState={
orderCreateFulfillment.opts.status
}
errors={
orderCreateFulfillment.opts.data
?.orderFulfillmentCreate.errors || []
}
open={params.action === "fulfill"}
lines={maybe(() => order.lines, []).filter(
line => line.quantityFulfilled < line.quantity
)}
onClose={closeModal}
onSubmit={variables =>
orderCreateFulfillment.mutate({
input: {
...variables,
lines: maybe(() => order.lines, [])
.filter(
line =>
line.quantityFulfilled < line.quantity
)
.map((line, lineIndex) => ({
orderLineId: line.id,
quantity: variables.lines[lineIndex]
}))
.filter(line => line.quantity > 0)
},
order: order.id
})
}
/>
<OrderFulfillmentCancelDialog
confirmButtonState={
orderFulfillmentCancel.opts.status
@ -339,6 +321,11 @@ export const OrderDetails: React.FC<OrderDetailsProps> = ({ id, params }) => {
?.orderFulfillmentCancel.errors || []
}
open={params.action === "cancel-fulfillment"}
warehouses={
warehouses.data?.warehouses.edges.map(
edge => edge.node
) || []
}
onConfirm={variables =>
orderFulfillmentCancel.mutate({
id: params.id,

View file

@ -0,0 +1,94 @@
import { useIntl } from "react-intl";
import React from "react";
import { useOrderFulfillData } from "@saleor/orders/queries";
import OrderFulfillPage from "@saleor/orders/components/OrderFulfillPage";
import useNavigator from "@saleor/hooks/useNavigator";
import { orderUrl } from "@saleor/orders/urls";
import { useWarehouseList } from "@saleor/warehouses/queries";
import { WindowTitle } from "@saleor/components/WindowTitle";
import { useOrderFulfill } from "@saleor/orders/mutations";
import useNotifier from "@saleor/hooks/useNotifier";
export interface OrderFulfillProps {
orderId: string;
}
const OrderFulfill: React.FC<OrderFulfillProps> = ({ orderId }) => {
const navigate = useNavigator();
const notify = useNotifier();
const intl = useIntl();
const { data, loading } = useOrderFulfillData({
displayLoader: true,
variables: {
orderId
}
});
const { data: warehouseData, loading: warehousesLoading } = useWarehouseList({
displayLoader: true,
variables: {
first: 20
}
});
const [fulfillOrder, fulfillOrderOpts] = useOrderFulfill({
onCompleted: data => {
if (data.orderFulfill.errors.length === 0) {
navigate(orderUrl(orderId), true);
notify({
text: intl.formatMessage({
defaultMessage: "Fulfilled Items",
description: "order fulfilled success message"
})
});
}
}
});
return (
<>
<WindowTitle
title={
data?.order?.number
? intl.formatMessage(
{
defaultMessage: "Fulfill Order #{orderNumber}",
description: "window title"
},
{
orderNumber: data.order.number
}
)
: intl.formatMessage({
defaultMessage: "Fulfill Order",
description: "window title"
})
}
/>
<OrderFulfillPage
disabled={loading || warehousesLoading || fulfillOrderOpts.loading}
errors={fulfillOrderOpts.data?.orderFulfill.errors}
onBack={() => navigate(orderUrl(orderId))}
onSubmit={formData =>
fulfillOrder({
variables: {
input: {
lines: formData.items.map(line => ({
orderLineId: line.id,
stocks: line.value
})),
notifyCustomer: formData.sendInfo
},
orderId
}
})
}
order={data?.order}
saveButtonBar="default"
warehouses={warehouseData?.warehouses.edges.map(edge => edge.node)}
/>
</>
);
};
OrderFulfill.displayName = "OrderFulfill";
export default OrderFulfill;

View file

@ -0,0 +1,2 @@
export * from "./OrderFulfill";
export { default } from "./OrderFulfill";

View file

@ -1,12 +1,10 @@
import Button from "@material-ui/core/Button";
import React from "react";
import { FormattedMessage, useIntl } from "react-intl";
import { useIntl } from "react-intl";
import DeleteFilterTabDialog from "@saleor/components/DeleteFilterTabDialog";
import SaveFilterTabDialog, {
SaveFilterTabDialogFormData
} from "@saleor/components/SaveFilterTabDialog";
import useBulkActions from "@saleor/hooks/useBulkActions";
import useListSettings from "@saleor/hooks/useListSettings";
import useNavigator from "@saleor/hooks/useNavigator";
import useNotifier from "@saleor/hooks/useNotifier";
@ -14,20 +12,15 @@ import usePaginator, {
createPaginationState
} from "@saleor/hooks/usePaginator";
import useShop from "@saleor/hooks/useShop";
import { maybe } from "@saleor/misc";
import { maybe, getStringOrPlaceholder } from "@saleor/misc";
import { ListViews } from "@saleor/types";
import createSortHandler from "@saleor/utils/handlers/sortHandler";
import { getSortParams } from "@saleor/utils/sort";
import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers";
import createFilterHandlers from "@saleor/utils/handlers/filterHandlers";
import OrderBulkCancelDialog from "../../components/OrderBulkCancelDialog";
import OrderListPage from "../../components/OrderListPage/OrderListPage";
import {
TypedOrderBulkCancelMutation,
useOrderDraftCreateMutation
} from "../../mutations";
import { useOrderDraftCreateMutation } from "../../mutations";
import { useOrderListQuery } from "../../queries";
import { OrderBulkCancel } from "../../types/OrderBulkCancel";
import { OrderDraftCreate } from "../../types/OrderDraftCreate";
import {
orderListUrl,
@ -56,9 +49,6 @@ export const OrderList: React.FC<OrderListProps> = ({ params }) => {
const notify = useNotifier();
const paginate = usePaginator();
const shop = useShop();
const { isSelected, listElements, reset, toggle, toggleAll } = useBulkActions(
params.ids
);
const { updateListSettings, settings } = useListSettings(
ListViews.ORDER_LIST
);
@ -91,7 +81,6 @@ export const OrderList: React.FC<OrderListProps> = ({ params }) => {
resetFilters,
handleSearchChange
] = createFilterHandlers({
cleanupFn: reset,
createUrl: orderListUrl,
getFilterQueryParam,
navigate,
@ -103,19 +92,16 @@ export const OrderList: React.FC<OrderListProps> = ({ params }) => {
OrderListUrlQueryParams
>(navigate, orderListUrl, params);
const handleTabChange = (tab: number) => {
reset();
const handleTabChange = (tab: number) =>
navigate(
orderListUrl({
activeTab: tab.toString(),
...getFilterTabs()[tab - 1].data
})
);
};
const handleFilterTabDelete = () => {
deleteFilterTab(currentTab);
reset();
navigate(orderListUrl());
};
@ -135,7 +121,7 @@ export const OrderList: React.FC<OrderListProps> = ({ params }) => {
}),
[params, settings.rowNumber]
);
const { data, loading, refetch } = useOrderListQuery({
const { data, loading } = useOrderListQuery({
displayLoader: true,
variables: queryVariables
});
@ -146,101 +132,48 @@ export const OrderList: React.FC<OrderListProps> = ({ params }) => {
params
);
const handleOrderBulkCancel = (data: OrderBulkCancel) => {
if (data.orderBulkCancel.errors.length === 0) {
notify({
text: intl.formatMessage({
defaultMessage: "Orders cancelled"
})
});
reset();
refetch();
closeModal();
}
};
const handleSort = createSortHandler(navigate, orderListUrl, params);
return (
<TypedOrderBulkCancelMutation onCompleted={handleOrderBulkCancel}>
{(orderBulkCancel, orderBulkCancelOpts) => {
const onOrderBulkCancel = (restock: boolean) =>
orderBulkCancel({
variables: {
ids: params.ids,
restock
}
});
return (
<>
<OrderListPage
currencySymbol={currencySymbol}
settings={settings}
currentTab={currentTab}
disabled={loading}
filterOpts={getFilterOpts(params)}
orders={maybe(() => data.orders.edges.map(edge => edge.node))}
pageInfo={pageInfo}
sort={getSortParams(params)}
onAdd={createOrder}
onNextPage={loadNextPage}
onPreviousPage={loadPreviousPage}
onUpdateListSettings={updateListSettings}
onRowClick={id => () => navigate(orderUrl(id))}
onSort={handleSort}
isChecked={isSelected}
selected={listElements.length}
toggle={toggle}
toggleAll={toggleAll}
toolbar={
<Button
color="primary"
onClick={() =>
openModal("cancel", {
ids: listElements
})
}
>
<FormattedMessage
defaultMessage="Cancel"
description="cancel orders, button"
/>
</Button>
}
onSearchChange={handleSearchChange}
onFilterChange={changeFilters}
onTabSave={() => openModal("save-search")}
onTabDelete={() => openModal("delete-search")}
onTabChange={handleTabChange}
initialSearch={params.query || ""}
tabs={getFilterTabs().map(tab => tab.name)}
onAll={resetFilters}
/>
<OrderBulkCancelDialog
confirmButtonState={orderBulkCancelOpts.status}
numberOfOrders={maybe(() => params.ids.length.toString(), "...")}
onClose={closeModal}
onConfirm={onOrderBulkCancel}
open={params.action === "cancel"}
/>
<SaveFilterTabDialog
open={params.action === "save-search"}
confirmButtonState="default"
onClose={closeModal}
onSubmit={handleFilterTabSave}
/>
<DeleteFilterTabDialog
open={params.action === "delete-search"}
confirmButtonState="default"
onClose={closeModal}
onSubmit={handleFilterTabDelete}
tabName={maybe(() => tabs[currentTab - 1].name, "...")}
/>
</>
);
}}
</TypedOrderBulkCancelMutation>
<>
<OrderListPage
currencySymbol={currencySymbol}
settings={settings}
currentTab={currentTab}
disabled={loading}
filterOpts={getFilterOpts(params)}
orders={maybe(() => data.orders.edges.map(edge => edge.node))}
pageInfo={pageInfo}
sort={getSortParams(params)}
onAdd={createOrder}
onNextPage={loadNextPage}
onPreviousPage={loadPreviousPage}
onUpdateListSettings={updateListSettings}
onRowClick={id => () => navigate(orderUrl(id))}
onSort={handleSort}
onSearchChange={handleSearchChange}
onFilterChange={changeFilters}
onTabSave={() => openModal("save-search")}
onTabDelete={() => openModal("delete-search")}
onTabChange={handleTabChange}
initialSearch={params.query || ""}
tabs={getFilterTabs().map(tab => tab.name)}
onAll={resetFilters}
/>
<SaveFilterTabDialog
open={params.action === "save-search"}
confirmButtonState="default"
onClose={closeModal}
onSubmit={handleFilterTabSave}
/>
<DeleteFilterTabDialog
open={params.action === "delete-search"}
confirmButtonState="default"
onClose={closeModal}
onSubmit={handleFilterTabDelete}
tabName={getStringOrPlaceholder(tabs[currentTab - 1]?.name)}
/>
</>
);
};

View file

@ -28,6 +28,7 @@ import { SearchProductTypes_search_edges_node_productAttributes } from "@saleor/
import createMultiAutocompleteSelectHandler from "@saleor/utils/handlers/multiAutocompleteSelectChangeHandler";
import createSingleAutocompleteSelectHandler from "@saleor/utils/handlers/singleAutocompleteSelectChangeHandler";
import { ProductErrorFragment } from "@saleor/attributes/types/ProductErrorFragment";
import { SearchWarehouses_search_edges_node } from "@saleor/searches/types/SearchWarehouses";
import { FetchMoreProps } from "../../../types";
import {
createAttributeChangeHandler,
@ -41,7 +42,7 @@ import ProductAttributes, {
import ProductDetailsForm from "../ProductDetailsForm";
import ProductOrganization from "../ProductOrganization";
import ProductPricing from "../ProductPricing";
import ProductStock from "../ProductStock";
import ProductStocks, { ProductStockInput } from "../ProductStocks";
interface FormData {
basePrice: number;
@ -57,9 +58,11 @@ interface FormData {
seoTitle: string;
sku: string;
stockQuantity: number;
trackInventory: boolean;
}
export interface ProductCreatePageSubmitData extends FormData {
attributes: ProductAttributeInput[];
stocks: ProductStockInput[];
}
interface ProductCreatePageProps {
@ -79,9 +82,11 @@ interface ProductCreatePageProps {
}>;
header: string;
saveButtonBarState: ConfirmButtonTransitionState;
warehouses: SearchWarehouses_search_edges_node[];
fetchCategories: (data: string) => void;
fetchCollections: (data: string) => void;
fetchProductTypes: (data: string) => void;
onWarehouseEdit: () => void;
onBack?();
onSubmit?(data: ProductCreatePageSubmitData);
}
@ -100,9 +105,11 @@ export const ProductCreatePage: React.FC<ProductCreatePageProps> = ({
header,
productTypes: productTypeChoiceList,
saveButtonBarState,
warehouses,
onBack,
fetchProductTypes,
onSubmit
onSubmit,
onWarehouseEdit
}: ProductCreatePageProps) => {
const intl = useIntl();
const localizeDate = useDateLocalize();
@ -112,6 +119,18 @@ export const ProductCreatePage: React.FC<ProductCreatePageProps> = ({
data: attributes,
set: setAttributeData
} = useFormset<ProductAttributeInputData>([]);
const { change: changeStockData, data: stocks, set: setStocks } = useFormset<
null
>([]);
React.useEffect(() => {
const newStocks = warehouses.map(warehouse => ({
data: null,
id: warehouse.id,
label: warehouse.name,
value: stocks.find(stock => stock.id === warehouse.id)?.value || 0
}));
setStocks(newStocks);
}, [JSON.stringify(warehouses)]);
// Ensures that it will not change after component rerenders, because it
// generates different block keys and it causes editor to lose its content.
@ -131,7 +150,8 @@ export const ProductCreatePage: React.FC<ProductCreatePageProps> = ({
seoDescription: "",
seoTitle: "",
sku: null,
stockQuantity: null
stockQuantity: null,
trackInventory: false
};
// Display values
@ -145,12 +165,7 @@ export const ProductCreatePage: React.FC<ProductCreatePageProps> = ({
MultiAutocompleteChoiceType[]
>([]);
const [productType, setProductType] = React.useState<ProductType>({
hasVariants: false,
id: "",
name: "",
productAttributes: []
});
const [productType, setProductType] = React.useState<ProductType>(null);
const categories = getChoices(categoryChoiceList);
const collections = getChoices(collectionChoiceList);
@ -159,6 +174,7 @@ export const ProductCreatePage: React.FC<ProductCreatePageProps> = ({
const handleSubmit = (data: FormData) =>
onSubmit({
attributes,
stocks,
...data
});
@ -232,14 +248,16 @@ export const ProductCreatePage: React.FC<ProductCreatePageProps> = ({
onChange={change}
/>
<CardSpacer />
{!productType.hasVariants && (
{!!productType && !productType.hasVariants && (
<>
<ProductStock
<ProductStocks
data={data}
disabled={disabled}
product={undefined}
onChange={change}
onChange={changeStockData}
onFormDataChange={change}
errors={errors}
stocks={stocks}
onWarehousesEdit={onWarehouseEdit}
/>
<CardSpacer />
</>
@ -273,7 +291,7 @@ export const ProductCreatePage: React.FC<ProductCreatePageProps> = ({
fetchMoreProductTypes={fetchMoreProductTypes}
fetchProductTypes={fetchProductTypes}
productType={productType}
productTypeInputDisplayValue={productType.name}
productTypeInputDisplayValue={productType?.name || ""}
productTypes={productTypes}
onCategoryChange={handleCategorySelect}
onCollectionChange={handleCollectionSelect}

View file

@ -1,96 +0,0 @@
import Card from "@material-ui/core/Card";
import CardContent from "@material-ui/core/CardContent";
import { makeStyles } from "@material-ui/core/styles";
import TextField from "@material-ui/core/TextField";
import React from "react";
import { useIntl } from "react-intl";
import CardTitle from "@saleor/components/CardTitle";
import { ProductErrorFragment } from "@saleor/attributes/types/ProductErrorFragment";
import { getFormErrors, getProductErrorMessage } from "@saleor/utils/errors";
import { ProductDetails_product } from "../../types/ProductDetails";
const useStyles = makeStyles(
theme => ({
root: {
display: "grid",
gridColumnGap: theme.spacing(2),
gridTemplateColumns: "1fr 1fr"
}
}),
{ name: "ProductStock" }
);
interface ProductStockProps {
data: {
sku: string;
stockQuantity: number;
};
disabled: boolean;
errors: ProductErrorFragment[];
product: ProductDetails_product;
onChange: (event: React.ChangeEvent<any>) => void;
}
const ProductStock: React.FC<ProductStockProps> = props => {
const { data, disabled, product, onChange, errors } = props;
const classes = useStyles(props);
const intl = useIntl();
const formErrors = getFormErrors(["sku", "stockQuantity"], errors);
return (
<Card>
<CardTitle
title={intl.formatMessage({
defaultMessage: "Inventory",
description: "product stock, section header",
id: "productStockHeader"
})}
/>
<CardContent>
<div className={classes.root}>
<TextField
disabled={disabled}
name="sku"
label={intl.formatMessage({
defaultMessage: "SKU (Stock Keeping Unit)"
})}
value={data.sku}
onChange={onChange}
error={!!formErrors.sku}
helperText={getProductErrorMessage(formErrors.sku, intl)}
/>
<TextField
disabled={disabled}
name="stockQuantity"
label={intl.formatMessage({
defaultMessage: "Inventory",
description: "product stock",
id: "prodictStockInventoryLabel"
})}
value={data.stockQuantity}
type="number"
onChange={onChange}
helperText={
getProductErrorMessage(formErrors.stockQuantity, intl) ||
(product &&
intl.formatMessage(
{
defaultMessage: "Allocated: {quantity}",
description: "allocated product stock"
},
{
quantity: product?.variants[0].quantityAllocated
}
))
}
/>
</div>
</CardContent>
</Card>
);
};
ProductStock.displayName = "ProductStock";
export default ProductStock;

View file

@ -1,2 +0,0 @@
export { default } from "./ProductStock";
export * from "./ProductStock";

View file

@ -0,0 +1,218 @@
import Button from "@material-ui/core/Button";
import Card from "@material-ui/core/Card";
import CardContent from "@material-ui/core/CardContent";
import Table from "@material-ui/core/Table";
import TableHead from "@material-ui/core/TableHead";
import TableBody from "@material-ui/core/TableBody";
import TableCell from "@material-ui/core/TableCell";
import TableRow from "@material-ui/core/TableRow";
import TextField from "@material-ui/core/TextField";
import Typography from "@material-ui/core/Typography";
import React from "react";
import { useIntl, FormattedMessage } from "react-intl";
import makeStyles from "@material-ui/core/styles/makeStyles";
import { FormChange } from "@saleor/hooks/useForm";
import { FormsetChange, FormsetAtomicData } from "@saleor/hooks/useFormset";
import CardTitle from "@saleor/components/CardTitle";
import { getFieldError } from "@saleor/utils/errors";
import { UserError } from "@saleor/types";
import ControlledCheckbox from "@saleor/components/ControlledCheckbox";
import FormSpacer from "@saleor/components/FormSpacer";
import Hr from "@saleor/components/Hr";
import { renderCollection } from "@saleor/misc";
import Link from "@saleor/components/Link";
export type ProductStockInput = FormsetAtomicData<null, string>;
export interface ProductStockFormData {
sku: string;
trackInventory: boolean;
}
export interface ProductStocksProps {
data: ProductStockFormData;
disabled: boolean;
errors: UserError[];
stocks: ProductStockInput[];
onChange: FormsetChange;
onFormDataChange: FormChange;
onWarehousesEdit: () => void;
}
const useStyles = makeStyles(
theme => ({
colName: {},
colQuantity: {
textAlign: "right",
width: 200
},
editWarehouses: {
marginRight: -theme.spacing()
},
input: {
padding: theme.spacing(1.5),
textAlign: "right"
},
inputComponent: {
width: 100
},
quantityContainer: {
paddingTop: theme.spacing()
},
quantityHeader: {
alignItems: "center",
display: "flex",
justifyContent: "space-between"
},
skuInputContainer: {
display: "grid",
gridColumnGap: theme.spacing(3) + "px",
gridTemplateColumns: "repeat(2, 1fr)"
}
}),
{
name: "ProductStocks"
}
);
const ProductStocks: React.FC<ProductStocksProps> = ({
data,
disabled,
errors,
stocks,
onChange,
onFormDataChange,
onWarehousesEdit
}) => {
const classes = useStyles({});
const intl = useIntl();
return (
<Card>
<CardTitle
title={intl.formatMessage({
defaultMessage: "Inventory",
description: "product stock, section header",
id: "productStockHeader"
})}
/>
<CardContent>
<div className={classes.skuInputContainer}>
<TextField
disabled={disabled}
error={!!getFieldError(errors, "sku")}
fullWidth
helperText={getFieldError(errors, "sku")?.message}
label={intl.formatMessage({
defaultMessage: "SKU (Stock Keeping Unit)"
})}
name="sku"
onChange={onFormDataChange}
value={data.sku}
/>
</div>
<FormSpacer />
<ControlledCheckbox
checked={data.trackInventory}
name="trackInventory"
onChange={onFormDataChange}
disabled={disabled}
label={
<>
<FormattedMessage
defaultMessage="Track Inventory"
description="product inventory, checkbox"
/>
<Typography variant="caption">
<FormattedMessage defaultMessage="Active inventory tracking will automatically calculate changes of stock" />
</Typography>
</>
}
/>
</CardContent>
<Hr />
<CardContent className={classes.quantityContainer}>
<Typography>
<div className={classes.quantityHeader}>
<span>
<FormattedMessage
defaultMessage="Quantity"
description="header"
/>
</span>
<Button
className={classes.editWarehouses}
color="primary"
data-cy="edit-warehouses"
onClick={onWarehousesEdit}
>
<FormattedMessage
defaultMessage="Edit Warehouses"
description="button"
/>
</Button>
</div>
</Typography>
</CardContent>
<Table>
<TableHead>
<TableRow>
<TableCell className={classes.colName}>
<FormattedMessage
defaultMessage="Warehouse Name"
description="tabel column header"
/>
</TableCell>
<TableCell className={classes.colQuantity}>
<FormattedMessage
defaultMessage="Quantity Available"
description="tabel column header"
/>
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{renderCollection(
stocks,
stock => (
<TableRow key={stock.id}>
<TableCell className={classes.colName}>{stock.label}</TableCell>
<TableCell className={classes.colQuantity}>
<TextField
className={classes.inputComponent}
disabled={disabled}
fullWidth
inputProps={{
className: classes.input,
min: 0,
type: "number"
}}
onChange={event => onChange(stock.id, event.target.value)}
value={stock.value}
/>
</TableCell>
</TableRow>
),
() => (
<TableRow>
<TableCell colSpan={2}>
<FormattedMessage
defaultMessage={
"This product doesn't have any stock. You can add it <l>here</l>."
}
values={{
l: str => <Link onClick={onWarehousesEdit}>{str}</Link>
}}
/>
</TableCell>
</TableRow>
)
)}
</TableBody>
</Table>
</Card>
);
};
ProductStocks.displayName = "ProductStocks";
export default ProductStocks;

View file

@ -0,0 +1,2 @@
export * from "./ProductStocks";
export { default } from "./ProductStocks";

View file

@ -34,7 +34,8 @@ import {
getProductUpdatePageFormData,
getSelectedAttributesFromProduct,
ProductAttributeValueChoices,
ProductUpdatePageFormData
ProductUpdatePageFormData,
getStockInputFromProduct
} from "../../utils/data";
import {
createAttributeChangeHandler,
@ -45,8 +46,8 @@ import ProductDetailsForm from "../ProductDetailsForm";
import ProductImages from "../ProductImages";
import ProductOrganization from "../ProductOrganization";
import ProductPricing from "../ProductPricing";
import ProductStock from "../ProductStock";
import ProductVariants from "../ProductVariants";
import ProductStocks, { ProductStockInput } from "../ProductStocks";
export interface ProductUpdatePageProps extends ListActions {
errors: ProductErrorFragment[];
@ -63,6 +64,7 @@ export interface ProductUpdatePageProps extends ListActions {
saveButtonBarState: ConfirmButtonTransitionState;
fetchCategories: (query: string) => void;
fetchCollections: (query: string) => void;
onWarehousesEdit: () => void;
onVariantsAdd: () => void;
onVariantShow: (id: string) => () => void;
onImageDelete: (id: string) => () => void;
@ -71,7 +73,6 @@ export interface ProductUpdatePageProps extends ListActions {
onImageEdit?(id: string);
onImageReorder?(event: { oldIndex: number; newIndex: number });
onImageUpload(file: File);
onProductShow?();
onSeoClick?();
onSubmit?(data: ProductUpdatePageSubmitData);
onVariantAdd?();
@ -80,6 +81,7 @@ export interface ProductUpdatePageProps extends ListActions {
export interface ProductUpdatePageSubmitData extends ProductUpdatePageFormData {
attributes: ProductAttributeInput[];
collections: string[];
stocks: ProductStockInput[];
}
export const ProductUpdatePage: React.FC<ProductUpdatePageProps> = ({
@ -108,6 +110,7 @@ export const ProductUpdatePage: React.FC<ProductUpdatePageProps> = ({
onVariantAdd,
onVariantsAdd,
onVariantShow,
onWarehousesEdit,
isChecked,
selected,
toggle,
@ -120,9 +123,13 @@ export const ProductUpdatePage: React.FC<ProductUpdatePageProps> = ({
() => getAttributeInputFromProduct(product),
[product]
);
const stockInput = React.useMemo(() => getStockInputFromProduct(product), [
product
]);
const { change: changeAttributeData, data: attributes } = useFormset(
attributeInput
);
const { change: changeStockData, data: stocks } = useFormset(stockInput);
const [selectedAttributes, setSelectedAttributes] = useStateFromProps<
ProductAttributeValueChoices[]
@ -149,6 +156,7 @@ export const ProductUpdatePage: React.FC<ProductUpdatePageProps> = ({
const handleSubmit = (data: ProductUpdatePageFormData) =>
onSubmit({
attributes,
stocks,
...data
});
@ -239,12 +247,17 @@ export const ProductUpdatePage: React.FC<ProductUpdatePageProps> = ({
toggleAll={toggleAll}
/>
) : (
<ProductStock
<ProductStocks
data={data}
disabled={disabled}
product={product}
onChange={change}
errors={errors}
stocks={stocks}
onChange={(id, value) => {
triggerChange();
changeStockData(id, value);
}}
onFormDataChange={change}
onWarehousesEdit={onWarehousesEdit}
/>
)}
<CardSpacer />

View file

@ -1,123 +0,0 @@
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_errors } 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_errors[] = [
{
__typename: "BulkProductError",
code: ProductErrorCode.UNIQUE,
field: "sku",
index: 3
}
];
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 => (
<Card
style={{
margin: "auto",
overflow: "visible",
width: 800
}}
>
<CardContent>{storyFn()}</CardContent>
</Card>
))
.addDecorator(Decorator)
.add("choose values", () => <ProductVariantCreateContent {...props} />)
.add("prices and SKU", () => (
<ProductVariantCreateContent {...props} step="prices" />
));
storiesOf("Views / Products / Create multiple variants / summary", module)
.addDecorator(storyFn => (
<Card
style={{
margin: "auto",
overflow: "visible",
width: 800
}}
>
<CardContent>{storyFn()}</CardContent>
</Card>
))
.addDecorator(Decorator)
.add("default", () => (
<ProductVariantCreateContent {...props} step="summary" />
))
.add("errors", () => (
<ProductVariantCreateContent {...props} step="summary" errors={errors} />
));
storiesOf("Views / Products / Create multiple variants", module)
.addDecorator(Decorator)
.add("interactive", () => (
<ProductVariantCreateDialog
{...props}
defaultPrice="10.99"
open={true}
onClose={() => undefined}
onSubmit={() => undefined}
/>
));

View file

@ -1,147 +0,0 @@
import { makeStyles } from "@material-ui/core/styles";
import React from "react";
import { ProductDetails_product_productType_variantAttributes } from "@saleor/products/types/ProductDetails";
import { ProductVariantBulkCreate_productVariantBulkCreate_errors } 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 => ({
root: {
maxHeight: 400,
overflowX: "hidden",
overflowY: "scroll",
paddingLeft: theme.spacing(3),
paddingRight: theme.spacing(2),
position: "relative",
right: theme.spacing(3),
width: `calc(100% + ${theme.spacing(3)}px)`
}
}),
{ name: "ProductVariantCreateContent" }
);
export interface ProductVariantCreateContentProps {
attributes: ProductDetails_product_productType_variantAttributes[];
currencySymbol: string;
data: ProductVariantCreateFormData;
dispatchFormDataAction: React.Dispatch<ProductVariantCreateReducerAction>;
errors: ProductVariantBulkCreate_productVariantBulkCreate_errors[];
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 (
<div>
<ProductVariantCreateTabs step={step} onStepClick={onStepClick} />
<div className={classes.root}>
{step === "values" && (
<ProductVariantCreateValues
attributes={selectedAttributes}
data={data}
onValueClick={(attributeId, valueId) =>
dispatchFormDataAction({
attributeId,
type: "selectValue",
valueId
})
}
/>
)}
{step === "prices" && (
<ProductVariantCreatePrices
attributes={selectedAttributes}
currencySymbol={currencySymbol}
data={data}
onApplyPriceOrStockChange={(all, type) =>
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" && (
<ProductVariantCreateSummary
attributes={selectedAttributes}
currencySymbol={currencySymbol}
data={data}
errors={errors}
onVariantDataChange={(variantIndex, field, value) =>
dispatchFormDataAction({
field,
type: "changeVariantData",
value,
variantIndex
})
}
onVariantDelete={variantIndex =>
dispatchFormDataAction({
type: "deleteVariant",
variantIndex
})
}
/>
)}
</div>
</div>
);
};
ProductVariantCreateContent.displayName = "ProductVariantCreateContent";
export default ProductVariantCreateContent;

View file

@ -1,216 +0,0 @@
import Button from "@material-ui/core/Button";
import Dialog from "@material-ui/core/Dialog";
import DialogActions from "@material-ui/core/DialogActions";
import DialogContent from "@material-ui/core/DialogContent";
import DialogTitle from "@material-ui/core/DialogTitle";
import { makeStyles } from "@material-ui/core/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 => ({
button: {
marginLeft: theme.spacing(2)
},
content: {
overflowX: "visible",
overflowY: "hidden",
width: 800
},
spacer: {
flex: 1
}
}),
{ name: "ProductVariantCreateDialog" }
);
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<ProductVariantCreateStep>("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 (
<Dialog open={open} maxWidth="md">
<DialogTitle>
<FormattedMessage
defaultMessage="Assign Attribute"
description="dialog header"
/>
</DialogTitle>
<DialogContent className={classes.content}>
<ProductVariantCreateContent
{...contentProps}
attributes={attributes}
data={data}
dispatchFormDataAction={dispatchFormDataAction}
errors={errors}
step={step}
onStepClick={step => setStep(step)}
/>
</DialogContent>
<DialogActions>
<Button className={classes.button} onClick={onClose}>
<FormattedMessage defaultMessage="Cancel" description="button" />
</Button>
<div className={classes.spacer} />
{step !== "values" && (
<Button
className={classes.button}
color="primary"
onClick={handlePrevStep}
>
<FormattedMessage
defaultMessage="Previous"
description="previous step, button"
/>
</Button>
)}
{step !== "summary" ? (
<Button
className={classes.button}
color="primary"
disabled={!canHitNext(step, data)}
variant="contained"
onClick={handleNextStep}
>
<FormattedMessage defaultMessage="Next" description="button" />
</Button>
) : (
<Button
className={classes.button}
color="primary"
disabled={!canHitNext(step, data)}
variant="contained"
onClick={() => onSubmit(data.variants)}
>
<FormattedMessage
defaultMessage="Create"
description="create multiple variants, button"
/>
</Button>
)}
</DialogActions>
</Dialog>
);
};
ProductVariantCreateDialog.displayName = "ProductVariantCreateDialog";
export default ProductVariantCreateDialog;

View file

@ -1,315 +0,0 @@
import FormControlLabel from "@material-ui/core/FormControlLabel";
import Radio from "@material-ui/core/Radio";
import RadioGroup from "@material-ui/core/RadioGroup";
import { makeStyles } from "@material-ui/core/styles";
import TextField from "@material-ui/core/TextField";
import Typography from "@material-ui/core/Typography";
import React from "react";
import { FormattedMessage, useIntl } from "react-intl";
import 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 => ({
hr: {
marginBottom: theme.spacing(),
marginTop: theme.spacing(0.5)
},
hrAttribute: {
marginTop: theme.spacing(2)
},
label: {
alignSelf: "center"
},
shortInput: {
width: "50%"
}
}),
{ name: "ProductVariantCreatePrices" }
);
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 (
<>
<Typography color="textSecondary" variant="h5">
<FormattedMessage
defaultMessage="Price"
description="variant price, header"
/>
</Typography>
<Hr className={classes.hr} />
<RadioGroup value={data.price.all ? "applyToAll" : "applyToAttribute"}>
<FormControlLabel
value="applyToAll"
control={<Radio color="primary" />}
label={intl.formatMessage({
defaultMessage: "Apply single price to all SKUs"
})}
onChange={() => onApplyPriceOrStockChange(true, "price")}
/>
<FormSpacer />
<TextField
className={classes.shortInput}
inputProps={{
min: 0,
type: "number"
}}
InputProps={{
endAdornment: currencySymbol
}}
label={intl.formatMessage({
defaultMessage: "Price",
id: "productVariantCreatePricesPriceInputLabel"
})}
value={data.price.value}
onChange={event => onApplyToAllChange(event.target.value, "price")}
/>
<FormSpacer />
<FormControlLabel
value="applyToAttribute"
control={<Radio color="primary" />}
label={intl.formatMessage({
defaultMessage: "Apply unique prices by attribute to each SKU"
})}
onChange={() => onApplyPriceOrStockChange(false, "price")}
/>
</RadioGroup>
{!data.price.all && (
<>
<FormSpacer />
<Grid variant="uniform">
<div className={classes.label}>
<Typography>
<FormattedMessage
defaultMessage="Choose attribute"
description="variant attribute"
/>
</Typography>
</div>
<div>
<SingleSelectField
choices={attributeChoices}
label={intl.formatMessage({
defaultMessage: "Attribute",
description: "variant attribute"
})}
value={data.price.attribute}
onChange={event =>
onAttributeSelect(event.target.value, "price")
}
/>
</div>
</Grid>
<Hr className={classes.hrAttribute} />
{priceAttributeValues &&
priceAttributeValues.map(attributeValue => (
<React.Fragment key={attributeValue.id}>
<FormSpacer />
<Grid variant="uniform">
<div className={classes.label}>
<Typography>{attributeValue.name}</Typography>
</div>
<div>
<TextField
label={intl.formatMessage({
defaultMessage: "Price",
description: "variant price",
id: "productVariantCreatePricesSetPricePlaceholder"
})}
inputProps={{
min: 0,
type: "number"
}}
InputProps={{
endAdornment: currencySymbol
}}
fullWidth
value={
data.price.values.find(
value => value.slug === attributeValue.slug
).value
}
onChange={event =>
onAttributeValueChange(
attributeValue.slug,
event.target.value,
"price"
)
}
/>
</div>
</Grid>
</React.Fragment>
))}
</>
)}
<FormSpacer />
<Typography color="textSecondary" variant="h5">
<FormattedMessage
defaultMessage="Stock"
description="variant stock, header"
/>
</Typography>
<Hr className={classes.hr} />
<RadioGroup value={data.stock.all ? "applyToAll" : "applyToAttribute"}>
<FormControlLabel
value="applyToAll"
control={<Radio color="primary" />}
label={intl.formatMessage({
defaultMessage: "Apply single stock to all SKUs"
})}
onChange={() => onApplyPriceOrStockChange(true, "stock")}
/>
<FormSpacer />
<TextField
className={classes.shortInput}
inputProps={{
min: 0,
type: "number"
}}
label={intl.formatMessage({
defaultMessage: "Stock",
id: "productVariantCreatePricesStockInputLabel"
})}
value={data.stock.value}
onChange={event => onApplyToAllChange(event.target.value, "stock")}
/>
<FormSpacer />
<FormControlLabel
value="applyToAttribute"
control={<Radio color="primary" />}
label={intl.formatMessage({
defaultMessage: "Apply unique stock by attribute to each SKU"
})}
onChange={() => onApplyPriceOrStockChange(false, "stock")}
/>
</RadioGroup>
{!data.stock.all && (
<>
<FormSpacer />
<Grid variant="uniform">
<div className={classes.label}>
<Typography>
<FormattedMessage
defaultMessage="Choose attribute"
description="variant attribute"
/>
</Typography>
</div>
<div>
<SingleSelectField
choices={attributeChoices}
label={intl.formatMessage({
defaultMessage: "Attribute",
description: "variant attribute"
})}
value={data.stock.attribute}
onChange={event =>
onAttributeSelect(event.target.value, "stock")
}
/>
</div>
</Grid>
<Hr className={classes.hrAttribute} />
{stockAttributeValues &&
stockAttributeValues.map(attributeValue => (
<React.Fragment key={attributeValue.id}>
<FormSpacer />
<Grid variant="uniform">
<div className={classes.label}>
<Typography>{attributeValue.name}</Typography>
</div>
<div>
<TextField
label={intl.formatMessage({
defaultMessage: "Stock",
description: "variant stock",
id: "productVariantCreatePricesSetStockPlaceholder"
})}
fullWidth
value={
data.stock.values.find(
value => value.slug === attributeValue.slug
).value
}
onChange={event =>
onAttributeValueChange(
attributeValue.slug,
event.target.value,
"stock"
)
}
/>
</div>
</Grid>
</React.Fragment>
))}
</>
)}
</>
);
};
ProductVariantCreatePrices.displayName = "ProductVariantCreatePrices";
export default ProductVariantCreatePrices;

View file

@ -1,81 +0,0 @@
import makeStyles from "@material-ui/core/styles/makeStyles";
import Typography from "@material-ui/core/Typography";
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 => ({
hr: {
marginBottom: theme.spacing(),
marginTop: theme.spacing(0.5)
},
valueContainer: {
display: "grid",
gridColumnGap: theme.spacing(3),
gridTemplateColumns: "repeat(3, 1fr)",
marginBottom: theme.spacing(3)
}
}),
{ name: "ProductVariantCreateValues" }
);
const ProductVariantCreateValues: React.FC<
ProductVariantCreateValuesProps
> = props => {
const { attributes, data, onValueClick } = props;
const classes = useStyles(props);
return (
<>
{attributes.map(attribute => (
<React.Fragment key={attribute.id}>
<Typography color="textSecondary" variant="h5">
{maybe<React.ReactNode>(() => attribute.name, <Skeleton />)}
</Typography>
<Hr className={classes.hr} />
<div className={classes.valueContainer}>
{attribute.values.map(value => (
<Debounce
debounceFn={() => onValueClick(attribute.id, value.slug)}
time={100}
key={value.slug}
>
{change => (
<ControlledCheckbox
checked={isSelected(
value.slug,
data.attributes.find(
dataAttribute => attribute.id === dataAttribute.id
).values,
(a, b) => a === b
)}
name={`value:${value.slug}`}
label={value.name}
onChange={change}
/>
)}
</Debounce>
))}
</div>
</React.Fragment>
))}
</>
);
};
ProductVariantCreateValues.displayName = "ProductVariantCreateValues";
export default ProductVariantCreateValues;

View file

@ -1,185 +0,0 @@
import { attributes, fourthStep, secondStep, thirdStep } from "./fixtures";
import reducer, { VariantField } from "./reducer";
function execActions<TState, TAction>(
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);
});
});

View file

@ -1,362 +0,0 @@
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;
}
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;

View file

@ -1 +0,0 @@
export type ProductVariantCreateStep = "values" | "prices" | "summary";

View file

@ -15,6 +15,7 @@ import useFormset, {
} from "@saleor/hooks/useFormset";
import { getVariantAttributeInputFromProduct } from "@saleor/products/utils/data";
import { ProductErrorFragment } from "@saleor/attributes/types/ProductErrorFragment";
import { SearchWarehouses_search_edges_node } from "@saleor/searches/types/SearchWarehouses";
import { maybe } from "../../../misc";
import { ProductVariantCreateData_product } from "../../types/ProductVariantCreateData";
import ProductVariantAttributes, {
@ -22,7 +23,7 @@ import ProductVariantAttributes, {
} from "../ProductVariantAttributes";
import ProductVariantNavigation from "../ProductVariantNavigation";
import ProductVariantPrice from "../ProductVariantPrice";
import ProductVariantStock from "../ProductVariantStock";
import ProductStocks, { ProductStockInput } from "../ProductStocks";
interface ProductVariantCreatePageFormData {
costPrice: string;
@ -30,35 +31,41 @@ interface ProductVariantCreatePageFormData {
priceOverride: string;
quantity: string;
sku: string;
trackInventory: boolean;
}
export interface ProductVariantCreatePageSubmitData
extends ProductVariantCreatePageFormData {
attributes: FormsetData<VariantAttributeInputData>;
stocks: ProductStockInput[];
}
interface ProductVariantCreatePageProps {
currencySymbol: string;
disabled: boolean;
errors: ProductErrorFragment[];
header: string;
loading: boolean;
product: ProductVariantCreateData_product;
saveButtonBarState: ConfirmButtonTransitionState;
warehouses: SearchWarehouses_search_edges_node[];
onBack: () => void;
onSubmit: (data: ProductVariantCreatePageSubmitData) => void;
onVariantClick: (variantId: string) => void;
onWarehouseEdit: () => void;
}
const ProductVariantCreatePage: React.FC<ProductVariantCreatePageProps> = ({
currencySymbol,
disabled,
errors,
loading,
header,
product,
saveButtonBarState,
warehouses,
onBack,
onSubmit,
onVariantClick
onVariantClick,
onWarehouseEdit
}) => {
const intl = useIntl();
const attributeInput = React.useMemo(
@ -68,28 +75,33 @@ const ProductVariantCreatePage: React.FC<ProductVariantCreatePageProps> = ({
const { change: changeAttributeData, data: attributes } = useFormset(
attributeInput
);
const { change: changeStockData, data: stocks, set: setStocks } = useFormset<
null
>([]);
React.useEffect(() => {
const newStocks = warehouses.map(warehouse => ({
data: null,
id: warehouse.id,
label: warehouse.name,
value: stocks.find(stock => stock.id === warehouse.id)?.value || 0
}));
setStocks(newStocks);
}, [JSON.stringify(warehouses)]);
const initialForm = {
attributes: maybe(
() =>
product.productType.variantAttributes.map(attribute => ({
name: attribute.name,
slug: attribute.slug,
values: [""]
})),
[]
),
const initialForm: ProductVariantCreatePageFormData = {
costPrice: "",
images: maybe(() => product.images.map(image => image.id)),
priceOverride: "",
quantity: "0",
sku: ""
sku: "",
trackInventory: true
};
const handleSubmit = (data: ProductVariantCreatePageFormData) =>
onSubmit({
...data,
attributes
attributes,
stocks
});
return (
@ -119,7 +131,7 @@ const ProductVariantCreatePage: React.FC<ProductVariantCreatePageProps> = ({
<div>
<ProductVariantAttributes
attributes={attributes}
disabled={loading}
disabled={disabled}
errors={errors}
onChange={handleAttributeChange}
/>
@ -129,21 +141,23 @@ const ProductVariantCreatePage: React.FC<ProductVariantCreatePageProps> = ({
priceOverride={data.priceOverride}
currencySymbol={currencySymbol}
costPrice={data.costPrice}
loading={loading}
loading={disabled}
onChange={change}
/>
<CardSpacer />
<ProductVariantStock
<ProductStocks
data={data}
disabled={disabled}
onChange={changeStockData}
onFormDataChange={change}
errors={errors}
sku={data.sku}
quantity={data.quantity}
loading={loading}
onChange={change}
stocks={stocks}
onWarehousesEdit={onWarehouseEdit}
/>
</div>
</Grid>
<SaveButtonBar
disabled={loading || !onSubmit || !hasChanged}
disabled={disabled || !onSubmit || !hasChanged}
labels={{
delete: intl.formatMessage({
defaultMessage: "Delete Variant",

View file

@ -0,0 +1,170 @@
import { storiesOf } from "@storybook/react";
import React from "react";
import { attributes } from "@saleor/attributes/fixtures";
import { ProductVariantBulkCreate_productVariantBulkCreate_errors } from "@saleor/products/types/ProductVariantBulkCreate";
import { ProductErrorCode } from "@saleor/types/globalTypes";
import Container from "@saleor/components/Container";
import { warehouseList } from "@saleor/warehouses/fixtures";
import Decorator from "../../../storybook/Decorator";
import { createVariants } from "./createVariants";
import { AllOrAttribute, ProductVariantCreateFormData } from "./form";
import ProductVariantCreatorContent, {
ProductVariantCreatorContentProps
} from "./ProductVariantCreatorContent";
import ProductVariantCreatorPage from "./ProductVariantCreatorPage";
import { ProductVariantCreatorStep } from "./types";
const selectedAttributes = [1, 4, 5].map(index => attributes[index]);
const selectedWarehouses = [0, 1, 3].map(index => warehouseList[index]);
const price: AllOrAttribute<string> = {
attribute: selectedAttributes[0].id,
mode: "attribute",
value: "2.79",
values: selectedAttributes[0].values.map((attribute, attributeIndex) => ({
slug: attribute.slug,
value: (attributeIndex + 4).toFixed(2)
}))
};
const stock: AllOrAttribute<number[]> = {
attribute: selectedAttributes[0].id,
mode: "attribute",
value: selectedWarehouses.map(
(_, warehouseIndex) => (warehouseIndex + 2) * 3
),
values: selectedAttributes[0].values.map((attribute, attributeIndex) => ({
slug: attribute.slug,
value: selectedWarehouses.map(
(_, warehouseIndex) =>
selectedAttributes.length * 10 - attributeIndex - warehouseIndex * 3
)
}))
};
const dataAttributes = selectedAttributes.map(attribute => ({
id: attribute.id,
values: attribute.values
.map(value => value.slug)
.filter((_, valueIndex) => valueIndex % 2 !== 1)
}));
const errors: ProductVariantBulkCreate_productVariantBulkCreate_errors[] = [
{
__typename: "BulkProductError",
code: ProductErrorCode.UNIQUE,
field: "sku",
index: 3
}
];
const data: ProductVariantCreateFormData = {
attributes: dataAttributes,
price,
stock,
variants: createVariants({
attributes: dataAttributes,
price,
stock,
variants: [],
warehouses: selectedWarehouses.map(warehouse => warehouse.id)
}),
warehouses: selectedWarehouses.map(warehouse => warehouse.id)
};
const props: ProductVariantCreatorContentProps = {
attributes: [0, 1, 4, 6].map(index => attributes[index]),
currencySymbol: "USD",
data: {
...data,
variants: createVariants(data)
},
dispatchFormDataAction: () => undefined,
errors: [],
step: ProductVariantCreatorStep.values,
warehouses: warehouseList
};
storiesOf("Views / Products / Create multiple variants", module)
.addDecorator(storyFn => <Container>{storyFn()}</Container>)
.addDecorator(Decorator)
.add("choose values", () => <ProductVariantCreatorContent {...props} />);
storiesOf(
"Views / Products / Create multiple variants / prices and SKUs",
module
)
.addDecorator(storyFn => <Container>{storyFn()}</Container>)
.addDecorator(Decorator)
.add("apply to all", () => (
<ProductVariantCreatorContent
{...props}
data={{
...data,
stock: {
...data.stock,
mode: "all"
}
}}
step={ProductVariantCreatorStep.prices}
/>
))
.add("apply to all when one warehouse", () => (
<ProductVariantCreatorContent
{...props}
data={{
...data,
stock: {
...data.stock,
mode: "all"
},
warehouses: [data.warehouses[0]]
}}
step={ProductVariantCreatorStep.prices}
warehouses={[props.warehouses[0]]}
/>
))
.add("apply to attribute", () => (
<ProductVariantCreatorContent
{...props}
step={ProductVariantCreatorStep.prices}
/>
))
.add("apply to attribute when one warehouse", () => (
<ProductVariantCreatorContent
{...props}
data={{
...data,
warehouses: [data.warehouses[0]]
}}
step={ProductVariantCreatorStep.prices}
warehouses={[props.warehouses[0]]}
/>
));
storiesOf("Views / Products / Create multiple variants / summary", module)
.addDecorator(storyFn => <Container>{storyFn()}</Container>)
.addDecorator(Decorator)
.add("default", () => (
<ProductVariantCreatorContent
{...props}
step={ProductVariantCreatorStep.summary}
/>
))
.add("errors", () => (
<ProductVariantCreatorContent
{...props}
step={ProductVariantCreatorStep.summary}
errors={errors}
/>
));
storiesOf("Views / Products / Create multiple variants", module)
.addDecorator(Decorator)
.add("interactive", () => (
<ProductVariantCreatorPage
{...props}
defaultPrice="10.99"
onSubmit={() => undefined}
/>
));

View file

@ -0,0 +1,183 @@
import React from "react";
import { ProductDetails_product_productType_variantAttributes } from "@saleor/products/types/ProductDetails";
import { ProductVariantBulkCreate_productVariantBulkCreate_errors } from "@saleor/products/types/ProductVariantBulkCreate";
import { WarehouseFragment } from "@saleor/warehouses/types/WarehouseFragment";
import { isSelected } from "@saleor/utils/lists";
import { ProductVariantCreateFormData } from "./form";
import ProductVariantCreatePriceAndSku from "./ProductVariantCreatorPriceAndSku";
import ProductVariantCreateSummary from "./ProductVariantCreatorSummary";
import ProductVariantCreateValues from "./ProductVariantCreatorValues";
import {
ProductVariantCreateReducerAction,
ProductVariantCreateReducerActionType
} from "./reducer";
import { ProductVariantCreatorStep } from "./types";
export interface ProductVariantCreatorContentProps {
attributes: ProductDetails_product_productType_variantAttributes[];
currencySymbol: string;
data: ProductVariantCreateFormData;
dispatchFormDataAction: React.Dispatch<ProductVariantCreateReducerAction>;
errors: ProductVariantBulkCreate_productVariantBulkCreate_errors[];
step: ProductVariantCreatorStep;
warehouses: WarehouseFragment[];
}
const ProductVariantCreatorContent: React.FC<ProductVariantCreatorContentProps> = ({
attributes,
currencySymbol,
data,
dispatchFormDataAction,
errors,
step,
warehouses
}) => {
const selectedAttributes = attributes.filter(attribute =>
isSelected(
attribute.id,
data.attributes.map(dataAttribute => dataAttribute.id),
(a, b) => a === b
)
);
return (
<>
{step === ProductVariantCreatorStep.values && (
<ProductVariantCreateValues
attributes={selectedAttributes}
data={data}
onValueClick={(attributeId, valueId) =>
dispatchFormDataAction({
selectValue: {
attributeId,
valueId
},
type: ProductVariantCreateReducerActionType.selectValue
})
}
/>
)}
{step === ProductVariantCreatorStep.prices && (
<ProductVariantCreatePriceAndSku
attributes={selectedAttributes}
currencySymbol={currencySymbol}
data={data}
warehouses={warehouses}
onApplyToAllChange={(mode, type) =>
dispatchFormDataAction({
applyPriceOrStockToAll: {
mode
},
type:
type === "price"
? ProductVariantCreateReducerActionType.applyPriceToAll
: ProductVariantCreateReducerActionType.applyStockToAll
})
}
onApplyToAllPriceChange={price =>
dispatchFormDataAction({
changeApplyPriceToAllValue: {
price
},
type:
ProductVariantCreateReducerActionType.changeApplyPriceToAllValue
})
}
onApplyToAllStockChange={(quantity, warehouseIndex) =>
dispatchFormDataAction({
changeApplyStockToAllValue: {
quantity,
warehouseIndex
},
type:
ProductVariantCreateReducerActionType.changeApplyStockToAllValue
})
}
onAttributeSelect={(attributeId, type) =>
dispatchFormDataAction({
changeApplyPriceOrStockToAttributeId: {
attributeId
},
type:
type === "price"
? ProductVariantCreateReducerActionType.changeApplyPriceToAttributeId
: ProductVariantCreateReducerActionType.changeApplyStockToAttributeId
})
}
onAttributePriceChange={(valueId, price) =>
dispatchFormDataAction({
changeAttributeValuePrice: {
price,
valueId
},
type:
ProductVariantCreateReducerActionType.changeAttributeValuePrice
})
}
onAttributeStockChange={(valueId, quantity, warehouseIndex) =>
dispatchFormDataAction({
changeAttributeValueStock: {
quantity,
valueId,
warehouseIndex
},
type:
ProductVariantCreateReducerActionType.changeAttributeValueStock
})
}
onWarehouseToggle={warehouseId =>
dispatchFormDataAction({
changeWarehouses: {
warehouseId
},
type: ProductVariantCreateReducerActionType.changeWarehouses
})
}
/>
)}
{step === ProductVariantCreatorStep.summary && (
<ProductVariantCreateSummary
attributes={selectedAttributes}
currencySymbol={currencySymbol}
data={data}
errors={errors}
onVariantDataChange={(variantIndex, field, value) =>
dispatchFormDataAction({
changeVariantData: {
field,
value,
variantIndex
},
type: ProductVariantCreateReducerActionType.changeVariantData
})
}
onVariantStockDataChange={(variantIndex, warehouse, value) =>
dispatchFormDataAction({
changeVariantStockData: {
stock: {
quantity: parseInt(value, 10),
warehouse
},
variantIndex
},
type: ProductVariantCreateReducerActionType.changeVariantStockData
})
}
onVariantDelete={variantIndex =>
dispatchFormDataAction({
deleteVariant: {
variantIndex
},
type: ProductVariantCreateReducerActionType.deleteVariant
})
}
warehouses={warehouses}
/>
)}
</>
);
};
ProductVariantCreatorContent.displayName = "ProductVariantCreatorContent";
export default ProductVariantCreatorContent;

View file

@ -0,0 +1,233 @@
import Button from "@material-ui/core/Button";
import Typography from "@material-ui/core/Typography";
import { makeStyles } from "@material-ui/core/styles";
import React from "react";
import { FormattedMessage, useIntl, IntlShape } from "react-intl";
import useWizard from "@saleor/hooks/useWizard";
import PageHeader from "@saleor/components/PageHeader";
import Container from "@saleor/components/Container";
import Hr from "@saleor/components/Hr";
import { ProductVariantBulkCreateInput } from "../../../types/globalTypes";
import { createInitialForm, ProductVariantCreateFormData } from "./form";
import ProductVariantCreatorContent, {
ProductVariantCreatorContentProps
} from "./ProductVariantCreatorContent";
import reduceProductVariantCreateFormData, {
ProductVariantCreateReducerActionType
} from "./reducer";
import { ProductVariantCreatorStep } from "./types";
import ProductVariantCreateTabs from "./ProductVariantCreatorTabs";
const useStyles = makeStyles(
theme => ({
button: {
marginLeft: theme.spacing(2)
},
content: {
overflowX: "visible",
overflowY: "hidden",
width: 800
},
description: {
marginTop: theme.spacing()
},
hr: {
margin: theme.spacing(3, 0)
}
}),
{ name: "ProductVariantCreatePage" }
);
function canHitNext(
step: ProductVariantCreatorStep,
data: ProductVariantCreateFormData
): boolean {
switch (step) {
case ProductVariantCreatorStep.values:
return data.attributes.every(attribute => attribute.values.length > 0);
case ProductVariantCreatorStep.prices:
if (data.price.mode === "all") {
if (data.price.value === "") {
return false;
}
} else if (data.price.mode === "attribute") {
if (
data.price.attribute === "" ||
data.price.values.some(attributeValue => attributeValue.value === "")
) {
return false;
}
}
if (data.stock.mode === "attribute" && data.stock.attribute === "") {
return false;
}
return true;
case ProductVariantCreatorStep.summary:
return data.variants.every(variant => variant.sku !== "");
default:
return false;
}
}
export interface ProductVariantCreatePageProps
extends Omit<
ProductVariantCreatorContentProps,
"data" | "dispatchFormDataAction" | "step" | "onStepClick"
> {
defaultPrice: string;
onSubmit: (data: ProductVariantBulkCreateInput[]) => void;
}
function getTitle(step: ProductVariantCreatorStep, intl: IntlShape): string {
switch (step) {
case ProductVariantCreatorStep.values:
return intl.formatMessage({
defaultMessage: "Choose Values",
description: "product attribute values, page title"
});
case ProductVariantCreatorStep.prices:
return intl.formatMessage({
defaultMessage: "Price and SKUs",
description: "page title"
});
case ProductVariantCreatorStep.summary:
return intl.formatMessage({
defaultMessage: "Summary",
description: "page title"
});
}
}
function getDescription(
step: ProductVariantCreatorStep,
intl: IntlShape
): string {
switch (step) {
case ProductVariantCreatorStep.values:
return intl.formatMessage({
defaultMessage:
"Selected values will be used to create variants for the configurable product."
});
case ProductVariantCreatorStep.prices:
return intl.formatMessage({
defaultMessage:
"Based on your selections we will create 8 products. Use this step to customize price and stocks for your new products."
});
case ProductVariantCreatorStep.summary:
return intl.formatMessage({
defaultMessage:
"Here is the summary of variants that will be created. You can change prices, stocks an SKU for each one created."
});
}
}
const ProductVariantCreatePage: React.FC<ProductVariantCreatePageProps> = props => {
const {
attributes,
defaultPrice,
errors,
onSubmit,
warehouses,
...contentProps
} = props;
const classes = useStyles(props);
const intl = useIntl();
const [wizardData, dispatchFormDataAction] = React.useReducer(
reduceProductVariantCreateFormData,
createInitialForm(attributes, defaultPrice, warehouses)
);
const [step, { next: nextStep, prev: prevStep, set: setStep }] = useWizard<
ProductVariantCreatorStep
>(
ProductVariantCreatorStep.values,
[
ProductVariantCreatorStep.values,
ProductVariantCreatorStep.prices,
ProductVariantCreatorStep.summary
],
{
onTransition: (_, nextStep) => {
if (nextStep === ProductVariantCreatorStep.summary) {
dispatchFormDataAction({
type: ProductVariantCreateReducerActionType.reload
});
}
}
}
);
const reloadForm = () =>
dispatchFormDataAction({
reload: {
data: createInitialForm(attributes, defaultPrice, warehouses)
},
type: ProductVariantCreateReducerActionType.reload
});
React.useEffect(reloadForm, [attributes.length, warehouses.length]);
return (
<Container>
<ProductVariantCreateTabs step={step} onStepClick={setStep} />
<PageHeader
title={
<>
{getTitle(step, intl)}
<Typography className={classes.description} variant="body2">
{getDescription(step, intl)}
</Typography>
</>
}
>
{step !== ProductVariantCreatorStep.values && (
<Button className={classes.button} color="primary" onClick={prevStep}>
<FormattedMessage
defaultMessage="Previous"
description="previous step, button"
/>
</Button>
)}
{step !== ProductVariantCreatorStep.summary ? (
<Button
className={classes.button}
color="primary"
disabled={!canHitNext(step, wizardData)}
variant="contained"
onClick={nextStep}
>
<FormattedMessage defaultMessage="Next" description="button" />
</Button>
) : (
<Button
className={classes.button}
color="primary"
disabled={!canHitNext(step, wizardData)}
variant="contained"
onClick={() => onSubmit(wizardData.variants)}
>
<FormattedMessage
defaultMessage="Create"
description="create multiple variants, button"
/>
</Button>
)}
</PageHeader>
<Hr className={classes.hr} />
<ProductVariantCreatorContent
{...contentProps}
attributes={attributes}
data={wizardData}
dispatchFormDataAction={dispatchFormDataAction}
errors={errors}
step={step}
warehouses={warehouses}
/>
</Container>
);
};
ProductVariantCreatePage.displayName = "ProductVariantCreatePage";
export default ProductVariantCreatePage;

View file

@ -0,0 +1,74 @@
import React from "react";
import { ProductDetails_product_productType_variantAttributes } from "@saleor/products/types/ProductDetails";
import CardSpacer from "@saleor/components/CardSpacer";
import { WarehouseFragment } from "@saleor/warehouses/types/WarehouseFragment";
import {
ProductVariantCreateFormData,
VariantCreatorPricesAndSkuMode
} from "./form";
import ProductVariantCreatorPrices from "./ProductVariantCreatorPrices";
import ProductVariantCreatorStock from "./ProductVariantCreatorStock";
export type PriceOrStock = "price" | "stock";
export interface ProductVariantCreatorPriceAndSkuProps {
attributes: ProductDetails_product_productType_variantAttributes[];
currencySymbol: string;
data: ProductVariantCreateFormData;
warehouses: WarehouseFragment[];
onApplyToAllChange: (
value: VariantCreatorPricesAndSkuMode,
type: PriceOrStock
) => void;
onApplyToAllPriceChange: (value: string) => void;
onApplyToAllStockChange: (quantity: number, warehouseIndex: number) => void;
onAttributeSelect: (id: string, type: PriceOrStock) => void;
onAttributePriceChange: (id: string, value: string) => void;
onAttributeStockChange: (
id: string,
quantity: number,
warehouseIndex: number
) => void;
onWarehouseToggle: (id: string) => void;
}
const ProductVariantCreatorPriceAndSku: React.FC<ProductVariantCreatorPriceAndSkuProps> = ({
attributes,
currencySymbol,
data,
warehouses,
onApplyToAllChange,
onApplyToAllPriceChange,
onApplyToAllStockChange,
onAttributeSelect,
onAttributePriceChange,
onAttributeStockChange,
onWarehouseToggle
}) => (
<>
<ProductVariantCreatorPrices
attributes={attributes}
currencySymbol={currencySymbol}
data={data}
onApplyToAllChange={value => onApplyToAllChange(value, "price")}
onApplyToAllPriceChange={onApplyToAllPriceChange}
onAttributeSelect={id => onAttributeSelect(id, "price")}
onAttributeValueChange={onAttributePriceChange}
/>
<CardSpacer />
<ProductVariantCreatorStock
attributes={attributes}
data={data}
warehouses={warehouses}
onApplyToAllChange={value => onApplyToAllChange(value, "stock")}
onApplyToAllStockChange={onApplyToAllStockChange}
onAttributeSelect={id => onAttributeSelect(id, "stock")}
onAttributeValueChange={onAttributeStockChange}
onWarehouseToggle={onWarehouseToggle}
/>
</>
);
ProductVariantCreatorPriceAndSku.displayName =
"ProductVariantCreatorPriceAndSku";
export default ProductVariantCreatorPriceAndSku;

View file

@ -0,0 +1,189 @@
import FormControlLabel from "@material-ui/core/FormControlLabel";
import Card from "@material-ui/core/Card";
import CardContent from "@material-ui/core/CardContent";
import Radio from "@material-ui/core/Radio";
import RadioGroup from "@material-ui/core/RadioGroup";
import { makeStyles } from "@material-ui/core/styles";
import TextField from "@material-ui/core/TextField";
import Typography from "@material-ui/core/Typography";
import React from "react";
import { FormattedMessage, useIntl } from "react-intl";
import 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 CardTitle from "@saleor/components/CardTitle";
import {
ProductVariantCreateFormData,
VariantCreatorPricesAndSkuMode
} from "./form";
import { getPriceAttributeValues } from "./utils";
const useStyles = makeStyles(
theme => ({
hr: {
marginBottom: theme.spacing(),
marginTop: theme.spacing(0.5)
},
hrAttribute: {
marginTop: theme.spacing(2)
},
label: {
alignSelf: "center"
},
shortInput: {
width: "33%"
}
}),
{ name: "ProductVariantCreatorPrices" }
);
export interface ProductVariantCreatorPricesProps {
attributes: ProductDetails_product_productType_variantAttributes[];
currencySymbol: string;
data: ProductVariantCreateFormData;
onApplyToAllChange: (applyToAll: VariantCreatorPricesAndSkuMode) => void;
onApplyToAllPriceChange: (value: string) => void;
onAttributeSelect: (id: string) => void;
onAttributeValueChange: (id: string, value: string) => void;
}
const ProductVariantCreatorPrices: React.FC<ProductVariantCreatorPricesProps> = props => {
const {
attributes,
currencySymbol,
data,
onApplyToAllChange,
onApplyToAllPriceChange,
onAttributeSelect,
onAttributeValueChange
} = props;
const classes = useStyles(props);
const intl = useIntl();
const attributeChoices = attributes.map(attribute => ({
label: attribute.name,
value: attribute.id
}));
const priceAttributeValues = getPriceAttributeValues(data, attributes);
return (
<Card>
<CardTitle
title={intl.formatMessage({
defaultMessage: "Price",
description: "variant price, header"
})}
/>
<CardContent>
<RadioGroup value={data.price.mode}>
<FormControlLabel
value="all"
control={<Radio color="primary" />}
label={intl.formatMessage({
defaultMessage: "Apply single price to all SKUs"
})}
onChange={() => onApplyToAllChange("all")}
/>
<FormSpacer />
<TextField
className={classes.shortInput}
inputProps={{
min: 0,
type: "number"
}}
InputProps={{
endAdornment: currencySymbol
}}
label={intl.formatMessage({
defaultMessage: "Price",
id: "productVariantCreatePricesPriceInputLabel"
})}
value={data.price.value}
onChange={event => onApplyToAllPriceChange(event.target.value)}
/>
<FormSpacer />
<FormControlLabel
value="attribute"
control={<Radio color="primary" />}
label={intl.formatMessage({
defaultMessage: "Apply unique prices by attribute to each SKU"
})}
onChange={() => onApplyToAllChange("attribute")}
/>
</RadioGroup>
{data.price.mode === "attribute" && (
<>
<FormSpacer />
<Grid variant="uniform">
<div className={classes.label}>
<Typography>
<FormattedMessage
defaultMessage="Choose attribute"
description="variant attribute"
/>
</Typography>
</div>
<div>
<SingleSelectField
choices={attributeChoices}
label={intl.formatMessage({
defaultMessage: "Attribute",
description: "variant attribute"
})}
value={data.price.attribute}
onChange={event => onAttributeSelect(event.target.value)}
/>
</div>
</Grid>
{priceAttributeValues &&
priceAttributeValues.map(attributeValue => (
<React.Fragment key={attributeValue.id}>
<Hr className={classes.hrAttribute} />
<FormSpacer />
<Grid variant="uniform">
<div className={classes.label}>
<Typography>{attributeValue.name}</Typography>
</div>
<div>
<TextField
label={intl.formatMessage({
defaultMessage: "Price",
description: "variant price",
id: "productVariantCreatePricesSetPricePlaceholder"
})}
inputProps={{
min: 0,
type: "number"
}}
InputProps={{
endAdornment: currencySymbol
}}
fullWidth
value={
data.price.values.find(
value => value.slug === attributeValue.slug
).value
}
onChange={event =>
onAttributeValueChange(
attributeValue.slug,
event.target.value
)
}
/>
</div>
</Grid>
</React.Fragment>
))}
</>
)}
</CardContent>
</Card>
);
};
ProductVariantCreatorPrices.displayName = "ProductVariantCreatorPrices";
export default ProductVariantCreatorPrices;

View file

@ -0,0 +1,318 @@
import FormControlLabel from "@material-ui/core/FormControlLabel";
import Card from "@material-ui/core/Card";
import CardContent from "@material-ui/core/CardContent";
import Radio from "@material-ui/core/Radio";
import RadioGroup from "@material-ui/core/RadioGroup";
import { makeStyles } from "@material-ui/core/styles";
import TextField from "@material-ui/core/TextField";
import Typography from "@material-ui/core/Typography";
import React from "react";
import { FormattedMessage, useIntl } from "react-intl";
import FormSpacer from "@saleor/components/FormSpacer";
import Hr from "@saleor/components/Hr";
import SingleSelectField from "@saleor/components/SingleSelectField";
import { ProductDetails_product_productType_variantAttributes } from "@saleor/products/types/ProductDetails";
import CardTitle from "@saleor/components/CardTitle";
import { WarehouseFragment } from "@saleor/warehouses/types/WarehouseFragment";
import CardSpacer from "@saleor/components/CardSpacer";
import ControlledCheckbox from "@saleor/components/ControlledCheckbox";
import { isSelected } from "@saleor/utils/lists";
import {
ProductVariantCreateFormData,
VariantCreatorPricesAndSkuMode
} from "./form";
import { getStockAttributeValues } from "./utils";
const useStyles = makeStyles(
theme => ({
attributeStockContainer: {
columnGap: theme.spacing(3) + "px",
display: "grid",
gridTemplateColumns: ({ data }: ProductVariantCreatorStockProps) =>
`150px repeat(${data.warehouses.length}, 288px)`,
rowGap: theme.spacing(2) + "px"
},
attributeStockScroll: {
overflowX: "scroll",
width: "100%"
},
hr: {
marginBottom: theme.spacing(),
marginTop: theme.spacing(0.5)
},
hrAttribute: {
marginTop: theme.spacing(2)
},
label: {
alignSelf: "center"
},
shortInput: {
width: "33%"
},
stockContainer: {
columnGap: theme.spacing(3) + "px",
display: "grid",
gridTemplateColumns: "repeat(3, 288px)",
marginTop: theme.spacing(2),
rowGap: theme.spacing(2) + "px"
},
stockHeader: {
marginBottom: theme.spacing()
},
warehouseContainer: {
columnGap: theme.spacing(3) + "px",
display: "grid",
gridTemplateColumns: "repeat(4, 1fr)",
rowGap: theme.spacing(2) + "px"
},
warehouseHeader: {
marginBottom: theme.spacing()
},
warehouseName: {
marginBottom: theme.spacing()
},
warehouseSubheader: {
marginBottom: theme.spacing(2)
}
}),
{ name: "ProductVariantCreatorStock" }
);
export interface ProductVariantCreatorStockProps {
attributes: ProductDetails_product_productType_variantAttributes[];
data: ProductVariantCreateFormData;
warehouses: WarehouseFragment[];
onApplyToAllChange: (mode: VariantCreatorPricesAndSkuMode) => void;
onApplyToAllStockChange: (quantity: number, warehouseIndex: number) => void;
onAttributeSelect: (id: string) => void;
onAttributeValueChange: (
id: string,
quantity: number,
warehouseIndex: number
) => void;
onWarehouseToggle: (id: string) => void;
}
const ProductVariantCreatorStock: React.FC<ProductVariantCreatorStockProps> = props => {
const {
attributes,
data,
warehouses,
onApplyToAllChange,
onApplyToAllStockChange,
onAttributeSelect,
onAttributeValueChange,
onWarehouseToggle
} = props;
const classes = useStyles(props);
const intl = useIntl();
const attributeChoices = attributes.map(attribute => ({
label: attribute.name,
value: attribute.id
}));
const stockAttributeValues = getStockAttributeValues(data, attributes);
return (
<Card>
<CardTitle
title={intl.formatMessage({
defaultMessage: "Stock and Warehousing",
description: "variant stock, header"
})}
/>
<CardContent>
{warehouses.length > 1 && (
<>
<Typography className={classes.warehouseHeader} variant="h5">
<FormattedMessage
defaultMessage="Warehouses"
description="header"
id="productVariantCreatorWarehouseSectionHeader"
/>
</Typography>
<Typography className={classes.warehouseSubheader}>
<FormattedMessage
defaultMessage="Based on your selections we will create {numberOfProducts} products. Use this step to customize price and stocks for your new products"
values={{
numberOfProducts: data.attributes.reduce(
(acc, attr) => acc + attr.values.length,
0
)
}}
/>
</Typography>
<div className={classes.warehouseContainer}>
{warehouses.map(warehouse => (
<ControlledCheckbox
checked={isSelected(
warehouse.id,
data.warehouses,
(a, b) => a === b
)}
name={`warehouse:${warehouse.id}`}
label={warehouse.name}
onChange={() => onWarehouseToggle(warehouse.id)}
key={warehouse.id}
/>
))}
</div>
<CardSpacer />
<Hr />
<CardSpacer />
</>
)}
<Typography className={classes.stockHeader} variant="h5">
<FormattedMessage
defaultMessage="Stock"
description="variant stock, header"
id="productVariantCreatorStockSectionHeader"
/>
</Typography>
<RadioGroup value={data.stock.mode}>
<FormControlLabel
value="all"
control={<Radio color="primary" />}
label={intl.formatMessage({
defaultMessage: "Apply single stock to all SKUs"
})}
onChange={() => onApplyToAllChange("all")}
/>
{data.stock.mode === "all" && (
<div className={classes.stockContainer}>
{data.warehouses.map((warehouseId, warehouseIndex) => (
<div key={warehouseId}>
<Typography className={classes.warehouseName}>
{
warehouses.find(warehouse => warehouse.id === warehouseId)
.name
}
</Typography>
<TextField
fullWidth
inputProps={{
min: 0,
type: "number"
}}
label={intl.formatMessage({
defaultMessage: "Stock",
id: "productVariantCreatePricesStockInputLabel"
})}
value={data.stock.value[warehouseIndex]}
onChange={event =>
onApplyToAllStockChange(
parseInt(event.target.value, 10),
warehouseIndex
)
}
/>
</div>
))}
</div>
)}
<FormSpacer />
<FormControlLabel
value="attribute"
control={<Radio color="primary" />}
label={intl.formatMessage({
defaultMessage: "Apply unique stock by attribute to each SKU"
})}
onChange={() => onApplyToAllChange("attribute")}
/>
{data.stock.mode === "attribute" && (
<>
<FormSpacer />
<SingleSelectField
className={classes.shortInput}
choices={attributeChoices}
label={intl.formatMessage({
defaultMessage: "Select Attribute",
description: "variant attribute"
})}
value={data.stock.attribute}
onChange={event => onAttributeSelect(event.target.value)}
/>
{stockAttributeValues && (
<>
<Hr className={classes.hrAttribute} />
<FormSpacer />
<div className={classes.attributeStockScroll}>
<div className={classes.attributeStockContainer}>
<div />
{data.stock.attribute &&
data.warehouses.map(warehouseId => (
<Typography
className={classes.warehouseName}
key={warehouseId}
>
{
warehouses.find(
warehouse => warehouse.id === warehouseId
).name
}
</Typography>
))}
{stockAttributeValues.map(attributeValue => (
<React.Fragment key={attributeValue.id}>
<Typography>{attributeValue.name}</Typography>
{data.warehouses.map(
(warehouseId, warehouseIndex) => (
<TextField
fullWidth
inputProps={{
min: 0,
type: "number"
}}
label={intl.formatMessage({
defaultMessage: "Stock",
id:
"productVariantCreatePricesStockInputLabel"
})}
value={
data.stock.values.find(
value => value.slug === attributeValue.slug
).value[warehouseIndex]
}
onChange={event =>
onAttributeValueChange(
attributeValue.slug,
parseInt(event.target.value, 10),
warehouseIndex
)
}
key={warehouseId}
/>
)
)}
</React.Fragment>
))}
</div>
</div>
</>
)}
</>
)}
{data.stock.mode === "attribute" && !!data.stock.attribute && (
<>
<FormSpacer />
<Hr />
</>
)}
<FormSpacer />
<FormControlLabel
value="skip"
control={<Radio color="primary" />}
label={intl.formatMessage({
defaultMessage: "Skip stock for now"
})}
onChange={() => onApplyToAllChange("skip")}
/>
</RadioGroup>
</CardContent>
</Card>
);
};
ProductVariantCreatorStock.displayName = "ProductVariantCreatorStock";
export default ProductVariantCreatorStock;

View file

@ -3,62 +3,87 @@ 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 Card from "@material-ui/core/Card";
import IconButton from "@material-ui/core/IconButton";
import { makeStyles } from "@material-ui/core/styles";
import { makeStyles, 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 classNames from "classnames";
import React from "react";
import { FormattedMessage, useIntl } from "react-intl";
import Hr from "@saleor/components/Hr";
import { ProductVariantBulkCreate_productVariantBulkCreate_errors } from "@saleor/products/types/ProductVariantBulkCreate";
import { ProductVariantBulkCreateInput } from "@saleor/types/globalTypes";
import { getFormErrors } from "@saleor/utils/errors";
import { getBulkProductErrorMessage } from "@saleor/utils/errors/product";
import CardTitle from "@saleor/components/CardTitle";
import { WarehouseFragment } from "@saleor/warehouses/types/WarehouseFragment";
import Hr from "@saleor/components/Hr";
import { ProductDetails_product_productType_variantAttributes } from "../../types/ProductDetails";
import { ProductVariantCreateFormData } from "./form";
import { VariantField } from "./reducer";
export interface ProductVariantCreateSummaryProps {
export interface ProductVariantCreatorSummaryProps {
attributes: ProductDetails_product_productType_variantAttributes[];
currencySymbol: string;
data: ProductVariantCreateFormData;
errors: ProductVariantBulkCreate_productVariantBulkCreate_errors[];
warehouses: WarehouseFragment[];
onVariantDataChange: (
variantIndex: number,
field: VariantField,
value: string
) => void;
onVariantStockDataChange: (
variantIndex: number,
warehouseId: string,
value: string
) => void;
onVariantDelete: (variantIndex: number) => void;
}
type ClassKey =
| "attributeValue"
| "card"
| "col"
| "colHeader"
| "colName"
| "colPrice"
| "colSku"
| "colStock"
| "delete"
| "hr"
| "input"
| "summary";
const colors = [blue, cyan, green, purple, yellow].map(color => color[800]);
const useStyles = makeStyles(
const useStyles = makeStyles<
Theme,
ProductVariantCreatorSummaryProps,
ClassKey
>(
theme => ({
attributeValue: {
display: "inline-block",
marginRight: theme.spacing(1)
},
col: {
...theme.typography.body1,
fontSize: 14,
paddingLeft: theme.spacing(),
paddingRight: theme.spacing(1)
card: {
paddingBottom: theme.spacing()
},
colHeader: {
col: {
...theme.typography.body1,
fontSize: 14
},
colHeader: {
...theme.typography.body1,
fontSize: 14,
paddingTop: theme.spacing(3)
},
colName: {
"&&": {
paddingLeft: "0 !important"
},
"&:not($colHeader)": {
paddingTop: theme.spacing(2)
}
},
paddingLeft: theme.spacing(3)
},
colPrice: {},
colSku: {},
@ -66,26 +91,25 @@ const useStyles = makeStyles(
delete: {
marginTop: theme.spacing(0.5)
},
errorRow: {},
hr: {
marginBottom: theme.spacing(),
marginTop: theme.spacing(0.5)
gridColumn: props => `span ${4 + props.data.variants[0].stocks.length}`
},
input: {
"& input": {
padding: "16px 12px 17px"
},
marginTop: theme.spacing(0.5)
}
},
row: {
borderBottom: `1px solid ${theme.palette.divider}`,
summary: {
columnGap: theme.spacing(3),
display: "grid",
gridTemplateColumns: "1fr 180px 120px 180px 64px",
padding: theme.spacing(1, 0)
gridTemplateColumns: props =>
`minmax(240px, auto) 170px repeat(${props.data.variants[0].stocks.length}, 140px) 140px 64px`,
overflowX: "scroll",
rowGap: theme.spacing() + "px"
}
}),
{
name: "ProductVariantCreateSummary"
name: "ProductVariantCreatorSummary"
}
);
@ -108,75 +132,72 @@ function getVariantName(
);
}
const ProductVariantCreateSummary: React.FC<ProductVariantCreateSummaryProps> = props => {
const ProductVariantCreatorSummary: React.FC<ProductVariantCreatorSummaryProps> = props => {
const {
attributes,
currencySymbol,
data,
errors,
warehouses,
onVariantDataChange,
onVariantDelete
onVariantDelete,
onVariantStockDataChange
} = props;
const classes = useStyles(props);
const intl = useIntl();
return (
<>
<Typography color="textSecondary" variant="h5">
<FormattedMessage
defaultMessage="You will create variants below"
description="header"
/>
</Typography>
<Hr className={classes.hr} />
<div>
<div className={classes.row}>
<div
className={classNames(
classes.col,
classes.colHeader,
classes.colName
)}
>
<FormattedMessage
defaultMessage="Variant"
description="variant name"
/>
</div>
<div
className={classNames(
classes.col,
classes.colHeader,
classes.colPrice
)}
>
<FormattedMessage
defaultMessage="Price"
description="variant price"
/>
</div>
<Card className={classes.card}>
<CardTitle
title={intl.formatMessage({
defaultMessage: "Created Variants",
description: "variant creator summary card header"
})}
/>
<div className={classes.summary}>
<div
className={classNames(
classes.col,
classes.colHeader,
classes.colName
)}
>
<FormattedMessage
defaultMessage="Variant"
description="variant name"
/>
</div>
<div
className={classNames(
classes.col,
classes.colHeader,
classes.colPrice
)}
>
<FormattedMessage
defaultMessage="Price"
description="variant price"
/>
</div>
{data.warehouses.map(warehouseId => (
<div
className={classNames(
classes.col,
classes.colHeader,
classes.colStock
)}
key={warehouseId}
>
<FormattedMessage
defaultMessage="Inventory"
description="variant stock amount"
/>
</div>
<div
className={classNames(
classes.col,
classes.colHeader,
classes.colSku
)}
>
<FormattedMessage defaultMessage="SKU" />
{warehouses.find(warehouse => warehouse.id === warehouseId).name}
</div>
))}
<div
className={classNames(classes.col, classes.colHeader, classes.colSku)}
>
<FormattedMessage defaultMessage="SKU" />
</div>
<div className={classNames(classes.col, classes.colHeader)} />
<Hr className={classes.hr} />
{data.variants.map((variant, variantIndex) => {
const variantErrors = errors.filter(
error => error.index === variantIndex
@ -187,10 +208,7 @@ const ProductVariantCreateSummary: React.FC<ProductVariantCreateSummaryProps> =
);
return (
<div
className={classNames(classes.row, {
[classes.errorRow]: variantErrors.length > 0
})}
<React.Fragment
key={variant.attributes
.map(attribute => attribute.values[0])
.join(":")}
@ -203,7 +221,7 @@ const ProductVariantCreateSummary: React.FC<ProductVariantCreateSummaryProps> =
style={{
color: colors[valueIndex % colors.length]
}}
key={value}
key={`${value}:${valueIndex}`}
>
{value}
</span>
@ -236,29 +254,34 @@ const ProductVariantCreateSummary: React.FC<ProductVariantCreateSummaryProps> =
}
/>
</div>
<div className={classNames(classes.col, classes.colStock)}>
<TextField
className={classes.input}
error={!!variantFormErrors.quantity}
helperText={getBulkProductErrorMessage(
variantFormErrors.quantity,
intl
)}
inputProps={{
min: 0,
type: "number"
}}
fullWidth
value={variant.quantity}
onChange={event =>
onVariantDataChange(
variantIndex,
"stock",
event.target.value
)
}
/>
</div>
{variant.stocks.map(stock => (
<div
className={classNames(classes.col, classes.colStock)}
key={stock.warehouse}
>
<TextField
className={classes.input}
error={!!variantFormErrors.quantity}
helperText={getBulkProductErrorMessage(
variantFormErrors.quantity,
intl
)}
inputProps={{
min: 0,
type: "number"
}}
fullWidth
value={stock.quantity}
onChange={event =>
onVariantStockDataChange(
variantIndex,
stock.warehouse,
event.target.value
)
}
/>
</div>
))}
<div className={classNames(classes.col, classes.colSku)}>
<TextField
className={classes.input}
@ -283,13 +306,16 @@ const ProductVariantCreateSummary: React.FC<ProductVariantCreateSummaryProps> =
<DeleteIcon />
</IconButton>
</div>
</div>
{variantIndex !== data.variants.length - 1 && (
<Hr className={classes.hr} />
)}
</React.Fragment>
);
})}
</div>
</>
</Card>
);
};
ProductVariantCreateSummary.displayName = "ProductVariantCreateSummary";
export default ProductVariantCreateSummary;
ProductVariantCreatorSummary.displayName = "ProductVariantCreatorSummary";
export default ProductVariantCreatorSummary;

View file

@ -4,11 +4,11 @@ import classNames from "classnames";
import React from "react";
import { IntlShape, useIntl } from "react-intl";
import { ProductVariantCreateStep } from "./types";
import { ProductVariantCreatorStep } from "./types";
interface Step {
label: string;
value: ProductVariantCreateStep;
value: ProductVariantCreatorStep;
}
function getSteps(intl: IntlShape): Step[] {
return [
@ -17,21 +17,21 @@ function getSteps(intl: IntlShape): Step[] {
defaultMessage: "Select Values",
description: "attribute values, variant creation step"
}),
value: "values"
value: ProductVariantCreatorStep.values
},
{
label: intl.formatMessage({
defaultMessage: "Prices and SKU",
description: "variant creation step"
}),
value: "prices"
value: ProductVariantCreatorStep.prices
},
{
label: intl.formatMessage({
defaultMessage: "Summary",
description: "variant creation step"
}),
value: "summary"
value: ProductVariantCreatorStep.summary
}
];
}
@ -62,18 +62,16 @@ const useStyles = makeStyles(
}
}),
{
name: "ProductVariantCreateTabs"
name: "ProductVariantCreatorTabs"
}
);
export interface ProductVariantCreateTabsProps {
step: ProductVariantCreateStep;
onStepClick: (step: ProductVariantCreateStep) => void;
export interface ProductVariantCreatorTabsProps {
step: ProductVariantCreatorStep;
onStepClick: (step: ProductVariantCreatorStep) => void;
}
const ProductVariantCreateTabs: React.FC<
ProductVariantCreateTabsProps
> = props => {
const ProductVariantCreatorTabs: React.FC<ProductVariantCreatorTabsProps> = props => {
const { step: currentStep, onStepClick } = props;
const classes = useStyles(props);
const intl = useIntl();
@ -104,5 +102,5 @@ const ProductVariantCreateTabs: React.FC<
);
};
ProductVariantCreateTabs.displayName = "ProductVariantCreateTabs";
export default ProductVariantCreateTabs;
ProductVariantCreatorTabs.displayName = "ProductVariantCreatorTabs";
export default ProductVariantCreatorTabs;

View file

@ -0,0 +1,75 @@
import makeStyles from "@material-ui/core/styles/makeStyles";
import Card from "@material-ui/core/Card";
import CardContent from "@material-ui/core/CardContent";
import React from "react";
import ControlledCheckbox from "@saleor/components/ControlledCheckbox";
import Debounce from "@saleor/components/Debounce";
import Skeleton from "@saleor/components/Skeleton";
import { ProductDetails_product_productType_variantAttributes } from "@saleor/products/types/ProductDetails";
import { isSelected } from "@saleor/utils/lists";
import CardTitle from "@saleor/components/CardTitle";
import CardSpacer from "@saleor/components/CardSpacer";
import { ProductVariantCreateFormData } from "./form";
export interface ProductVariantCreatorValuesProps {
attributes: ProductDetails_product_productType_variantAttributes[];
data: ProductVariantCreateFormData;
onValueClick: (attributeId: string, valueId: string) => void;
}
const useStyles = makeStyles(
theme => ({
valueContainer: {
display: "grid",
gridColumnGap: theme.spacing(3),
gridTemplateColumns: "repeat(5, 1fr)"
}
}),
{ name: "ProductVariantCreatorValues" }
);
const ProductVariantCreatorValues: React.FC<ProductVariantCreatorValuesProps> = props => {
const { attributes, data, onValueClick } = props;
const classes = useStyles(props);
return (
<>
{attributes.map(attribute => (
<React.Fragment key={attribute.id}>
<Card>
<CardTitle title={attribute?.name || <Skeleton />} />
<CardContent className={classes.valueContainer}>
{attribute.values.map(value => (
<Debounce
debounceFn={() => onValueClick(attribute.id, value.slug)}
time={100}
key={value.slug}
>
{change => (
<ControlledCheckbox
checked={isSelected(
value.slug,
data.attributes.find(
dataAttribute => attribute.id === dataAttribute.id
)?.values || [],
(a, b) => a === b
)}
name={`value:${value.slug}`}
label={value.name}
onChange={change}
/>
)}
</Debounce>
))}
</CardContent>
</Card>
<CardSpacer />
</React.Fragment>
))}
</>
);
};
ProductVariantCreatorValues.displayName = "ProductVariantCreatorValues";
export default ProductVariantCreatorValues;

Some files were not shown because too many files have changed in this diff Show more