Merge pull request #177 from mirumee/add/variant-creator
Add variant creator
This commit is contained in:
commit
e5b19d2c23
35 changed files with 6064 additions and 73 deletions
|
@ -32,3 +32,4 @@ All notable, unreleased changes to this project will be documented in this file.
|
||||||
- Allow sorting products by attribute - #180 by @dominik-zeglen
|
- Allow sorting products by attribute - #180 by @dominik-zeglen
|
||||||
- Hide variants and attributes if product has none - #179 by @dominik-zeglen
|
- Hide variants and attributes if product has none - #179 by @dominik-zeglen
|
||||||
- Add service account section - #188 by @dominik-zeglen
|
- Add service account section - #188 by @dominik-zeglen
|
||||||
|
- Add variant creator - #177 by @dominik-zeglen
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"POT-Creation-Date: 2019-10-04T11:19:12.447Z\n"
|
"POT-Creation-Date: 2019-10-09T10:25:56.800Z\n"
|
||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
"MIME-Version: 1.0\n"
|
"MIME-Version: 1.0\n"
|
||||||
|
@ -639,6 +639,38 @@ msgctxt "voucher"
|
||||||
msgid "Applies to"
|
msgid "Applies to"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: build/locale/src/products/components/ProductVariantCreateDialog/ProductVariantCreatePrices.json
|
||||||
|
#. [src.products.components.ProductVariantCreateDialog.2783195765]
|
||||||
|
#. defaultMessage is:
|
||||||
|
#. Apply single price to all SKUs
|
||||||
|
msgctxt "description"
|
||||||
|
msgid "Apply single price to all SKUs"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: build/locale/src/products/components/ProductVariantCreateDialog/ProductVariantCreatePrices.json
|
||||||
|
#. [src.products.components.ProductVariantCreateDialog.3601538615]
|
||||||
|
#. defaultMessage is:
|
||||||
|
#. Apply single stock to all SKUs
|
||||||
|
msgctxt "description"
|
||||||
|
msgid "Apply single stock to all SKUs"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: build/locale/src/products/components/ProductVariantCreateDialog/ProductVariantCreatePrices.json
|
||||||
|
#. [src.products.components.ProductVariantCreateDialog.3570949907]
|
||||||
|
#. defaultMessage is:
|
||||||
|
#. Apply unique prices by attribute to each SKU
|
||||||
|
msgctxt "description"
|
||||||
|
msgid "Apply unique prices by attribute to each SKU"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: build/locale/src/products/components/ProductVariantCreateDialog/ProductVariantCreatePrices.json
|
||||||
|
#. [src.products.components.ProductVariantCreateDialog.3387090508]
|
||||||
|
#. defaultMessage is:
|
||||||
|
#. Apply unique stock by attribute to each SKU
|
||||||
|
msgctxt "description"
|
||||||
|
msgid "Apply unique stock by attribute to each SKU"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: build/locale/src/orders/components/OrderCancelDialog/OrderCancelDialog.json
|
#: build/locale/src/orders/components/OrderCancelDialog/OrderCancelDialog.json
|
||||||
#. [src.orders.components.OrderCancelDialog.3981375672]
|
#. [src.orders.components.OrderCancelDialog.3981375672]
|
||||||
#. defaultMessage is:
|
#. defaultMessage is:
|
||||||
|
@ -1139,6 +1171,10 @@ msgctxt "description"
|
||||||
msgid "Are you sure you want to void this payment?"
|
msgid "Are you sure you want to void this payment?"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: build/locale/src/products/components/ProductVariantCreateDialog/ProductVariantCreateDialog.json
|
||||||
|
#. [src.products.components.ProductVariantCreateDialog.3922579741] - dialog header
|
||||||
|
#. defaultMessage is:
|
||||||
|
#. Assign Attribute
|
||||||
#: build/locale/src/productTypes/components/AssignAttributeDialog/AssignAttributeDialog.json
|
#: build/locale/src/productTypes/components/AssignAttributeDialog/AssignAttributeDialog.json
|
||||||
#. [src.productTypes.components.AssignAttributeDialog.3922579741] - dialog header
|
#. [src.productTypes.components.AssignAttributeDialog.3922579741] - dialog header
|
||||||
#. defaultMessage is:
|
#. defaultMessage is:
|
||||||
|
@ -1267,6 +1303,14 @@ msgctxt "assign attribute value button"
|
||||||
msgid "Assign value"
|
msgid "Assign value"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: build/locale/src/products/components/ProductVariantCreateDialog/ProductVariantCreatePrices.json
|
||||||
|
#. [src.products.components.ProductVariantCreateDialog.168343345] - variant attribute
|
||||||
|
#. defaultMessage is:
|
||||||
|
#. Attribute
|
||||||
|
msgctxt "variant attribute"
|
||||||
|
msgid "Attribute"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: build/locale/src/attributes/components/AttributeDetails/AttributeDetails.json
|
#: build/locale/src/attributes/components/AttributeDetails/AttributeDetails.json
|
||||||
#. [src.attributes.components.AttributeDetails.3605174225] - attribute's slug short code label
|
#. [src.attributes.components.AttributeDetails.3605174225] - attribute's slug short code label
|
||||||
#. defaultMessage is:
|
#. defaultMessage is:
|
||||||
|
@ -1511,6 +1555,10 @@ msgstr ""
|
||||||
#. [src.cancel] - button
|
#. [src.cancel] - button
|
||||||
#. defaultMessage is:
|
#. defaultMessage is:
|
||||||
#. Cancel
|
#. Cancel
|
||||||
|
#: build/locale/src/products/components/ProductVariantCreateDialog/ProductVariantCreateDialog.json
|
||||||
|
#. [src.products.components.ProductVariantCreateDialog.3528672691] - button
|
||||||
|
#. defaultMessage is:
|
||||||
|
#. Cancel
|
||||||
msgctxt "button"
|
msgctxt "button"
|
||||||
msgid "Cancel"
|
msgid "Cancel"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
@ -1751,6 +1799,14 @@ msgctxt "tax rate"
|
||||||
msgid "Children's clothing"
|
msgid "Children's clothing"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: build/locale/src/products/components/ProductVariantCreateDialog/ProductVariantCreatePrices.json
|
||||||
|
#. [src.products.components.ProductVariantCreateDialog.2670525734] - variant attribute
|
||||||
|
#. defaultMessage is:
|
||||||
|
#. Choose attribute
|
||||||
|
msgctxt "variant attribute"
|
||||||
|
msgid "Choose attribute"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: build/locale/src/shipping/components/ShippingZoneCountriesAssignDialog/ShippingZoneCountriesAssignDialog.json
|
#: build/locale/src/shipping/components/ShippingZoneCountriesAssignDialog/ShippingZoneCountriesAssignDialog.json
|
||||||
#. [src.shipping.components.ShippingZoneCountriesAssignDialog.2404264158]
|
#. [src.shipping.components.ShippingZoneCountriesAssignDialog.2404264158]
|
||||||
#. defaultMessage is:
|
#. defaultMessage is:
|
||||||
|
@ -2139,6 +2195,14 @@ msgctxt "description"
|
||||||
msgid "Country area"
|
msgid "Country area"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: build/locale/src/products/components/ProductVariantCreateDialog/ProductVariantCreateDialog.json
|
||||||
|
#. [src.products.components.ProductVariantCreateDialog.4120989039] - create multiple variants, button
|
||||||
|
#. defaultMessage is:
|
||||||
|
#. Create
|
||||||
|
msgctxt "create multiple variants, button"
|
||||||
|
msgid "Create"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: build/locale/src/services/components/ServiceTokenCreateDialog/ServiceTokenCreateDialog.json
|
#: build/locale/src/services/components/ServiceTokenCreateDialog/ServiceTokenCreateDialog.json
|
||||||
#. [src.services.components.ServiceTokenCreateDialog.4120989039] - create service token, button
|
#. [src.services.components.ServiceTokenCreateDialog.4120989039] - create service token, button
|
||||||
#. defaultMessage is:
|
#. defaultMessage is:
|
||||||
|
@ -2435,6 +2499,14 @@ msgctxt "window title"
|
||||||
msgid "Create variant"
|
msgid "Create variant"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: build/locale/src/products/components/ProductVariants/ProductVariants.json
|
||||||
|
#. [src.products.components.ProductVariants.1721716102] - button
|
||||||
|
#. defaultMessage is:
|
||||||
|
#. Create variants
|
||||||
|
msgctxt "button"
|
||||||
|
msgid "Create variants"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: build/locale/src/discounts/components/VoucherListPage/VoucherListPage.json
|
#: build/locale/src/discounts/components/VoucherListPage/VoucherListPage.json
|
||||||
#. [src.discounts.components.VoucherListPage.614836274] - button
|
#. [src.discounts.components.VoucherListPage.614836274] - button
|
||||||
#. defaultMessage is:
|
#. defaultMessage is:
|
||||||
|
@ -3931,6 +4003,14 @@ msgctxt "product stock"
|
||||||
msgid "Inventory"
|
msgid "Inventory"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: build/locale/src/products/components/ProductVariantCreateDialog/ProductVariantCreateSummary.json
|
||||||
|
#. [src.products.components.ProductVariantCreateDialog.3490038570] - variant stock amount
|
||||||
|
#. defaultMessage is:
|
||||||
|
#. Inventory
|
||||||
|
msgctxt "variant stock amount"
|
||||||
|
msgid "Inventory"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: build/locale/src/products/components/ProductVariantStock/ProductVariantStock.json
|
#: build/locale/src/products/components/ProductVariantStock/ProductVariantStock.json
|
||||||
#. [src.products.components.ProductVariantStock.3490038570] - product variant stock
|
#. [src.products.components.ProductVariantStock.3490038570] - product variant stock
|
||||||
#. defaultMessage is:
|
#. defaultMessage is:
|
||||||
|
@ -4507,6 +4587,14 @@ msgctxt "tax rate"
|
||||||
msgid "Newspapers"
|
msgid "Newspapers"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: build/locale/src/products/components/ProductVariantCreateDialog/ProductVariantCreateDialog.json
|
||||||
|
#. [src.products.components.ProductVariantCreateDialog.3673120330] - button
|
||||||
|
#. defaultMessage is:
|
||||||
|
#. Next
|
||||||
|
msgctxt "button"
|
||||||
|
msgid "Next"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: build/locale/src/intl.json
|
#: build/locale/src/intl.json
|
||||||
#. [src.no]
|
#. [src.no]
|
||||||
#. defaultMessage is:
|
#. defaultMessage is:
|
||||||
|
@ -5651,6 +5739,14 @@ msgctxt "order payment"
|
||||||
msgid "Preauthorized amount"
|
msgid "Preauthorized amount"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: build/locale/src/products/components/ProductVariantCreateDialog/ProductVariantCreateDialog.json
|
||||||
|
#. [src.products.components.ProductVariantCreateDialog.904693740] - previous step, button
|
||||||
|
#. defaultMessage is:
|
||||||
|
#. Previous
|
||||||
|
msgctxt "previous step, button"
|
||||||
|
msgid "Previous"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: build/locale/src/categories/components/CategoryProductList/CategoryProductList.json
|
#: build/locale/src/categories/components/CategoryProductList/CategoryProductList.json
|
||||||
#. [src.categories.components.CategoryProductList.1134347598] - product price
|
#. [src.categories.components.CategoryProductList.1134347598] - product price
|
||||||
#. defaultMessage is:
|
#. defaultMessage is:
|
||||||
|
@ -5695,10 +5791,34 @@ msgstr ""
|
||||||
#. [src.products.components.ProductListFilter.1134347598]
|
#. [src.products.components.ProductListFilter.1134347598]
|
||||||
#. defaultMessage is:
|
#. defaultMessage is:
|
||||||
#. Price
|
#. Price
|
||||||
|
#: build/locale/src/products/components/ProductVariantCreateDialog/ProductVariantCreatePrices.json
|
||||||
|
#. [productVariantCreatePricesPriceInputLabel]
|
||||||
|
#. defaultMessage is:
|
||||||
|
#. Price
|
||||||
msgctxt "description"
|
msgctxt "description"
|
||||||
msgid "Price"
|
msgid "Price"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: build/locale/src/products/components/ProductVariantCreateDialog/ProductVariantCreatePrices.json
|
||||||
|
#. [src.products.components.ProductVariantCreateDialog.1134347598] - variant price, header
|
||||||
|
#. defaultMessage is:
|
||||||
|
#. Price
|
||||||
|
msgctxt "variant price, header"
|
||||||
|
msgid "Price"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: build/locale/src/products/components/ProductVariantCreateDialog/ProductVariantCreatePrices.json
|
||||||
|
#. [productVariantCreatePricesSetPricePlaceholder] - variant price
|
||||||
|
#. defaultMessage is:
|
||||||
|
#. Price
|
||||||
|
#: build/locale/src/products/components/ProductVariantCreateDialog/ProductVariantCreateSummary.json
|
||||||
|
#. [src.products.components.ProductVariantCreateDialog.1134347598] - variant price
|
||||||
|
#. defaultMessage is:
|
||||||
|
#. Price
|
||||||
|
msgctxt "variant price"
|
||||||
|
msgid "Price"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: build/locale/src/products/components/ProductVariants/ProductVariants.json
|
#: build/locale/src/products/components/ProductVariants/ProductVariants.json
|
||||||
#. [src.products.components.ProductVariants.1134347598] - product variant price
|
#. [src.products.components.ProductVariants.1134347598] - product variant price
|
||||||
#. defaultMessage is:
|
#. defaultMessage is:
|
||||||
|
@ -5747,6 +5867,14 @@ msgctxt "filter by price"
|
||||||
msgid "Price to {price}"
|
msgid "Price to {price}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: build/locale/src/products/components/ProductVariantCreateDialog/ProductVariantCreateTabs.json
|
||||||
|
#. [src.products.components.ProductVariantCreateDialog.705096461] - variant creation step
|
||||||
|
#. defaultMessage is:
|
||||||
|
#. Prices and SKU
|
||||||
|
msgctxt "variant creation step"
|
||||||
|
msgid "Prices and SKU"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: build/locale/src/discounts/components/SalePricing/SalePricing.json
|
#: build/locale/src/discounts/components/SalePricing/SalePricing.json
|
||||||
#. [src.discounts.components.SalePricing.1099355007] - sale pricing, header
|
#. [src.discounts.components.SalePricing.1099355007] - sale pricing, header
|
||||||
#. defaultMessage is:
|
#. defaultMessage is:
|
||||||
|
@ -6387,6 +6515,10 @@ msgctxt "product's sku"
|
||||||
msgid "SKU"
|
msgid "SKU"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: build/locale/src/products/components/ProductVariantCreateDialog/ProductVariantCreateSummary.json
|
||||||
|
#. [src.products.components.ProductVariantCreateDialog.693960049]
|
||||||
|
#. defaultMessage is:
|
||||||
|
#. SKU
|
||||||
#: build/locale/src/products/components/ProductVariants/ProductVariants.json
|
#: build/locale/src/products/components/ProductVariants/ProductVariants.json
|
||||||
#. [src.products.components.ProductVariants.693960049]
|
#. [src.products.components.ProductVariants.693960049]
|
||||||
#. defaultMessage is:
|
#. defaultMessage is:
|
||||||
|
@ -6839,6 +6971,14 @@ msgctxt "description"
|
||||||
msgid "Select Filter..."
|
msgid "Select Filter..."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: build/locale/src/products/components/ProductVariantCreateDialog/ProductVariantCreateTabs.json
|
||||||
|
#. [src.products.components.ProductVariantCreateDialog.2478977538] - attribute values, variant creation step
|
||||||
|
#. defaultMessage is:
|
||||||
|
#. Select Values
|
||||||
|
msgctxt "attribute values, variant creation step"
|
||||||
|
msgid "Select Values"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: build/locale/src/products/components/ProductVariantImages/ProductVariantImages.json
|
#: build/locale/src/products/components/ProductVariantImages/ProductVariantImages.json
|
||||||
#. [src.products.components.ProductVariantImages.3449133076]
|
#. [src.products.components.ProductVariantImages.3449133076]
|
||||||
#. defaultMessage is:
|
#. defaultMessage is:
|
||||||
|
@ -7347,6 +7487,30 @@ msgctxt "product stock"
|
||||||
msgid "Stock"
|
msgid "Stock"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: build/locale/src/products/components/ProductVariantCreateDialog/ProductVariantCreatePrices.json
|
||||||
|
#. [src.products.components.ProductVariantCreateDialog.3841616483] - variant stock, header
|
||||||
|
#. defaultMessage is:
|
||||||
|
#. Stock
|
||||||
|
msgctxt "variant stock, header"
|
||||||
|
msgid "Stock"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: build/locale/src/products/components/ProductVariantCreateDialog/ProductVariantCreatePrices.json
|
||||||
|
#. [productVariantCreatePricesStockInputLabel]
|
||||||
|
#. defaultMessage is:
|
||||||
|
#. Stock
|
||||||
|
msgctxt "description"
|
||||||
|
msgid "Stock"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: build/locale/src/products/components/ProductVariantCreateDialog/ProductVariantCreatePrices.json
|
||||||
|
#. [productVariantCreatePricesSetStockPlaceholder] - variant stock
|
||||||
|
#. defaultMessage is:
|
||||||
|
#. Stock
|
||||||
|
msgctxt "variant stock"
|
||||||
|
msgid "Stock"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: build/locale/src/products/components/ProductVariantStock/ProductVariantStock.json
|
#: build/locale/src/products/components/ProductVariantStock/ProductVariantStock.json
|
||||||
#. [src.products.components.ProductVariantStock.3841616483] - product variant stock, section header
|
#. [src.products.components.ProductVariantStock.3841616483] - product variant stock, section header
|
||||||
#. defaultMessage is:
|
#. defaultMessage is:
|
||||||
|
@ -7491,6 +7655,14 @@ msgctxt "description"
|
||||||
msgid "Summary"
|
msgid "Summary"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: build/locale/src/products/components/ProductVariantCreateDialog/ProductVariantCreateTabs.json
|
||||||
|
#. [src.products.components.ProductVariantCreateDialog.2745385064] - variant creation step
|
||||||
|
#. defaultMessage is:
|
||||||
|
#. Summary
|
||||||
|
msgctxt "variant creation step"
|
||||||
|
msgid "Summary"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: build/locale/src/productTypes/components/ProductTypeList/ProductTypeList.json
|
#: build/locale/src/productTypes/components/ProductTypeList/ProductTypeList.json
|
||||||
#. [src.productTypes.components.ProductTypeList.1240292548] - tax rate for a product type
|
#. [src.productTypes.components.ProductTypeList.1240292548] - tax rate for a product type
|
||||||
#. defaultMessage is:
|
#. defaultMessage is:
|
||||||
|
@ -8459,6 +8631,14 @@ msgctxt "attribute values"
|
||||||
msgid "Values"
|
msgid "Values"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: build/locale/src/products/components/ProductVariantCreateDialog/ProductVariantCreateSummary.json
|
||||||
|
#. [src.products.components.ProductVariantCreateDialog.998917294] - variant name
|
||||||
|
#. defaultMessage is:
|
||||||
|
#. Variant
|
||||||
|
msgctxt "variant name"
|
||||||
|
msgid "Variant"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: build/locale/src/translations/components/TranslationsProductTypesPage/TranslationsProductTypesPage.json
|
#: build/locale/src/translations/components/TranslationsProductTypesPage/TranslationsProductTypesPage.json
|
||||||
#. [src.translations.components.TranslationsProductTypesPage.3538502409] - header
|
#. [src.translations.components.TranslationsProductTypesPage.3538502409] - header
|
||||||
#. defaultMessage is:
|
#. defaultMessage is:
|
||||||
|
@ -8711,6 +8891,14 @@ msgctxt "description"
|
||||||
msgid "Yes"
|
msgid "Yes"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: build/locale/src/products/components/ProductVariantCreateDialog/ProductVariantCreateSummary.json
|
||||||
|
#. [src.products.components.ProductVariantCreateDialog.1009678918] - header
|
||||||
|
#. defaultMessage is:
|
||||||
|
#. You will create variants below
|
||||||
|
msgctxt "header"
|
||||||
|
msgid "You will create variants below"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: build/locale/src/components/AddressEdit/AddressEdit.json
|
#: build/locale/src/components/AddressEdit/AddressEdit.json
|
||||||
#. [src.components.AddressEdit.2965971965]
|
#. [src.components.AddressEdit.2965971965]
|
||||||
#. defaultMessage is:
|
#. defaultMessage is:
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { ProductDetails_product_productType_variantAttributes } from "@saleor/products/types/ProductDetails";
|
||||||
import {
|
import {
|
||||||
AttributeInputTypeEnum,
|
AttributeInputTypeEnum,
|
||||||
AttributeValueType
|
AttributeValueType
|
||||||
|
@ -35,7 +36,10 @@ export const attribute: AttributeDetailsFragment = {
|
||||||
visibleInStorefront: true
|
visibleInStorefront: true
|
||||||
};
|
};
|
||||||
|
|
||||||
export const attributes: AttributeList_attributes_edges_node[] = [
|
export const attributes: Array<
|
||||||
|
AttributeList_attributes_edges_node &
|
||||||
|
ProductDetails_product_productType_variantAttributes
|
||||||
|
> = [
|
||||||
{
|
{
|
||||||
node: {
|
node: {
|
||||||
__typename: "Attribute" as "Attribute",
|
__typename: "Attribute" as "Attribute",
|
||||||
|
|
|
@ -30,6 +30,9 @@ const styles = (theme: Theme) =>
|
||||||
"& > div": {
|
"& > div": {
|
||||||
padding: "0 14px"
|
padding: "0 14px"
|
||||||
},
|
},
|
||||||
|
"& fieldset": {
|
||||||
|
background: theme.palette.background.paper
|
||||||
|
},
|
||||||
"& textarea": {
|
"& textarea": {
|
||||||
"&::placeholder": {
|
"&::placeholder": {
|
||||||
opacity: [[1], "!important"] as any
|
opacity: [[1], "!important"] as any
|
||||||
|
|
|
@ -60,6 +60,7 @@ export interface ProductUpdatePageProps extends ListActions {
|
||||||
saveButtonBarState: ConfirmButtonTransitionState;
|
saveButtonBarState: ConfirmButtonTransitionState;
|
||||||
fetchCategories: (query: string) => void;
|
fetchCategories: (query: string) => void;
|
||||||
fetchCollections: (query: string) => void;
|
fetchCollections: (query: string) => void;
|
||||||
|
onVariantsAdd: () => void;
|
||||||
onVariantShow: (id: string) => () => void;
|
onVariantShow: (id: string) => () => void;
|
||||||
onImageDelete: (id: string) => () => void;
|
onImageDelete: (id: string) => () => void;
|
||||||
onBack?();
|
onBack?();
|
||||||
|
@ -100,6 +101,7 @@ export const ProductUpdatePage: React.FC<ProductUpdatePageProps> = ({
|
||||||
onSeoClick,
|
onSeoClick,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
onVariantAdd,
|
onVariantAdd,
|
||||||
|
onVariantsAdd,
|
||||||
onVariantShow,
|
onVariantShow,
|
||||||
isChecked,
|
isChecked,
|
||||||
selected,
|
selected,
|
||||||
|
@ -236,6 +238,7 @@ export const ProductUpdatePage: React.FC<ProductUpdatePageProps> = ({
|
||||||
fallbackPrice={product ? product.basePrice : undefined}
|
fallbackPrice={product ? product.basePrice : undefined}
|
||||||
onRowClick={onVariantShow}
|
onRowClick={onVariantShow}
|
||||||
onVariantAdd={onVariantAdd}
|
onVariantAdd={onVariantAdd}
|
||||||
|
onVariantsAdd={onVariantsAdd}
|
||||||
toolbar={toolbar}
|
toolbar={toolbar}
|
||||||
isChecked={isChecked}
|
isChecked={isChecked}
|
||||||
selected={selected}
|
selected={selected}
|
||||||
|
|
|
@ -0,0 +1,124 @@
|
||||||
|
import Card from "@material-ui/core/Card";
|
||||||
|
import CardContent from "@material-ui/core/CardContent";
|
||||||
|
import { storiesOf } from "@storybook/react";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { attributes } from "@saleor/attributes/fixtures";
|
||||||
|
import { ProductVariantBulkCreate_productVariantBulkCreate_bulkProductErrors } from "@saleor/products/types/ProductVariantBulkCreate";
|
||||||
|
import { ProductErrorCode } from "@saleor/types/globalTypes";
|
||||||
|
import Decorator from "../../../storybook/Decorator";
|
||||||
|
import { createVariants } from "./createVariants";
|
||||||
|
import { AllOrAttribute } from "./form";
|
||||||
|
import ProductVariantCreateContent, {
|
||||||
|
ProductVariantCreateContentProps
|
||||||
|
} from "./ProductVariantCreateContent";
|
||||||
|
import ProductVariantCreateDialog from "./ProductVariantCreateDialog";
|
||||||
|
|
||||||
|
const selectedAttributes = [1, 4, 5].map(index => attributes[index]);
|
||||||
|
|
||||||
|
const price: AllOrAttribute = {
|
||||||
|
all: false,
|
||||||
|
attribute: selectedAttributes[1].id,
|
||||||
|
value: "2.79",
|
||||||
|
values: selectedAttributes[1].values.map((attribute, attributeIndex) => ({
|
||||||
|
slug: attribute.slug,
|
||||||
|
value: (attributeIndex + 4).toFixed(2)
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
|
||||||
|
const stock: AllOrAttribute = {
|
||||||
|
all: false,
|
||||||
|
attribute: selectedAttributes[1].id,
|
||||||
|
value: "8",
|
||||||
|
values: selectedAttributes[1].values.map((attribute, attributeIndex) => ({
|
||||||
|
slug: attribute.slug,
|
||||||
|
value: (selectedAttributes.length * 10 - attributeIndex).toString()
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
|
||||||
|
const dataAttributes = selectedAttributes.map(attribute => ({
|
||||||
|
id: attribute.id,
|
||||||
|
values: attribute.values
|
||||||
|
.map(value => value.slug)
|
||||||
|
.filter((_, valueIndex) => valueIndex % 2 !== 1)
|
||||||
|
}));
|
||||||
|
|
||||||
|
const errors: ProductVariantBulkCreate_productVariantBulkCreate_bulkProductErrors[] = [
|
||||||
|
{
|
||||||
|
__typename: "BulkProductError",
|
||||||
|
code: ProductErrorCode.UNIQUE,
|
||||||
|
field: "sku",
|
||||||
|
index: 3,
|
||||||
|
message: "Duplicated SKU."
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const props: ProductVariantCreateContentProps = {
|
||||||
|
attributes,
|
||||||
|
currencySymbol: "USD",
|
||||||
|
data: {
|
||||||
|
attributes: dataAttributes,
|
||||||
|
price,
|
||||||
|
stock,
|
||||||
|
variants: createVariants({
|
||||||
|
attributes: dataAttributes,
|
||||||
|
price,
|
||||||
|
stock,
|
||||||
|
variants: []
|
||||||
|
})
|
||||||
|
},
|
||||||
|
dispatchFormDataAction: () => undefined,
|
||||||
|
errors: [],
|
||||||
|
onStepClick: () => undefined,
|
||||||
|
step: "values"
|
||||||
|
};
|
||||||
|
|
||||||
|
storiesOf("Views / Products / Create multiple variants", module)
|
||||||
|
.addDecorator(storyFn => (
|
||||||
|
<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}
|
||||||
|
/>
|
||||||
|
));
|
|
@ -0,0 +1,147 @@
|
||||||
|
import { Theme } from "@material-ui/core/styles";
|
||||||
|
import { makeStyles } from "@material-ui/styles";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { ProductDetails_product_productType_variantAttributes } from "@saleor/products/types/ProductDetails";
|
||||||
|
import { ProductVariantBulkCreate_productVariantBulkCreate_bulkProductErrors } from "@saleor/products/types/ProductVariantBulkCreate";
|
||||||
|
import { isSelected } from "@saleor/utils/lists";
|
||||||
|
import { ProductVariantCreateFormData } from "./form";
|
||||||
|
import ProductVariantCreatePrices from "./ProductVariantCreatePrices";
|
||||||
|
import ProductVariantCreateSummary from "./ProductVariantCreateSummary";
|
||||||
|
import ProductVariantCreateTabs from "./ProductVariantCreateTabs";
|
||||||
|
import ProductVariantCreateValues from "./ProductVariantCreateValues";
|
||||||
|
import { ProductVariantCreateReducerAction } from "./reducer";
|
||||||
|
import { ProductVariantCreateStep } from "./types";
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme: Theme) => ({
|
||||||
|
root: {
|
||||||
|
maxHeight: 400,
|
||||||
|
overflowX: "hidden",
|
||||||
|
overflowY: "scroll",
|
||||||
|
paddingLeft: theme.spacing.unit * 3,
|
||||||
|
paddingRight: theme.spacing.unit * 2,
|
||||||
|
position: "relative",
|
||||||
|
right: theme.spacing.unit * 3,
|
||||||
|
width: `calc(100% + ${theme.spacing.unit * 3}px)`
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
export interface ProductVariantCreateContentProps {
|
||||||
|
attributes: ProductDetails_product_productType_variantAttributes[];
|
||||||
|
currencySymbol: string;
|
||||||
|
data: ProductVariantCreateFormData;
|
||||||
|
dispatchFormDataAction: React.Dispatch<ProductVariantCreateReducerAction>;
|
||||||
|
errors: ProductVariantBulkCreate_productVariantBulkCreate_bulkProductErrors[];
|
||||||
|
step: ProductVariantCreateStep;
|
||||||
|
onStepClick: (step: ProductVariantCreateStep) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ProductVariantCreateContent: React.FC<
|
||||||
|
ProductVariantCreateContentProps
|
||||||
|
> = props => {
|
||||||
|
const {
|
||||||
|
attributes,
|
||||||
|
currencySymbol,
|
||||||
|
data,
|
||||||
|
dispatchFormDataAction,
|
||||||
|
errors,
|
||||||
|
step,
|
||||||
|
onStepClick
|
||||||
|
} = props;
|
||||||
|
const classes = useStyles(props);
|
||||||
|
|
||||||
|
const selectedAttributes = attributes.filter(attribute =>
|
||||||
|
isSelected(
|
||||||
|
attribute.id,
|
||||||
|
data.attributes.map(dataAttribute => dataAttribute.id),
|
||||||
|
(a, b) => a === b
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<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;
|
|
@ -0,0 +1,214 @@
|
||||||
|
import Button from "@material-ui/core/Button";
|
||||||
|
import Dialog from "@material-ui/core/Dialog";
|
||||||
|
import DialogActions from "@material-ui/core/DialogActions";
|
||||||
|
import DialogContent from "@material-ui/core/DialogContent";
|
||||||
|
import DialogTitle from "@material-ui/core/DialogTitle";
|
||||||
|
import { Theme } from "@material-ui/core/styles";
|
||||||
|
import { makeStyles } from "@material-ui/styles";
|
||||||
|
import React from "react";
|
||||||
|
import { FormattedMessage } from "react-intl";
|
||||||
|
|
||||||
|
import useModalDialogErrors from "@saleor/hooks/useModalDialogErrors";
|
||||||
|
import useModalDialogOpen from "@saleor/hooks/useModalDialogOpen";
|
||||||
|
import { ProductVariantBulkCreateInput } from "../../../types/globalTypes";
|
||||||
|
import { createInitialForm, ProductVariantCreateFormData } from "./form";
|
||||||
|
import ProductVariantCreateContent, {
|
||||||
|
ProductVariantCreateContentProps
|
||||||
|
} from "./ProductVariantCreateContent";
|
||||||
|
import reduceProductVariantCreateFormData from "./reducer";
|
||||||
|
import { ProductVariantCreateStep } from "./types";
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme: Theme) => ({
|
||||||
|
button: {
|
||||||
|
marginLeft: theme.spacing.unit * 2
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
overflowX: "visible",
|
||||||
|
overflowY: "hidden",
|
||||||
|
width: 800
|
||||||
|
},
|
||||||
|
spacer: {
|
||||||
|
flex: 1
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
function canHitNext(
|
||||||
|
step: ProductVariantCreateStep,
|
||||||
|
data: ProductVariantCreateFormData
|
||||||
|
): boolean {
|
||||||
|
switch (step) {
|
||||||
|
case "values":
|
||||||
|
return data.attributes.every(attribute => attribute.values.length > 0);
|
||||||
|
case "prices":
|
||||||
|
if (data.price.all) {
|
||||||
|
if (data.price.value === "") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (
|
||||||
|
data.price.attribute === "" ||
|
||||||
|
data.price.values.some(attributeValue => attributeValue.value === "")
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.stock.all) {
|
||||||
|
if (data.stock.value === "") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (
|
||||||
|
data.stock.attribute === "" ||
|
||||||
|
data.stock.values.some(attributeValue => attributeValue.value === "")
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
case "summary":
|
||||||
|
return data.variants.every(variant => variant.sku !== "");
|
||||||
|
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProductVariantCreateDialogProps
|
||||||
|
extends Omit<
|
||||||
|
ProductVariantCreateContentProps,
|
||||||
|
"data" | "dispatchFormDataAction" | "step" | "onStepClick"
|
||||||
|
> {
|
||||||
|
defaultPrice: string;
|
||||||
|
open: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onSubmit: (data: ProductVariantBulkCreateInput[]) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ProductVariantCreateDialog: React.FC<
|
||||||
|
ProductVariantCreateDialogProps
|
||||||
|
> = props => {
|
||||||
|
const {
|
||||||
|
attributes,
|
||||||
|
defaultPrice,
|
||||||
|
errors: apiErrors,
|
||||||
|
open,
|
||||||
|
onClose,
|
||||||
|
onSubmit,
|
||||||
|
...contentProps
|
||||||
|
} = props;
|
||||||
|
const classes = useStyles(props);
|
||||||
|
const [step, setStep] = React.useState<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;
|
|
@ -0,0 +1,313 @@
|
||||||
|
import FormControlLabel from "@material-ui/core/FormControlLabel";
|
||||||
|
import Radio from "@material-ui/core/Radio";
|
||||||
|
import RadioGroup from "@material-ui/core/RadioGroup";
|
||||||
|
import { Theme } from "@material-ui/core/styles";
|
||||||
|
import TextField from "@material-ui/core/TextField";
|
||||||
|
import Typography from "@material-ui/core/Typography";
|
||||||
|
import { makeStyles } from "@material-ui/styles";
|
||||||
|
import React from "react";
|
||||||
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
|
|
||||||
|
import FormSpacer from "@saleor/components/FormSpacer";
|
||||||
|
import Grid from "@saleor/components/Grid";
|
||||||
|
import Hr from "@saleor/components/Hr";
|
||||||
|
import SingleSelectField from "@saleor/components/SingleSelectField";
|
||||||
|
import { ProductDetails_product_productType_variantAttributes } from "@saleor/products/types/ProductDetails";
|
||||||
|
import { ProductVariantCreateFormData } from "./form";
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme: Theme) => ({
|
||||||
|
hr: {
|
||||||
|
marginBottom: theme.spacing.unit,
|
||||||
|
marginTop: theme.spacing.unit / 2
|
||||||
|
},
|
||||||
|
hrAttribute: {
|
||||||
|
marginTop: theme.spacing.unit * 2
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
alignSelf: "center"
|
||||||
|
},
|
||||||
|
shortInput: {
|
||||||
|
width: "50%"
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
export type PriceOrStock = "price" | "stock";
|
||||||
|
export interface ProductVariantCreatePricesProps {
|
||||||
|
attributes: ProductDetails_product_productType_variantAttributes[];
|
||||||
|
currencySymbol: string;
|
||||||
|
data: ProductVariantCreateFormData;
|
||||||
|
onApplyPriceOrStockChange: (applyToAll: boolean, type: PriceOrStock) => void;
|
||||||
|
onApplyToAllChange: (value: string, type: PriceOrStock) => void;
|
||||||
|
onAttributeSelect: (id: string, type: PriceOrStock) => void;
|
||||||
|
onAttributeValueChange: (
|
||||||
|
id: string,
|
||||||
|
value: string,
|
||||||
|
type: PriceOrStock
|
||||||
|
) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ProductVariantCreatePrices: React.FC<
|
||||||
|
ProductVariantCreatePricesProps
|
||||||
|
> = props => {
|
||||||
|
const {
|
||||||
|
attributes,
|
||||||
|
currencySymbol,
|
||||||
|
data,
|
||||||
|
onApplyPriceOrStockChange,
|
||||||
|
onApplyToAllChange,
|
||||||
|
onAttributeSelect,
|
||||||
|
onAttributeValueChange
|
||||||
|
} = props;
|
||||||
|
const classes = useStyles(props);
|
||||||
|
const intl = useIntl();
|
||||||
|
|
||||||
|
const attributeChoices = attributes.map(attribute => ({
|
||||||
|
label: attribute.name,
|
||||||
|
value: attribute.id
|
||||||
|
}));
|
||||||
|
const priceAttributeValues = data.price.all
|
||||||
|
? null
|
||||||
|
: data.price.attribute
|
||||||
|
? attributes
|
||||||
|
.find(attribute => attribute.id === data.price.attribute)
|
||||||
|
.values.filter(value =>
|
||||||
|
data.attributes
|
||||||
|
.find(attribute => attribute.id === data.price.attribute)
|
||||||
|
.values.includes(value.slug)
|
||||||
|
)
|
||||||
|
: [];
|
||||||
|
const stockAttributeValues = data.stock.all
|
||||||
|
? null
|
||||||
|
: data.stock.attribute
|
||||||
|
? attributes
|
||||||
|
.find(attribute => attribute.id === data.stock.attribute)
|
||||||
|
.values.filter(value =>
|
||||||
|
data.attributes
|
||||||
|
.find(attribute => attribute.id === data.stock.attribute)
|
||||||
|
.values.includes(value.slug)
|
||||||
|
)
|
||||||
|
: [];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<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 => (
|
||||||
|
<>
|
||||||
|
<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>
|
||||||
|
</>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<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 => (
|
||||||
|
<>
|
||||||
|
<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>
|
||||||
|
</>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
ProductVariantCreatePrices.displayName = "ProductVariantCreatePrices";
|
||||||
|
export default ProductVariantCreatePrices;
|
|
@ -0,0 +1,300 @@
|
||||||
|
import blue from "@material-ui/core/colors/blue";
|
||||||
|
import cyan from "@material-ui/core/colors/cyan";
|
||||||
|
import green from "@material-ui/core/colors/green";
|
||||||
|
import purple from "@material-ui/core/colors/purple";
|
||||||
|
import yellow from "@material-ui/core/colors/yellow";
|
||||||
|
import IconButton from "@material-ui/core/IconButton";
|
||||||
|
import { Theme } from "@material-ui/core/styles";
|
||||||
|
import TextField from "@material-ui/core/TextField";
|
||||||
|
import Typography from "@material-ui/core/Typography";
|
||||||
|
import DeleteIcon from "@material-ui/icons/Delete";
|
||||||
|
import { makeStyles } from "@material-ui/styles";
|
||||||
|
import classNames from "classnames";
|
||||||
|
import React from "react";
|
||||||
|
import { FormattedMessage } from "react-intl";
|
||||||
|
|
||||||
|
import Hr from "@saleor/components/Hr";
|
||||||
|
import { maybe } from "@saleor/misc";
|
||||||
|
import { ProductVariantBulkCreate_productVariantBulkCreate_bulkProductErrors } from "@saleor/products/types/ProductVariantBulkCreate";
|
||||||
|
import { ProductVariantBulkCreateInput } from "@saleor/types/globalTypes";
|
||||||
|
import { ProductDetails_product_productType_variantAttributes } from "../../types/ProductDetails";
|
||||||
|
import { ProductVariantCreateFormData } from "./form";
|
||||||
|
import { VariantField } from "./reducer";
|
||||||
|
|
||||||
|
export interface ProductVariantCreateSummaryProps {
|
||||||
|
attributes: ProductDetails_product_productType_variantAttributes[];
|
||||||
|
currencySymbol: string;
|
||||||
|
data: ProductVariantCreateFormData;
|
||||||
|
errors: ProductVariantBulkCreate_productVariantBulkCreate_bulkProductErrors[];
|
||||||
|
onVariantDataChange: (
|
||||||
|
variantIndex: number,
|
||||||
|
field: VariantField,
|
||||||
|
value: string
|
||||||
|
) => void;
|
||||||
|
onVariantDelete: (variantIndex: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const colors = [blue, cyan, green, purple, yellow].map(color => color[800]);
|
||||||
|
|
||||||
|
const useStyles = makeStyles(
|
||||||
|
(theme: Theme) => ({
|
||||||
|
attributeValue: {
|
||||||
|
display: "inline-block",
|
||||||
|
marginRight: theme.spacing.unit
|
||||||
|
},
|
||||||
|
col: {
|
||||||
|
...theme.typography.body2,
|
||||||
|
fontSize: 14,
|
||||||
|
paddingLeft: theme.spacing.unit,
|
||||||
|
paddingRight: theme.spacing.unit
|
||||||
|
},
|
||||||
|
colHeader: {
|
||||||
|
...theme.typography.body2,
|
||||||
|
fontSize: 14
|
||||||
|
},
|
||||||
|
colName: {
|
||||||
|
"&&": {
|
||||||
|
paddingLeft: "0 !important"
|
||||||
|
},
|
||||||
|
"&:not($colHeader)": {
|
||||||
|
paddingTop: theme.spacing.unit * 2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
colPrice: {},
|
||||||
|
colSku: {},
|
||||||
|
colStock: {},
|
||||||
|
delete: {
|
||||||
|
marginTop: theme.spacing.unit / 2
|
||||||
|
},
|
||||||
|
errorRow: {},
|
||||||
|
hr: {
|
||||||
|
marginBottom: theme.spacing.unit,
|
||||||
|
marginTop: theme.spacing.unit / 2
|
||||||
|
},
|
||||||
|
input: {
|
||||||
|
"& input": {
|
||||||
|
padding: "16px 12px 17px"
|
||||||
|
},
|
||||||
|
marginTop: theme.spacing.unit / 2
|
||||||
|
},
|
||||||
|
row: {
|
||||||
|
borderBottom: `1px solid ${theme.palette.divider}`,
|
||||||
|
display: "grid",
|
||||||
|
gridTemplateColumns: "1fr 180px 120px 180px 64px",
|
||||||
|
padding: `${theme.spacing.unit}px 0`
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
name: "ProductVariantCreateSummary"
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
function getVariantName(
|
||||||
|
variant: ProductVariantBulkCreateInput,
|
||||||
|
attributes: ProductDetails_product_productType_variantAttributes[]
|
||||||
|
): string[] {
|
||||||
|
return attributes.reduce(
|
||||||
|
(acc, attribute) => [
|
||||||
|
...acc,
|
||||||
|
attribute.values.find(
|
||||||
|
value =>
|
||||||
|
value.slug ===
|
||||||
|
variant.attributes.find(
|
||||||
|
variantAttribute => variantAttribute.id === attribute.id
|
||||||
|
).values[0]
|
||||||
|
).name
|
||||||
|
],
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ProductVariantCreateSummary: React.FC<
|
||||||
|
ProductVariantCreateSummaryProps
|
||||||
|
> = props => {
|
||||||
|
const {
|
||||||
|
attributes,
|
||||||
|
currencySymbol,
|
||||||
|
data,
|
||||||
|
errors,
|
||||||
|
onVariantDataChange,
|
||||||
|
onVariantDelete
|
||||||
|
} = props;
|
||||||
|
const classes = useStyles(props);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<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>
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
classes.col,
|
||||||
|
classes.colHeader,
|
||||||
|
classes.colStock
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<FormattedMessage
|
||||||
|
defaultMessage="Inventory"
|
||||||
|
description="variant stock amount"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
classes.col,
|
||||||
|
classes.colHeader,
|
||||||
|
classes.colSku
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<FormattedMessage defaultMessage="SKU" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{data.variants.map((variant, variantIndex) => {
|
||||||
|
const variantErrors = errors.filter(
|
||||||
|
error => error.index === variantIndex
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={classNames(classes.row, {
|
||||||
|
[classes.errorRow]: variantErrors.length > 0
|
||||||
|
})}
|
||||||
|
key={variant.attributes
|
||||||
|
.map(attribute => attribute.values[0])
|
||||||
|
.join(":")}
|
||||||
|
>
|
||||||
|
<div className={classNames(classes.col, classes.colName)}>
|
||||||
|
{getVariantName(variant, attributes).map(
|
||||||
|
(value, valueIndex) => (
|
||||||
|
<span
|
||||||
|
className={classes.attributeValue}
|
||||||
|
style={{
|
||||||
|
color: colors[valueIndex % colors.length]
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{value}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className={classNames(classes.col, classes.colPrice)}>
|
||||||
|
<TextField
|
||||||
|
InputProps={{
|
||||||
|
endAdornment: currencySymbol
|
||||||
|
}}
|
||||||
|
className={classes.input}
|
||||||
|
error={
|
||||||
|
!!variantErrors.find(
|
||||||
|
error => error.field === "priceOverride"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
helperText={maybe(
|
||||||
|
() =>
|
||||||
|
variantErrors.find(
|
||||||
|
error => error.field === "priceOverride"
|
||||||
|
).message
|
||||||
|
)}
|
||||||
|
inputProps={{
|
||||||
|
min: 0,
|
||||||
|
type: "number"
|
||||||
|
}}
|
||||||
|
fullWidth
|
||||||
|
value={variant.priceOverride}
|
||||||
|
onChange={event =>
|
||||||
|
onVariantDataChange(
|
||||||
|
variantIndex,
|
||||||
|
"price",
|
||||||
|
event.target.value
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={classNames(classes.col, classes.colStock)}>
|
||||||
|
<TextField
|
||||||
|
className={classes.input}
|
||||||
|
error={
|
||||||
|
!!variantErrors.find(error => error.field === "quantity")
|
||||||
|
}
|
||||||
|
helperText={maybe(
|
||||||
|
() =>
|
||||||
|
variantErrors.find(error => error.field === "quantity")
|
||||||
|
.message
|
||||||
|
)}
|
||||||
|
inputProps={{
|
||||||
|
min: 0,
|
||||||
|
type: "number"
|
||||||
|
}}
|
||||||
|
fullWidth
|
||||||
|
value={variant.quantity}
|
||||||
|
onChange={event =>
|
||||||
|
onVariantDataChange(
|
||||||
|
variantIndex,
|
||||||
|
"stock",
|
||||||
|
event.target.value
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={classNames(classes.col, classes.colSku)}>
|
||||||
|
<TextField
|
||||||
|
className={classes.input}
|
||||||
|
error={!!variantErrors.find(error => error.field === "sku")}
|
||||||
|
helperText={maybe(
|
||||||
|
() =>
|
||||||
|
variantErrors.find(error => error.field === "sku").message
|
||||||
|
)}
|
||||||
|
fullWidth
|
||||||
|
value={variant.sku}
|
||||||
|
onChange={event =>
|
||||||
|
onVariantDataChange(variantIndex, "sku", event.target.value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={classes.col}>
|
||||||
|
<IconButton
|
||||||
|
className={classes.delete}
|
||||||
|
color="primary"
|
||||||
|
onClick={() => onVariantDelete(variantIndex)}
|
||||||
|
>
|
||||||
|
<DeleteIcon />
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
ProductVariantCreateSummary.displayName = "ProductVariantCreateSummary";
|
||||||
|
export default ProductVariantCreateSummary;
|
|
@ -0,0 +1,109 @@
|
||||||
|
import { Theme } from "@material-ui/core/styles";
|
||||||
|
import Typography from "@material-ui/core/Typography";
|
||||||
|
import { makeStyles } from "@material-ui/styles";
|
||||||
|
import classNames from "classnames";
|
||||||
|
import React from "react";
|
||||||
|
import { IntlShape, useIntl } from "react-intl";
|
||||||
|
|
||||||
|
import { ProductVariantCreateStep } from "./types";
|
||||||
|
|
||||||
|
interface Step {
|
||||||
|
label: string;
|
||||||
|
value: ProductVariantCreateStep;
|
||||||
|
}
|
||||||
|
function getSteps(intl: IntlShape): Step[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
label: intl.formatMessage({
|
||||||
|
defaultMessage: "Select Values",
|
||||||
|
description: "attribute values, variant creation step"
|
||||||
|
}),
|
||||||
|
value: "values"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: intl.formatMessage({
|
||||||
|
defaultMessage: "Prices and SKU",
|
||||||
|
description: "variant creation step"
|
||||||
|
}),
|
||||||
|
value: "prices"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: intl.formatMessage({
|
||||||
|
defaultMessage: "Summary",
|
||||||
|
description: "variant creation step"
|
||||||
|
}),
|
||||||
|
value: "summary"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
const useStyles = makeStyles(
|
||||||
|
(theme: Theme) => ({
|
||||||
|
label: {
|
||||||
|
fontSize: 14,
|
||||||
|
textAlign: "center"
|
||||||
|
},
|
||||||
|
root: {
|
||||||
|
borderBottom: `1px solid ${theme.palette.divider}`,
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
marginBottom: theme.spacing.unit * 3
|
||||||
|
},
|
||||||
|
tab: {
|
||||||
|
flex: 1,
|
||||||
|
paddingBottom: theme.spacing.unit,
|
||||||
|
userSelect: "none"
|
||||||
|
},
|
||||||
|
tabActive: {
|
||||||
|
fontWeight: 600
|
||||||
|
},
|
||||||
|
tabVisited: {
|
||||||
|
borderBottom: `3px solid ${theme.palette.primary.main}`,
|
||||||
|
cursor: "pointer"
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
name: "ProductVariantCreateTabs"
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export interface ProductVariantCreateTabsProps {
|
||||||
|
step: ProductVariantCreateStep;
|
||||||
|
onStepClick: (step: ProductVariantCreateStep) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ProductVariantCreateTabs: React.FC<
|
||||||
|
ProductVariantCreateTabsProps
|
||||||
|
> = props => {
|
||||||
|
const { step: currentStep, onStepClick } = props;
|
||||||
|
const classes = useStyles(props);
|
||||||
|
const intl = useIntl();
|
||||||
|
const steps = getSteps(intl);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classes.root}>
|
||||||
|
{steps.map((step, stepIndex) => {
|
||||||
|
const visitedStep =
|
||||||
|
steps.findIndex(step => step.value === currentStep) >= stepIndex;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={classNames(classes.tab, {
|
||||||
|
[classes.tabActive]: step.value === currentStep,
|
||||||
|
[classes.tabVisited]: visitedStep
|
||||||
|
})}
|
||||||
|
onClick={visitedStep ? () => onStepClick(step.value) : undefined}
|
||||||
|
key={step.value}
|
||||||
|
>
|
||||||
|
<Typography className={classes.label} variant="caption">
|
||||||
|
{step.label}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
ProductVariantCreateTabs.displayName = "ProductVariantCreateTabs";
|
||||||
|
export default ProductVariantCreateTabs;
|
|
@ -0,0 +1,79 @@
|
||||||
|
import { Theme } from "@material-ui/core/styles";
|
||||||
|
import Typography from "@material-ui/core/Typography";
|
||||||
|
import makeStyles from "@material-ui/styles/makeStyles";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import ControlledCheckbox from "@saleor/components/ControlledCheckbox";
|
||||||
|
import Debounce from "@saleor/components/Debounce";
|
||||||
|
import Hr from "@saleor/components/Hr";
|
||||||
|
import Skeleton from "@saleor/components/Skeleton";
|
||||||
|
import { maybe } from "@saleor/misc";
|
||||||
|
import { ProductDetails_product_productType_variantAttributes } from "@saleor/products/types/ProductDetails";
|
||||||
|
import { isSelected } from "@saleor/utils/lists";
|
||||||
|
import { ProductVariantCreateFormData } from "./form";
|
||||||
|
|
||||||
|
export interface ProductVariantCreateValuesProps {
|
||||||
|
attributes: ProductDetails_product_productType_variantAttributes[];
|
||||||
|
data: ProductVariantCreateFormData;
|
||||||
|
onValueClick: (attributeId: string, valueId: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme: Theme) => ({
|
||||||
|
hr: {
|
||||||
|
marginBottom: theme.spacing.unit,
|
||||||
|
marginTop: theme.spacing.unit / 2
|
||||||
|
},
|
||||||
|
valueContainer: {
|
||||||
|
display: "grid",
|
||||||
|
gridColumnGap: theme.spacing.unit * 3 + "px",
|
||||||
|
gridTemplateColumns: "repeat(3, 1fr)",
|
||||||
|
marginBottom: theme.spacing.unit * 3
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
const ProductVariantCreateValues: React.FC<
|
||||||
|
ProductVariantCreateValuesProps
|
||||||
|
> = props => {
|
||||||
|
const { attributes, data, onValueClick } = props;
|
||||||
|
const classes = useStyles(props);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{attributes.map(attribute => (
|
||||||
|
<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;
|
File diff suppressed because it is too large
Load diff
|
@ -0,0 +1,220 @@
|
||||||
|
import {
|
||||||
|
createVariantFlatMatrixDimension,
|
||||||
|
createVariants
|
||||||
|
} from "./createVariants";
|
||||||
|
import { attributes, thirdStep } from "./fixtures";
|
||||||
|
import { ProductVariantCreateFormData } from "./form";
|
||||||
|
|
||||||
|
describe("Creates variant matrix", () => {
|
||||||
|
it("with proper size", () => {
|
||||||
|
const attributes = thirdStep.attributes;
|
||||||
|
|
||||||
|
const matrix = createVariantFlatMatrixDimension([[]], attributes);
|
||||||
|
|
||||||
|
expect(matrix).toHaveLength(
|
||||||
|
attributes.reduce((acc, attribute) => acc * attribute.values.length, 1)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("with constant price and stock", () => {
|
||||||
|
const price = "49.99";
|
||||||
|
const stock = 80;
|
||||||
|
|
||||||
|
const data: ProductVariantCreateFormData = {
|
||||||
|
...thirdStep,
|
||||||
|
price: {
|
||||||
|
...thirdStep.price,
|
||||||
|
all: true,
|
||||||
|
value: price
|
||||||
|
},
|
||||||
|
stock: {
|
||||||
|
...thirdStep.stock,
|
||||||
|
all: true,
|
||||||
|
value: stock.toString()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const variants = createVariants(data);
|
||||||
|
expect(variants).toHaveLength(
|
||||||
|
thirdStep.attributes.reduce(
|
||||||
|
(acc, attribute) => acc * attribute.values.length,
|
||||||
|
1
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
variants.forEach(variant => {
|
||||||
|
expect(variant.priceOverride).toBe(price);
|
||||||
|
expect(variant.quantity).toBe(stock);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("with constant stock and attribute dependent price", () => {
|
||||||
|
const price = 49.99;
|
||||||
|
const stock = 80;
|
||||||
|
const attribute = attributes.find(
|
||||||
|
attribute => attribute.id === thirdStep.attributes[0].id
|
||||||
|
);
|
||||||
|
|
||||||
|
const data: ProductVariantCreateFormData = {
|
||||||
|
...thirdStep,
|
||||||
|
price: {
|
||||||
|
...thirdStep.price,
|
||||||
|
all: false,
|
||||||
|
attribute: attribute.id,
|
||||||
|
values: attribute.values.map((attributeValue, attributeValueIndex) => ({
|
||||||
|
slug: attributeValue,
|
||||||
|
value: (price * (attributeValueIndex + 1)).toString()
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
stock: {
|
||||||
|
...thirdStep.stock,
|
||||||
|
all: true,
|
||||||
|
value: stock.toString()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const variants = createVariants(data);
|
||||||
|
expect(variants).toHaveLength(
|
||||||
|
thirdStep.attributes.reduce(
|
||||||
|
(acc, attribute) => acc * attribute.values.length,
|
||||||
|
1
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
variants.forEach(variant => {
|
||||||
|
expect(variant.quantity).toBe(stock);
|
||||||
|
});
|
||||||
|
|
||||||
|
attribute.values.forEach((attributeValue, attributeValueIndex) => {
|
||||||
|
variants
|
||||||
|
.filter(
|
||||||
|
variant =>
|
||||||
|
variant.attributes.find(
|
||||||
|
variantAttribute => variantAttribute.id === attribute.id
|
||||||
|
).values[0] === attributeValue
|
||||||
|
)
|
||||||
|
.forEach(variant => {
|
||||||
|
expect(variant.priceOverride).toBe(
|
||||||
|
(price * (attributeValueIndex + 1)).toString()
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("with constant price and attribute dependent stock", () => {
|
||||||
|
const price = "49.99";
|
||||||
|
const stock = 80;
|
||||||
|
const attribute = attributes.find(
|
||||||
|
attribute => attribute.id === thirdStep.attributes[0].id
|
||||||
|
);
|
||||||
|
|
||||||
|
const data: ProductVariantCreateFormData = {
|
||||||
|
...thirdStep,
|
||||||
|
price: {
|
||||||
|
...thirdStep.price,
|
||||||
|
all: true,
|
||||||
|
value: price
|
||||||
|
},
|
||||||
|
stock: {
|
||||||
|
...thirdStep.stock,
|
||||||
|
all: false,
|
||||||
|
attribute: attribute.id,
|
||||||
|
values: attribute.values.map((attributeValue, attributeValueIndex) => ({
|
||||||
|
slug: attributeValue,
|
||||||
|
value: (stock * (attributeValueIndex + 1)).toString()
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const variants = createVariants(data);
|
||||||
|
expect(variants).toHaveLength(
|
||||||
|
thirdStep.attributes.reduce(
|
||||||
|
(acc, attribute) => acc * attribute.values.length,
|
||||||
|
1
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
variants.forEach(variant => {
|
||||||
|
expect(variant.priceOverride).toBe(price);
|
||||||
|
});
|
||||||
|
|
||||||
|
attribute.values.forEach((attributeValue, attributeValueIndex) => {
|
||||||
|
variants
|
||||||
|
.filter(
|
||||||
|
variant =>
|
||||||
|
variant.attributes.find(
|
||||||
|
variantAttribute => variantAttribute.id === attribute.id
|
||||||
|
).values[0] === attributeValue
|
||||||
|
)
|
||||||
|
.forEach(variant => {
|
||||||
|
expect(variant.quantity).toBe(stock * (attributeValueIndex + 1));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("with attribute dependent price and stock", () => {
|
||||||
|
const price = 49.99;
|
||||||
|
const stock = 80;
|
||||||
|
const attribute = attributes.find(
|
||||||
|
attribute => attribute.id === thirdStep.attributes[0].id
|
||||||
|
);
|
||||||
|
|
||||||
|
const data: ProductVariantCreateFormData = {
|
||||||
|
...thirdStep,
|
||||||
|
price: {
|
||||||
|
...thirdStep.price,
|
||||||
|
all: false,
|
||||||
|
attribute: attribute.id,
|
||||||
|
values: attribute.values.map((attributeValue, attributeValueIndex) => ({
|
||||||
|
slug: attributeValue,
|
||||||
|
value: (price * (attributeValueIndex + 1)).toString()
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
stock: {
|
||||||
|
...thirdStep.stock,
|
||||||
|
all: false,
|
||||||
|
attribute: attribute.id,
|
||||||
|
values: attribute.values.map((attributeValue, attributeValueIndex) => ({
|
||||||
|
slug: attributeValue,
|
||||||
|
value: (stock * (attributeValueIndex + 1)).toString()
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const variants = createVariants(data);
|
||||||
|
expect(variants).toHaveLength(
|
||||||
|
thirdStep.attributes.reduce(
|
||||||
|
(acc, attribute) => acc * attribute.values.length,
|
||||||
|
1
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
attribute.values.forEach((attributeValue, attributeValueIndex) => {
|
||||||
|
variants
|
||||||
|
.filter(
|
||||||
|
variant =>
|
||||||
|
variant.attributes.find(
|
||||||
|
variantAttribute => variantAttribute.id === attribute.id
|
||||||
|
).values[0] === attributeValue
|
||||||
|
)
|
||||||
|
.forEach(variant => {
|
||||||
|
expect(variant.priceOverride).toBe(
|
||||||
|
(price * (attributeValueIndex + 1)).toString()
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
attribute.values.forEach((attributeValue, attributeValueIndex) => {
|
||||||
|
variants
|
||||||
|
.filter(
|
||||||
|
variant =>
|
||||||
|
variant.attributes.find(
|
||||||
|
variantAttribute => variantAttribute.id === attribute.id
|
||||||
|
).values[0] === attributeValue
|
||||||
|
)
|
||||||
|
.forEach(variant => {
|
||||||
|
expect(variant.quantity).toBe(stock * (attributeValueIndex + 1));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,105 @@
|
||||||
|
import { ProductVariantBulkCreateInput } from "@saleor/types/globalTypes";
|
||||||
|
import {
|
||||||
|
AllOrAttribute,
|
||||||
|
Attribute,
|
||||||
|
ProductVariantCreateFormData
|
||||||
|
} from "./form";
|
||||||
|
|
||||||
|
interface CreateVariantAttributeValueInput {
|
||||||
|
attributeId: string;
|
||||||
|
attributeValueSlug: string;
|
||||||
|
}
|
||||||
|
type CreateVariantInput = CreateVariantAttributeValueInput[];
|
||||||
|
|
||||||
|
function getAttributeValuePriceOrStock(
|
||||||
|
attributes: CreateVariantInput,
|
||||||
|
priceOrStock: AllOrAttribute
|
||||||
|
): string {
|
||||||
|
const attribute = attributes.find(
|
||||||
|
attribute => attribute.attributeId === priceOrStock.attribute
|
||||||
|
);
|
||||||
|
|
||||||
|
const attributeValue = priceOrStock.values.find(
|
||||||
|
attributeValue => attribute.attributeValueSlug === attributeValue.slug
|
||||||
|
);
|
||||||
|
|
||||||
|
return attributeValue.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createVariant(
|
||||||
|
data: ProductVariantCreateFormData,
|
||||||
|
attributes: CreateVariantInput
|
||||||
|
): ProductVariantBulkCreateInput {
|
||||||
|
const priceOverride = data.price.all
|
||||||
|
? data.price.value
|
||||||
|
: getAttributeValuePriceOrStock(attributes, data.price);
|
||||||
|
const quantity = parseInt(
|
||||||
|
data.stock.all
|
||||||
|
? data.stock.value
|
||||||
|
: getAttributeValuePriceOrStock(attributes, data.stock),
|
||||||
|
10
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
attributes: attributes.map(attribute => ({
|
||||||
|
id: attribute.attributeId,
|
||||||
|
values: [attribute.attributeValueSlug]
|
||||||
|
})),
|
||||||
|
priceOverride,
|
||||||
|
quantity,
|
||||||
|
sku: ""
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function addAttributeToVariant(
|
||||||
|
attribute: Attribute,
|
||||||
|
variant: CreateVariantInput
|
||||||
|
): CreateVariantInput[] {
|
||||||
|
return attribute.values.map(attributeValueSlug => [
|
||||||
|
...variant,
|
||||||
|
{
|
||||||
|
attributeId: attribute.id,
|
||||||
|
attributeValueSlug
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
function addVariantAttributeInput(
|
||||||
|
data: CreateVariantInput[],
|
||||||
|
attribute: Attribute
|
||||||
|
): CreateVariantInput[] {
|
||||||
|
const variants = data
|
||||||
|
.map(variant => addAttributeToVariant(attribute, variant))
|
||||||
|
.reduce((acc, variantInput) => [...acc, ...variantInput]);
|
||||||
|
|
||||||
|
return variants;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createVariantFlatMatrixDimension(
|
||||||
|
variants: CreateVariantInput[],
|
||||||
|
attributes: Attribute[]
|
||||||
|
): CreateVariantInput[] {
|
||||||
|
if (attributes.length > 0) {
|
||||||
|
return createVariantFlatMatrixDimension(
|
||||||
|
addVariantAttributeInput(variants, attributes[0]),
|
||||||
|
attributes.slice(1)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return variants;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createVariants(
|
||||||
|
data: ProductVariantCreateFormData
|
||||||
|
): ProductVariantBulkCreateInput[] {
|
||||||
|
if (
|
||||||
|
(!data.price.all && !data.price.attribute) ||
|
||||||
|
(!data.stock.all && !data.stock.attribute)
|
||||||
|
) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const variants = createVariantFlatMatrixDimension([[]], data.attributes).map(
|
||||||
|
variant => createVariant(data, variant)
|
||||||
|
);
|
||||||
|
|
||||||
|
return variants;
|
||||||
|
}
|
110
src/products/components/ProductVariantCreateDialog/fixtures.ts
Normal file
110
src/products/components/ProductVariantCreateDialog/fixtures.ts
Normal file
|
@ -0,0 +1,110 @@
|
||||||
|
import { createVariants } from "./createVariants";
|
||||||
|
import {
|
||||||
|
AllOrAttribute,
|
||||||
|
createInitialForm,
|
||||||
|
ProductVariantCreateFormData
|
||||||
|
} from "./form";
|
||||||
|
|
||||||
|
export const attributes = [
|
||||||
|
{
|
||||||
|
id: "attr-1",
|
||||||
|
values: Array(9)
|
||||||
|
.fill(0)
|
||||||
|
.map((_, index) => `val-1-${index + 1}`)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "attr-2",
|
||||||
|
values: Array(6)
|
||||||
|
.fill(0)
|
||||||
|
.map((_, index) => `val-2-${index + 1}`)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "attr-3",
|
||||||
|
values: Array(4)
|
||||||
|
.fill(0)
|
||||||
|
.map((_, index) => `val-3-${index + 1}`)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "attr-4",
|
||||||
|
values: Array(11)
|
||||||
|
.fill(0)
|
||||||
|
.map((_, index) => `val-4-${index + 1}`)
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
export const secondStep: ProductVariantCreateFormData = {
|
||||||
|
...createInitialForm([], "10.99"),
|
||||||
|
attributes: [
|
||||||
|
{
|
||||||
|
id: attributes[0].id,
|
||||||
|
values: []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: attributes[1].id,
|
||||||
|
values: []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: attributes[3].id,
|
||||||
|
values: []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
export const thirdStep: ProductVariantCreateFormData = {
|
||||||
|
...secondStep,
|
||||||
|
attributes: [
|
||||||
|
{
|
||||||
|
id: attributes[0].id,
|
||||||
|
values: [0, 6].map(index => attributes[0].values[index])
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: attributes[1].id,
|
||||||
|
values: [1, 3].map(index => attributes[1].values[index])
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: attributes[3].id,
|
||||||
|
values: [0, 4].map(index => attributes[3].values[index])
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
const price: AllOrAttribute = {
|
||||||
|
all: false,
|
||||||
|
attribute: thirdStep.attributes[1].id,
|
||||||
|
value: "",
|
||||||
|
values: [
|
||||||
|
{
|
||||||
|
slug: thirdStep.attributes[1].values[0],
|
||||||
|
value: "24.99"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: thirdStep.attributes[1].values[1],
|
||||||
|
value: "26.99"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
const stock: AllOrAttribute = {
|
||||||
|
all: false,
|
||||||
|
attribute: thirdStep.attributes[2].id,
|
||||||
|
value: "",
|
||||||
|
values: [
|
||||||
|
{
|
||||||
|
slug: thirdStep.attributes[2].values[0],
|
||||||
|
value: "50"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: thirdStep.attributes[2].values[1],
|
||||||
|
value: "35"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
export const fourthStep: ProductVariantCreateFormData = {
|
||||||
|
...thirdStep,
|
||||||
|
price,
|
||||||
|
stock,
|
||||||
|
variants: createVariants({
|
||||||
|
...thirdStep,
|
||||||
|
price,
|
||||||
|
stock
|
||||||
|
})
|
||||||
|
};
|
46
src/products/components/ProductVariantCreateDialog/form.ts
Normal file
46
src/products/components/ProductVariantCreateDialog/form.ts
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
import { ProductDetails_product_productType_variantAttributes } from "@saleor/products/types/ProductDetails";
|
||||||
|
import { ProductVariantBulkCreateInput } from "../../../types/globalTypes";
|
||||||
|
|
||||||
|
export interface AttributeValue {
|
||||||
|
slug: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
export interface AllOrAttribute {
|
||||||
|
all: boolean;
|
||||||
|
attribute: string;
|
||||||
|
value: string;
|
||||||
|
values: AttributeValue[];
|
||||||
|
}
|
||||||
|
export interface Attribute {
|
||||||
|
id: string;
|
||||||
|
values: string[];
|
||||||
|
}
|
||||||
|
export interface ProductVariantCreateFormData {
|
||||||
|
attributes: Attribute[];
|
||||||
|
price: AllOrAttribute;
|
||||||
|
stock: AllOrAttribute;
|
||||||
|
variants: ProductVariantBulkCreateInput[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createInitialForm = (
|
||||||
|
attributes: ProductDetails_product_productType_variantAttributes[],
|
||||||
|
price: string
|
||||||
|
): ProductVariantCreateFormData => ({
|
||||||
|
attributes: attributes.map(attribute => ({
|
||||||
|
id: attribute.id,
|
||||||
|
values: []
|
||||||
|
})),
|
||||||
|
price: {
|
||||||
|
all: true,
|
||||||
|
attribute: undefined,
|
||||||
|
value: price || "",
|
||||||
|
values: []
|
||||||
|
},
|
||||||
|
stock: {
|
||||||
|
all: true,
|
||||||
|
attribute: undefined,
|
||||||
|
value: "",
|
||||||
|
values: []
|
||||||
|
},
|
||||||
|
variants: []
|
||||||
|
});
|
|
@ -0,0 +1,185 @@
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
363
src/products/components/ProductVariantCreateDialog/reducer.ts
Normal file
363
src/products/components/ProductVariantCreateDialog/reducer.ts
Normal file
|
@ -0,0 +1,363 @@
|
||||||
|
//#region
|
||||||
|
import {
|
||||||
|
add,
|
||||||
|
remove,
|
||||||
|
removeAtIndex,
|
||||||
|
toggle,
|
||||||
|
updateAtIndex
|
||||||
|
} from "@saleor/utils/lists";
|
||||||
|
import { createVariants } from "./createVariants";
|
||||||
|
import { ProductVariantCreateFormData } from "./form";
|
||||||
|
|
||||||
|
export type ProductVariantCreateReducerActionType =
|
||||||
|
| "applyPriceToAll"
|
||||||
|
| "applyPriceToAttribute"
|
||||||
|
| "applyStockToAll"
|
||||||
|
| "applyStockToAttribute"
|
||||||
|
| "changeApplyPriceToAllValue"
|
||||||
|
| "changeApplyPriceToAttributeId"
|
||||||
|
| "changeApplyStockToAllValue"
|
||||||
|
| "changeApplyStockToAttributeId"
|
||||||
|
| "changeAttributeValuePrice"
|
||||||
|
| "changeAttributeValueStock"
|
||||||
|
| "changeVariantData"
|
||||||
|
| "deleteVariant"
|
||||||
|
| "reload"
|
||||||
|
| "selectValue";
|
||||||
|
|
||||||
|
export type VariantField = "stock" | "price" | "sku";
|
||||||
|
export interface ProductVariantCreateReducerAction {
|
||||||
|
all?: boolean;
|
||||||
|
attributeId?: string;
|
||||||
|
data?: ProductVariantCreateFormData;
|
||||||
|
field?: VariantField;
|
||||||
|
type: ProductVariantCreateReducerActionType;
|
||||||
|
value?: string;
|
||||||
|
valueId?: string;
|
||||||
|
variantIndex?: number;
|
||||||
|
}
|
||||||
|
//#endregion
|
||||||
|
function selectValue(
|
||||||
|
prevState: ProductVariantCreateFormData,
|
||||||
|
attributeId: string,
|
||||||
|
valueSlug: string
|
||||||
|
): ProductVariantCreateFormData {
|
||||||
|
const attribute = prevState.attributes.find(
|
||||||
|
attribute => attribute.id === attributeId
|
||||||
|
);
|
||||||
|
const values = toggle(valueSlug, attribute.values, (a, b) => a === b);
|
||||||
|
const updatedAttributes = add(
|
||||||
|
{
|
||||||
|
id: attributeId,
|
||||||
|
values
|
||||||
|
},
|
||||||
|
remove(attribute, prevState.attributes, (a, b) => a.id === b.id)
|
||||||
|
);
|
||||||
|
|
||||||
|
const priceValues =
|
||||||
|
prevState.price.attribute === attributeId
|
||||||
|
? toggle(
|
||||||
|
{
|
||||||
|
slug: valueSlug,
|
||||||
|
value: ""
|
||||||
|
},
|
||||||
|
prevState.price.values,
|
||||||
|
(a, b) => a.slug === b.slug
|
||||||
|
)
|
||||||
|
: prevState.price.values;
|
||||||
|
|
||||||
|
const stockValues =
|
||||||
|
prevState.stock.attribute === attributeId
|
||||||
|
? toggle(
|
||||||
|
{
|
||||||
|
slug: valueSlug,
|
||||||
|
value: ""
|
||||||
|
},
|
||||||
|
prevState.stock.values,
|
||||||
|
(a, b) => a.slug === b.slug
|
||||||
|
)
|
||||||
|
: prevState.stock.values;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...prevState,
|
||||||
|
attributes: updatedAttributes,
|
||||||
|
price: {
|
||||||
|
...prevState.price,
|
||||||
|
values: priceValues
|
||||||
|
},
|
||||||
|
stock: {
|
||||||
|
...prevState.stock,
|
||||||
|
values: stockValues
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyPriceToAll(
|
||||||
|
state: ProductVariantCreateFormData,
|
||||||
|
value: boolean
|
||||||
|
): ProductVariantCreateFormData {
|
||||||
|
const data = {
|
||||||
|
...state,
|
||||||
|
price: {
|
||||||
|
...state.price,
|
||||||
|
all: value
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
...data,
|
||||||
|
variants: createVariants(data)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyStockToAll(
|
||||||
|
state: ProductVariantCreateFormData,
|
||||||
|
value: boolean
|
||||||
|
): ProductVariantCreateFormData {
|
||||||
|
const data = {
|
||||||
|
...state,
|
||||||
|
stock: {
|
||||||
|
...state.stock,
|
||||||
|
all: value
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
...data,
|
||||||
|
variants: createVariants(data)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeAttributeValuePrice(
|
||||||
|
state: ProductVariantCreateFormData,
|
||||||
|
attributeValueSlug: string,
|
||||||
|
price: string
|
||||||
|
): ProductVariantCreateFormData {
|
||||||
|
const index = state.price.values.findIndex(
|
||||||
|
value => value.slug === attributeValueSlug
|
||||||
|
);
|
||||||
|
|
||||||
|
if (index === -1) {
|
||||||
|
throw new Error(`Value with id ${attributeValueSlug} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const values = updateAtIndex(
|
||||||
|
{
|
||||||
|
slug: attributeValueSlug,
|
||||||
|
value: price
|
||||||
|
},
|
||||||
|
state.price.values,
|
||||||
|
index
|
||||||
|
);
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
...state,
|
||||||
|
price: {
|
||||||
|
...state.price,
|
||||||
|
values
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
...data,
|
||||||
|
variants: createVariants(data)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeAttributeValueStock(
|
||||||
|
state: ProductVariantCreateFormData,
|
||||||
|
attributeValueSlug: string,
|
||||||
|
stock: string
|
||||||
|
): ProductVariantCreateFormData {
|
||||||
|
const index = state.stock.values.findIndex(
|
||||||
|
value => value.slug === attributeValueSlug
|
||||||
|
);
|
||||||
|
|
||||||
|
if (index === -1) {
|
||||||
|
throw new Error(`Value with id ${attributeValueSlug} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const values = updateAtIndex(
|
||||||
|
{
|
||||||
|
slug: attributeValueSlug,
|
||||||
|
value: stock
|
||||||
|
},
|
||||||
|
state.stock.values,
|
||||||
|
index
|
||||||
|
);
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
...state,
|
||||||
|
stock: {
|
||||||
|
...state.stock,
|
||||||
|
values
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
...data,
|
||||||
|
variants: createVariants(data)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeApplyPriceToAttributeId(
|
||||||
|
state: ProductVariantCreateFormData,
|
||||||
|
attributeId: string
|
||||||
|
): ProductVariantCreateFormData {
|
||||||
|
const attribute = state.attributes.find(
|
||||||
|
attribute => attribute.id === attributeId
|
||||||
|
);
|
||||||
|
const values = attribute.values.map(slug => ({
|
||||||
|
slug,
|
||||||
|
value: ""
|
||||||
|
}));
|
||||||
|
const data = {
|
||||||
|
...state,
|
||||||
|
price: {
|
||||||
|
...state.price,
|
||||||
|
attribute: attributeId,
|
||||||
|
values
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
...data,
|
||||||
|
variants: createVariants(data)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeApplyStockToAttributeId(
|
||||||
|
state: ProductVariantCreateFormData,
|
||||||
|
attributeId: string
|
||||||
|
): ProductVariantCreateFormData {
|
||||||
|
const attribute = state.attributes.find(
|
||||||
|
attribute => attribute.id === attributeId
|
||||||
|
);
|
||||||
|
const values = attribute.values.map(slug => ({
|
||||||
|
slug,
|
||||||
|
value: ""
|
||||||
|
}));
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
...state,
|
||||||
|
stock: {
|
||||||
|
...state.stock,
|
||||||
|
attribute: attributeId,
|
||||||
|
values
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
...data,
|
||||||
|
variants: createVariants(data)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeApplyPriceToAllValue(
|
||||||
|
state: ProductVariantCreateFormData,
|
||||||
|
value: string
|
||||||
|
): ProductVariantCreateFormData {
|
||||||
|
const data = {
|
||||||
|
...state,
|
||||||
|
price: {
|
||||||
|
...state.price,
|
||||||
|
value
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
...data,
|
||||||
|
variants: createVariants(data)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeApplyStockToAllValue(
|
||||||
|
state: ProductVariantCreateFormData,
|
||||||
|
value: string
|
||||||
|
): ProductVariantCreateFormData {
|
||||||
|
const data = {
|
||||||
|
...state,
|
||||||
|
stock: {
|
||||||
|
...state.stock,
|
||||||
|
value
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
...data,
|
||||||
|
variants: createVariants(data)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeVariantData(
|
||||||
|
state: ProductVariantCreateFormData,
|
||||||
|
field: VariantField,
|
||||||
|
value: string,
|
||||||
|
variantIndex: number
|
||||||
|
): ProductVariantCreateFormData {
|
||||||
|
const variant = state.variants[variantIndex];
|
||||||
|
if (field === "price") {
|
||||||
|
variant.priceOverride = value;
|
||||||
|
} else if (field === "sku") {
|
||||||
|
variant.sku = value;
|
||||||
|
} else {
|
||||||
|
variant.quantity = parseInt(value, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
variants: updateAtIndex(variant, state.variants, variantIndex)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteVariant(
|
||||||
|
state: ProductVariantCreateFormData,
|
||||||
|
variantIndex: number
|
||||||
|
): ProductVariantCreateFormData {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
variants: removeAtIndex(state.variants, variantIndex)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function reduceProductVariantCreateFormData(
|
||||||
|
prevState: ProductVariantCreateFormData,
|
||||||
|
action: ProductVariantCreateReducerAction
|
||||||
|
) {
|
||||||
|
switch (action.type) {
|
||||||
|
case "selectValue":
|
||||||
|
return selectValue(prevState, action.attributeId, action.valueId);
|
||||||
|
|
||||||
|
case "applyPriceToAll":
|
||||||
|
return applyPriceToAll(prevState, action.all);
|
||||||
|
case "applyStockToAll":
|
||||||
|
return applyStockToAll(prevState, action.all);
|
||||||
|
case "changeAttributeValuePrice":
|
||||||
|
return changeAttributeValuePrice(prevState, action.valueId, action.value);
|
||||||
|
case "changeAttributeValueStock":
|
||||||
|
return changeAttributeValueStock(prevState, action.valueId, action.value);
|
||||||
|
case "changeApplyPriceToAttributeId":
|
||||||
|
return changeApplyPriceToAttributeId(prevState, action.attributeId);
|
||||||
|
case "changeApplyStockToAttributeId":
|
||||||
|
return changeApplyStockToAttributeId(prevState, action.attributeId);
|
||||||
|
case "changeApplyPriceToAllValue":
|
||||||
|
return changeApplyPriceToAllValue(prevState, action.value);
|
||||||
|
case "changeApplyStockToAllValue":
|
||||||
|
return changeApplyStockToAllValue(prevState, action.value);
|
||||||
|
case "changeVariantData":
|
||||||
|
return changeVariantData(
|
||||||
|
prevState,
|
||||||
|
action.field,
|
||||||
|
action.value,
|
||||||
|
action.variantIndex
|
||||||
|
);
|
||||||
|
case "deleteVariant":
|
||||||
|
return deleteVariant(prevState, action.variantIndex);
|
||||||
|
case "reload":
|
||||||
|
return action.data;
|
||||||
|
default:
|
||||||
|
return prevState;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default reduceProductVariantCreateFormData;
|
|
@ -0,0 +1 @@
|
||||||
|
export type ProductVariantCreateStep = "values" | "prices" | "summary";
|
|
@ -69,6 +69,7 @@ interface ProductVariantsProps extends ListActions, WithStyles<typeof styles> {
|
||||||
fallbackPrice?: ProductVariant_costPrice;
|
fallbackPrice?: ProductVariant_costPrice;
|
||||||
onRowClick: (id: string) => () => void;
|
onRowClick: (id: string) => () => void;
|
||||||
onVariantAdd?();
|
onVariantAdd?();
|
||||||
|
onVariantsAdd?();
|
||||||
}
|
}
|
||||||
|
|
||||||
const numberOfColumns = 5;
|
const numberOfColumns = 5;
|
||||||
|
@ -81,6 +82,7 @@ export const ProductVariants = withStyles(styles, { name: "ProductVariants" })(
|
||||||
fallbackPrice,
|
fallbackPrice,
|
||||||
onRowClick,
|
onRowClick,
|
||||||
onVariantAdd,
|
onVariantAdd,
|
||||||
|
onVariantsAdd,
|
||||||
isChecked,
|
isChecked,
|
||||||
selected,
|
selected,
|
||||||
toggle,
|
toggle,
|
||||||
|
@ -98,7 +100,7 @@ export const ProductVariants = withStyles(styles, { name: "ProductVariants" })(
|
||||||
description: "section header"
|
description: "section header"
|
||||||
})}
|
})}
|
||||||
toolbar={
|
toolbar={
|
||||||
<>
|
hasVariants ? (
|
||||||
<Button
|
<Button
|
||||||
onClick={onVariantAdd}
|
onClick={onVariantAdd}
|
||||||
variant="text"
|
variant="text"
|
||||||
|
@ -110,7 +112,19 @@ export const ProductVariants = withStyles(styles, { name: "ProductVariants" })(
|
||||||
description="button"
|
description="button"
|
||||||
/>
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
) : (
|
||||||
|
<Button
|
||||||
|
onClick={onVariantsAdd}
|
||||||
|
variant="text"
|
||||||
|
color="primary"
|
||||||
|
data-tc="button-add-variants"
|
||||||
|
>
|
||||||
|
<FormattedMessage
|
||||||
|
defaultMessage="Create variants"
|
||||||
|
description="button"
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
{!variants.length && (
|
{!variants.length && (
|
||||||
|
|
|
@ -7,6 +7,7 @@ import {
|
||||||
TypedProductImageCreateMutation,
|
TypedProductImageCreateMutation,
|
||||||
TypedProductImageDeleteMutation,
|
TypedProductImageDeleteMutation,
|
||||||
TypedProductUpdateMutation,
|
TypedProductUpdateMutation,
|
||||||
|
TypedProductVariantBulkCreateMutation,
|
||||||
TypedProductVariantBulkDeleteMutation,
|
TypedProductVariantBulkDeleteMutation,
|
||||||
TypedSimpleProductUpdateMutation
|
TypedSimpleProductUpdateMutation
|
||||||
} from "../mutations";
|
} from "../mutations";
|
||||||
|
@ -25,6 +26,10 @@ import {
|
||||||
ProductImageReorderVariables
|
ProductImageReorderVariables
|
||||||
} from "../types/ProductImageReorder";
|
} from "../types/ProductImageReorder";
|
||||||
import { ProductUpdate, ProductUpdateVariables } from "../types/ProductUpdate";
|
import { ProductUpdate, ProductUpdateVariables } from "../types/ProductUpdate";
|
||||||
|
import {
|
||||||
|
ProductVariantBulkCreate,
|
||||||
|
ProductVariantBulkCreateVariables
|
||||||
|
} from "../types/ProductVariantBulkCreate";
|
||||||
import {
|
import {
|
||||||
ProductVariantBulkDelete,
|
ProductVariantBulkDelete,
|
||||||
ProductVariantBulkDeleteVariables
|
ProductVariantBulkDeleteVariables
|
||||||
|
@ -38,6 +43,10 @@ import ProductImagesReorderProvider from "./ProductImagesReorder";
|
||||||
interface ProductUpdateOperationsProps {
|
interface ProductUpdateOperationsProps {
|
||||||
product: ProductDetails_product;
|
product: ProductDetails_product;
|
||||||
children: (props: {
|
children: (props: {
|
||||||
|
bulkProductVariantCreate: PartialMutationProviderOutput<
|
||||||
|
ProductVariantBulkCreate,
|
||||||
|
ProductVariantBulkCreateVariables
|
||||||
|
>;
|
||||||
bulkProductVariantDelete: PartialMutationProviderOutput<
|
bulkProductVariantDelete: PartialMutationProviderOutput<
|
||||||
ProductVariantBulkDelete,
|
ProductVariantBulkDelete,
|
||||||
ProductVariantBulkDeleteVariables
|
ProductVariantBulkDeleteVariables
|
||||||
|
@ -67,6 +76,7 @@ interface ProductUpdateOperationsProps {
|
||||||
SimpleProductUpdateVariables
|
SimpleProductUpdateVariables
|
||||||
>;
|
>;
|
||||||
}) => React.ReactNode;
|
}) => React.ReactNode;
|
||||||
|
onBulkProductVariantCreate?: (data: ProductVariantBulkCreate) => void;
|
||||||
onBulkProductVariantDelete?: (data: ProductVariantBulkDelete) => void;
|
onBulkProductVariantDelete?: (data: ProductVariantBulkDelete) => void;
|
||||||
onDelete?: (data: ProductDelete) => void;
|
onDelete?: (data: ProductDelete) => void;
|
||||||
onImageCreate?: (data: ProductImageCreate) => void;
|
onImageCreate?: (data: ProductImageCreate) => void;
|
||||||
|
@ -80,6 +90,7 @@ const ProductUpdateOperations: React.StatelessComponent<
|
||||||
> = ({
|
> = ({
|
||||||
product,
|
product,
|
||||||
children,
|
children,
|
||||||
|
onBulkProductVariantCreate,
|
||||||
onBulkProductVariantDelete,
|
onBulkProductVariantDelete,
|
||||||
onDelete,
|
onDelete,
|
||||||
onImageDelete,
|
onImageDelete,
|
||||||
|
@ -112,31 +123,40 @@ const ProductUpdateOperations: React.StatelessComponent<
|
||||||
<TypedProductVariantBulkDeleteMutation
|
<TypedProductVariantBulkDeleteMutation
|
||||||
onCompleted={onBulkProductVariantDelete}
|
onCompleted={onBulkProductVariantDelete}
|
||||||
>
|
>
|
||||||
{(...bulkProductVariantDelete) =>
|
{(...bulkProductVariantDelete) => (
|
||||||
children({
|
<TypedProductVariantBulkCreateMutation
|
||||||
bulkProductVariantDelete: getMutationProviderData(
|
onCompleted={onBulkProductVariantCreate}
|
||||||
...bulkProductVariantDelete
|
>
|
||||||
),
|
{(...bulkProductVariantCreate) =>
|
||||||
createProductImage: getMutationProviderData(
|
children({
|
||||||
...createProductImage
|
bulkProductVariantCreate: getMutationProviderData(
|
||||||
),
|
...bulkProductVariantCreate
|
||||||
deleteProduct: getMutationProviderData(
|
),
|
||||||
...deleteProduct
|
bulkProductVariantDelete: getMutationProviderData(
|
||||||
),
|
...bulkProductVariantDelete
|
||||||
deleteProductImage: getMutationProviderData(
|
),
|
||||||
...deleteProductImage
|
createProductImage: getMutationProviderData(
|
||||||
),
|
...createProductImage
|
||||||
reorderProductImages: getMutationProviderData(
|
),
|
||||||
...reorderProductImages
|
deleteProduct: getMutationProviderData(
|
||||||
),
|
...deleteProduct
|
||||||
updateProduct: getMutationProviderData(
|
),
|
||||||
...updateProduct
|
deleteProductImage: getMutationProviderData(
|
||||||
),
|
...deleteProductImage
|
||||||
updateSimpleProduct: getMutationProviderData(
|
),
|
||||||
...updateSimpleProduct
|
reorderProductImages: getMutationProviderData(
|
||||||
)
|
...reorderProductImages
|
||||||
})
|
),
|
||||||
}
|
updateProduct: getMutationProviderData(
|
||||||
|
...updateProduct
|
||||||
|
),
|
||||||
|
updateSimpleProduct: getMutationProviderData(
|
||||||
|
...updateSimpleProduct
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</TypedProductVariantBulkCreateMutation>
|
||||||
|
)}
|
||||||
</TypedProductVariantBulkDeleteMutation>
|
</TypedProductVariantBulkDeleteMutation>
|
||||||
)}
|
)}
|
||||||
</TypedSimpleProductUpdateMutation>
|
</TypedSimpleProductUpdateMutation>
|
||||||
|
|
|
@ -45,6 +45,10 @@ import {
|
||||||
productBulkPublish,
|
productBulkPublish,
|
||||||
productBulkPublishVariables
|
productBulkPublishVariables
|
||||||
} from "./types/productBulkPublish";
|
} from "./types/productBulkPublish";
|
||||||
|
import {
|
||||||
|
ProductVariantBulkCreate,
|
||||||
|
ProductVariantBulkCreateVariables
|
||||||
|
} from "./types/ProductVariantBulkCreate";
|
||||||
import {
|
import {
|
||||||
ProductVariantBulkDelete,
|
ProductVariantBulkDelete,
|
||||||
ProductVariantBulkDeleteVariables
|
ProductVariantBulkDeleteVariables
|
||||||
|
@ -319,26 +323,8 @@ export const TypedVariantUpdateMutation = TypedMutation<
|
||||||
|
|
||||||
export const variantCreateMutation = gql`
|
export const variantCreateMutation = gql`
|
||||||
${fragmentVariant}
|
${fragmentVariant}
|
||||||
mutation VariantCreate(
|
mutation VariantCreate($input: ProductVariantCreateInput!) {
|
||||||
$attributes: [AttributeValueInput]!
|
productVariantCreate(input: $input) {
|
||||||
$costPrice: Decimal
|
|
||||||
$priceOverride: Decimal
|
|
||||||
$product: ID!
|
|
||||||
$sku: String
|
|
||||||
$quantity: Int
|
|
||||||
$trackInventory: Boolean!
|
|
||||||
) {
|
|
||||||
productVariantCreate(
|
|
||||||
input: {
|
|
||||||
attributes: $attributes
|
|
||||||
costPrice: $costPrice
|
|
||||||
priceOverride: $priceOverride
|
|
||||||
product: $product
|
|
||||||
sku: $sku
|
|
||||||
quantity: $quantity
|
|
||||||
trackInventory: $trackInventory
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
errors {
|
errors {
|
||||||
field
|
field
|
||||||
message
|
message
|
||||||
|
@ -458,6 +444,30 @@ export const TypedProductBulkPublishMutation = TypedMutation<
|
||||||
productBulkPublishVariables
|
productBulkPublishVariables
|
||||||
>(productBulkPublishMutation);
|
>(productBulkPublishMutation);
|
||||||
|
|
||||||
|
export const ProductVariantBulkCreateMutation = gql`
|
||||||
|
mutation ProductVariantBulkCreate(
|
||||||
|
$id: ID!
|
||||||
|
$inputs: [ProductVariantBulkCreateInput]!
|
||||||
|
) {
|
||||||
|
productVariantBulkCreate(product: $id, variants: $inputs) {
|
||||||
|
bulkProductErrors {
|
||||||
|
field
|
||||||
|
message
|
||||||
|
code
|
||||||
|
index
|
||||||
|
}
|
||||||
|
errors {
|
||||||
|
field
|
||||||
|
message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
export const TypedProductVariantBulkCreateMutation = TypedMutation<
|
||||||
|
ProductVariantBulkCreate,
|
||||||
|
ProductVariantBulkCreateVariables
|
||||||
|
>(ProductVariantBulkCreateMutation);
|
||||||
|
|
||||||
export const ProductVariantBulkDeleteMutation = gql`
|
export const ProductVariantBulkDeleteMutation = gql`
|
||||||
mutation ProductVariantBulkDelete($ids: [ID!]!) {
|
mutation ProductVariantBulkDelete($ids: [ID!]!) {
|
||||||
productVariantBulkDelete(ids: $ids) {
|
productVariantBulkDelete(ids: $ids) {
|
||||||
|
|
|
@ -260,6 +260,17 @@ const productDetailsQuery = gql`
|
||||||
query ProductDetails($id: ID!) {
|
query ProductDetails($id: ID!) {
|
||||||
product(id: $id) {
|
product(id: $id) {
|
||||||
...Product
|
...Product
|
||||||
|
productType {
|
||||||
|
variantAttributes {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
values {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
slug
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
|
@ -139,11 +139,26 @@ export interface ProductDetails_product_variants {
|
||||||
stockQuantity: number;
|
stockQuantity: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ProductDetails_product_productType_variantAttributes_values {
|
||||||
|
__typename: "AttributeValue";
|
||||||
|
id: string;
|
||||||
|
name: string | null;
|
||||||
|
slug: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProductDetails_product_productType_variantAttributes {
|
||||||
|
__typename: "Attribute";
|
||||||
|
id: string;
|
||||||
|
name: string | null;
|
||||||
|
values: (ProductDetails_product_productType_variantAttributes_values | null)[] | null;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ProductDetails_product_productType {
|
export interface ProductDetails_product_productType {
|
||||||
__typename: "ProductType";
|
__typename: "ProductType";
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
hasVariants: boolean;
|
hasVariants: boolean;
|
||||||
|
variantAttributes: (ProductDetails_product_productType_variantAttributes | null)[] | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ProductDetails_product {
|
export interface ProductDetails_product {
|
||||||
|
|
38
src/products/types/ProductVariantBulkCreate.ts
Normal file
38
src/products/types/ProductVariantBulkCreate.ts
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
// This file was automatically generated and should not be edited.
|
||||||
|
|
||||||
|
import { ProductVariantBulkCreateInput, ProductErrorCode } from "./../../types/globalTypes";
|
||||||
|
|
||||||
|
// ====================================================
|
||||||
|
// GraphQL mutation operation: ProductVariantBulkCreate
|
||||||
|
// ====================================================
|
||||||
|
|
||||||
|
export interface ProductVariantBulkCreate_productVariantBulkCreate_bulkProductErrors {
|
||||||
|
__typename: "BulkProductError";
|
||||||
|
field: string | null;
|
||||||
|
message: string | null;
|
||||||
|
code: ProductErrorCode | null;
|
||||||
|
index: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProductVariantBulkCreate_productVariantBulkCreate_errors {
|
||||||
|
__typename: "Error";
|
||||||
|
field: string | null;
|
||||||
|
message: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProductVariantBulkCreate_productVariantBulkCreate {
|
||||||
|
__typename: "ProductVariantBulkCreate";
|
||||||
|
bulkProductErrors: ProductVariantBulkCreate_productVariantBulkCreate_bulkProductErrors[] | null;
|
||||||
|
errors: ProductVariantBulkCreate_productVariantBulkCreate_errors[] | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProductVariantBulkCreate {
|
||||||
|
productVariantBulkCreate: ProductVariantBulkCreate_productVariantBulkCreate | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProductVariantBulkCreateVariables {
|
||||||
|
id: string;
|
||||||
|
inputs: (ProductVariantBulkCreateInput | null)[];
|
||||||
|
}
|
|
@ -2,7 +2,7 @@
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
// This file was automatically generated and should not be edited.
|
// This file was automatically generated and should not be edited.
|
||||||
|
|
||||||
import { AttributeValueInput } from "./../../types/globalTypes";
|
import { ProductVariantCreateInput } from "./../../types/globalTypes";
|
||||||
|
|
||||||
// ====================================================
|
// ====================================================
|
||||||
// GraphQL mutation operation: VariantCreate
|
// GraphQL mutation operation: VariantCreate
|
||||||
|
@ -122,11 +122,5 @@ export interface VariantCreate {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface VariantCreateVariables {
|
export interface VariantCreateVariables {
|
||||||
attributes: (AttributeValueInput | null)[];
|
input: ProductVariantCreateInput;
|
||||||
costPrice?: any | null;
|
|
||||||
priceOverride?: any | null;
|
|
||||||
product: string;
|
|
||||||
sku?: string | null;
|
|
||||||
quantity?: number | null;
|
|
||||||
trackInventory: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -53,7 +53,7 @@ export const productListUrl = (params?: ProductListUrlQueryParams): string =>
|
||||||
export const productPath = (id: string) => urlJoin(productSection + id);
|
export const productPath = (id: string) => urlJoin(productSection + id);
|
||||||
export type ProductUrlDialog = "remove";
|
export type ProductUrlDialog = "remove";
|
||||||
export type ProductUrlQueryParams = BulkAction &
|
export type ProductUrlQueryParams = BulkAction &
|
||||||
Dialog<"remove" | "remove-variants">;
|
Dialog<"create-variants" | "remove" | "remove-variants">;
|
||||||
export const productUrl = (id: string, params?: ProductUrlQueryParams) =>
|
export const productUrl = (id: string, params?: ProductUrlQueryParams) =>
|
||||||
productPath(encodeURIComponent(id)) + "?" + stringifyQs(params);
|
productPath(encodeURIComponent(id)) + "?" + stringifyQs(params);
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,10 @@ import { WindowTitle } from "@saleor/components/WindowTitle";
|
||||||
import useBulkActions from "@saleor/hooks/useBulkActions";
|
import useBulkActions from "@saleor/hooks/useBulkActions";
|
||||||
import useNavigator from "@saleor/hooks/useNavigator";
|
import useNavigator from "@saleor/hooks/useNavigator";
|
||||||
import useNotifier from "@saleor/hooks/useNotifier";
|
import useNotifier from "@saleor/hooks/useNotifier";
|
||||||
|
import useShop from "@saleor/hooks/useShop";
|
||||||
import { commonMessages } from "@saleor/intl";
|
import { commonMessages } from "@saleor/intl";
|
||||||
|
import ProductVariantCreateDialog from "@saleor/products/components/ProductVariantCreateDialog/ProductVariantCreateDialog";
|
||||||
|
import { ProductVariantBulkCreate } from "@saleor/products/types/ProductVariantBulkCreate";
|
||||||
import { DEFAULT_INITIAL_SEARCH_DATA } from "../../../config";
|
import { DEFAULT_INITIAL_SEARCH_DATA } from "../../../config";
|
||||||
import SearchCategories from "../../../containers/SearchCategories";
|
import SearchCategories from "../../../containers/SearchCategories";
|
||||||
import SearchCollections from "../../../containers/SearchCollections";
|
import SearchCollections from "../../../containers/SearchCollections";
|
||||||
|
@ -54,6 +57,7 @@ export const ProductUpdate: React.StatelessComponent<ProductUpdateProps> = ({
|
||||||
params.ids
|
params.ids
|
||||||
);
|
);
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
const shop = useShop();
|
||||||
|
|
||||||
const openModal = (action: ProductUrlDialog) =>
|
const openModal = (action: ProductUrlDialog) =>
|
||||||
navigate(
|
navigate(
|
||||||
|
@ -115,6 +119,15 @@ export const ProductUpdate: React.StatelessComponent<ProductUpdateProps> = ({
|
||||||
const handleVariantAdd = () =>
|
const handleVariantAdd = () =>
|
||||||
navigate(productVariantAddUrl(id));
|
navigate(productVariantAddUrl(id));
|
||||||
|
|
||||||
|
const handleBulkProductVariantCreate = (
|
||||||
|
data: ProductVariantBulkCreate
|
||||||
|
) => {
|
||||||
|
if (data.productVariantBulkCreate.errors.length === 0) {
|
||||||
|
navigate(productUrl(id), true);
|
||||||
|
refetch();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleBulkProductVariantDelete = (
|
const handleBulkProductVariantDelete = (
|
||||||
data: ProductVariantBulkDelete
|
data: ProductVariantBulkDelete
|
||||||
) => {
|
) => {
|
||||||
|
@ -125,10 +138,19 @@ export const ProductUpdate: React.StatelessComponent<ProductUpdateProps> = ({
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleVariantCreatorOpen = () =>
|
||||||
|
navigate(
|
||||||
|
productUrl(id, {
|
||||||
|
...params,
|
||||||
|
action: "create-variants"
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
const product = data ? data.product : undefined;
|
const product = data ? data.product : undefined;
|
||||||
return (
|
return (
|
||||||
<ProductUpdateOperations
|
<ProductUpdateOperations
|
||||||
product={product}
|
product={product}
|
||||||
|
onBulkProductVariantCreate={handleBulkProductVariantCreate}
|
||||||
onBulkProductVariantDelete={handleBulkProductVariantDelete}
|
onBulkProductVariantDelete={handleBulkProductVariantDelete}
|
||||||
onDelete={handleDelete}
|
onDelete={handleDelete}
|
||||||
onImageCreate={handleImageCreate}
|
onImageCreate={handleImageCreate}
|
||||||
|
@ -136,6 +158,7 @@ export const ProductUpdate: React.StatelessComponent<ProductUpdateProps> = ({
|
||||||
onUpdate={handleUpdate}
|
onUpdate={handleUpdate}
|
||||||
>
|
>
|
||||||
{({
|
{({
|
||||||
|
bulkProductVariantCreate,
|
||||||
bulkProductVariantDelete,
|
bulkProductVariantDelete,
|
||||||
createProductImage,
|
createProductImage,
|
||||||
deleteProduct,
|
deleteProduct,
|
||||||
|
@ -245,6 +268,7 @@ export const ProductUpdate: React.StatelessComponent<ProductUpdateProps> = ({
|
||||||
onImageReorder={handleImageReorder}
|
onImageReorder={handleImageReorder}
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
onVariantAdd={handleVariantAdd}
|
onVariantAdd={handleVariantAdd}
|
||||||
|
onVariantsAdd={handleVariantCreatorOpen}
|
||||||
onVariantShow={variantId => () =>
|
onVariantShow={variantId => () =>
|
||||||
navigate(
|
navigate(
|
||||||
productVariantEditUrl(product.id, variantId)
|
productVariantEditUrl(product.id, variantId)
|
||||||
|
@ -328,6 +352,37 @@ export const ProductUpdate: React.StatelessComponent<ProductUpdateProps> = ({
|
||||||
/>
|
/>
|
||||||
</DialogContentText>
|
</DialogContentText>
|
||||||
</ActionDialog>
|
</ActionDialog>
|
||||||
|
<ProductVariantCreateDialog
|
||||||
|
defaultPrice={maybe(() =>
|
||||||
|
data.product.basePrice.amount.toFixed(2)
|
||||||
|
)}
|
||||||
|
errors={maybe(
|
||||||
|
() =>
|
||||||
|
bulkProductVariantCreate.opts.data
|
||||||
|
.productVariantBulkCreate.bulkProductErrors,
|
||||||
|
[]
|
||||||
|
)}
|
||||||
|
open={params.action === "create-variants"}
|
||||||
|
attributes={maybe(
|
||||||
|
() => data.product.productType.variantAttributes,
|
||||||
|
[]
|
||||||
|
)}
|
||||||
|
currencySymbol={maybe(() => shop.defaultCurrency)}
|
||||||
|
onClose={() =>
|
||||||
|
navigate(
|
||||||
|
productUrl(id, {
|
||||||
|
...params,
|
||||||
|
action: undefined
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
onSubmit={inputs =>
|
||||||
|
bulkProductVariantCreate.mutate({
|
||||||
|
id,
|
||||||
|
inputs
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -58,18 +58,20 @@ export const ProductVariant: React.StatelessComponent<ProductUpdateProps> = ({
|
||||||
) =>
|
) =>
|
||||||
variantCreate({
|
variantCreate({
|
||||||
variables: {
|
variables: {
|
||||||
attributes: formData.attributes
|
input: {
|
||||||
.filter(attribute => attribute.value !== "")
|
attributes: formData.attributes
|
||||||
.map(attribute => ({
|
.filter(attribute => attribute.value !== "")
|
||||||
id: attribute.id,
|
.map(attribute => ({
|
||||||
values: [attribute.value]
|
id: attribute.id,
|
||||||
})),
|
values: [attribute.value]
|
||||||
costPrice: decimal(formData.costPrice),
|
})),
|
||||||
priceOverride: decimal(formData.priceOverride),
|
costPrice: decimal(formData.costPrice),
|
||||||
product: productId,
|
priceOverride: decimal(formData.priceOverride),
|
||||||
quantity: formData.quantity || null,
|
product: productId,
|
||||||
sku: formData.sku,
|
quantity: formData.quantity || null,
|
||||||
trackInventory: true
|
sku: formData.sku,
|
||||||
|
trackInventory: true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const handleVariantClick = (id: string) =>
|
const handleVariantClick = (id: string) =>
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -30,6 +30,7 @@ const props: ProductUpdatePageProps = {
|
||||||
onSubmit: () => undefined,
|
onSubmit: () => undefined,
|
||||||
onVariantAdd: () => undefined,
|
onVariantAdd: () => undefined,
|
||||||
onVariantShow: () => undefined,
|
onVariantShow: () => undefined,
|
||||||
|
onVariantsAdd: () => undefined,
|
||||||
placeholderImage,
|
placeholderImage,
|
||||||
product,
|
product,
|
||||||
saveButtonBarState: "default",
|
saveButtonBarState: "default",
|
||||||
|
|
|
@ -60,7 +60,7 @@ export default (colors: IThemeColors): Theme =>
|
||||||
},
|
},
|
||||||
flat: {
|
flat: {
|
||||||
"& span": {
|
"& span": {
|
||||||
color: colors.primary
|
color: colors.font.gray
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
flatPrimary: {
|
flatPrimary: {
|
||||||
|
@ -281,8 +281,7 @@ export default (colors: IThemeColors): Theme =>
|
||||||
"& fieldset": {
|
"& fieldset": {
|
||||||
"&&:not($error)": {
|
"&&:not($error)": {
|
||||||
borderColor: colors.input.border
|
borderColor: colors.input.border
|
||||||
},
|
}
|
||||||
background: colors.background.paper
|
|
||||||
},
|
},
|
||||||
"& legend": {
|
"& legend": {
|
||||||
display: "none"
|
display: "none"
|
||||||
|
|
|
@ -193,6 +193,20 @@ export enum PermissionEnum {
|
||||||
MANAGE_WEBHOOKS = "MANAGE_WEBHOOKS",
|
MANAGE_WEBHOOKS = "MANAGE_WEBHOOKS",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum ProductErrorCode {
|
||||||
|
ALREADY_EXISTS = "ALREADY_EXISTS",
|
||||||
|
ATTRIBUTE_ALREADY_ASSIGNED = "ATTRIBUTE_ALREADY_ASSIGNED",
|
||||||
|
ATTRIBUTE_CANNOT_BE_ASSIGNED = "ATTRIBUTE_CANNOT_BE_ASSIGNED",
|
||||||
|
ATTRIBUTE_VARIANTS_DISABLED = "ATTRIBUTE_VARIANTS_DISABLED",
|
||||||
|
GRAPHQL_ERROR = "GRAPHQL_ERROR",
|
||||||
|
INVALID = "INVALID",
|
||||||
|
NOT_FOUND = "NOT_FOUND",
|
||||||
|
NOT_PRODUCTS_IMAGE = "NOT_PRODUCTS_IMAGE",
|
||||||
|
REQUIRED = "REQUIRED",
|
||||||
|
UNIQUE = "UNIQUE",
|
||||||
|
VARIANT_NO_DIGITAL_CONTENT = "VARIANT_NO_DIGITAL_CONTENT",
|
||||||
|
}
|
||||||
|
|
||||||
export enum ProductOrderField {
|
export enum ProductOrderField {
|
||||||
DATE = "DATE",
|
DATE = "DATE",
|
||||||
MINIMAL_PRICE = "MINIMAL_PRICE",
|
MINIMAL_PRICE = "MINIMAL_PRICE",
|
||||||
|
@ -614,6 +628,27 @@ export interface ProductTypeInput {
|
||||||
taxCode?: string | null;
|
taxCode?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ProductVariantBulkCreateInput {
|
||||||
|
attributes: (AttributeValueInput | null)[];
|
||||||
|
costPrice?: any | null;
|
||||||
|
priceOverride?: any | null;
|
||||||
|
sku: string;
|
||||||
|
quantity?: number | null;
|
||||||
|
trackInventory?: boolean | null;
|
||||||
|
weight?: any | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProductVariantCreateInput {
|
||||||
|
attributes: (AttributeValueInput | null)[];
|
||||||
|
costPrice?: any | null;
|
||||||
|
priceOverride?: any | null;
|
||||||
|
sku?: string | null;
|
||||||
|
quantity?: number | null;
|
||||||
|
trackInventory?: boolean | null;
|
||||||
|
weight?: any | null;
|
||||||
|
product: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ProductVariantInput {
|
export interface ProductVariantInput {
|
||||||
attributes?: (AttributeValueInput | null)[] | null;
|
attributes?: (AttributeValueInput | null)[] | null;
|
||||||
costPrice?: any | null;
|
costPrice?: any | null;
|
||||||
|
|
Loading…
Reference in a new issue