Remove bulk variant creator (#2179)

* Remove bulk vaariant creator

* Update variant test snapshots

* Update variant E2E tests

* fixed test for products variants (#2187)

Co-authored-by: Karolina Rakoczy <rakoczy.karolina@gmail.com>
This commit is contained in:
Dawid 2022-08-10 11:29:17 +01:00 committed by GitHub
parent 6fdec6e5f1
commit eea1ba235f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
48 changed files with 419 additions and 16821 deletions

View file

@ -13,7 +13,6 @@ import {
import * as productUtils from "../../support/api/utils/products/productsUtils";
import { getProductVariants } from "../../support/api/utils/storeFront/storeFrontProductUtils";
import {
createFirstVariant,
createVariant,
variantsShouldBeVisible,
} from "../../support/pages/catalog/products/VariantsPage";
@ -81,10 +80,11 @@ describe("As an admin I should be able to create variant", () => {
channelId: newChannel.id,
});
cy.visit(`${urlList.products}${createdProduct.id}`);
createFirstVariant({
createVariant({
channelName: [defaultChannel.name, newChannel.name],
sku: name,
price,
attribute: attributeValues[0],
attributeName: attributeValues[0],
});
selectChannelInHeader(defaultChannel.name);
variantsShouldBeVisible({ name, price });

View file

@ -120,9 +120,9 @@ describe("Creating variants", () => {
});
cy.visit(`${urlList.products}${createdProduct.id}`);
cy.waitForProgressBarToNotBeVisible();
createFirstVariant({
createVariant({
price,
attribute: attributeValues[0],
attributeName: attributeValues[0],
});
getProductVariants(createdProduct.id, defaultChannel.slug);
})

View file

@ -8,11 +8,7 @@ export const PRODUCT_DETAILS = {
firstCategoryItem: "#downshift-0-item-0",
visibleRadioBtn: "[name='isPublished']",
channelAvailabilityItem: "[data-test-id*='channel-availability-item']",
addVariantsButton: "[data-test-id*='button-add-variant']",
addVariantsOptionDialog: {
optionMultiple: '[data-test-id="variant-create-option-multiple"]',
optionSingle: '[data-test-id="variant-create-option-single"]'
},
addVariantButton: "[data-test-id*='button-add-variant']",
descriptionInput: "[data-test-id='rich-text-editor-description']",
ratingInput: "[name='rating']",
skuInput: "[name='sku']",
@ -24,10 +20,9 @@ export const PRODUCT_DETAILS = {
stockInput: '[data-test-id="stock-input"]',
costPriceInput: '[name*="costPrice"]',
sellingPriceInput: '[name*="channel-price"]',
createSingleVariantCheckbox: '[value="single"]',
productImage: '[data-test-id="product-image"]',
uploadImageButton: '[data-test-id="button-upload-image"]',
uploadSavedImagesButton: '[data-test-id="upload-images"]',
uploadMediaUrlButton: '[data-test-id="upload-media-url"]',
saveUploadUrlButton: '[data-test-id="upload-url-button"]'
saveUploadUrlButton: '[data-test-id="upload-url-button"]',
};

View file

@ -1,8 +1,4 @@
export const VARIANTS_SELECTORS = {
attributeCheckbox: "[name*='value:']",
valueContainer: "[data-test-id='value-container']",
nextButton: "[class*='MuiButton-containedPrimary']",
warehouseCheckboxes: "[name*='warehouse:']",
skuInput: "[ name='sku']",
attributeOption:
"[data-test-type='option'], [data-test-id='multi-autocomplete-select-option']",
@ -20,5 +16,5 @@ export const VARIANTS_SELECTORS = {
preorderEndTimeInput: "[name='preorderEndDateTime:time']",
globalThresholdInput: "[name='globalThreshold']",
stockInput: "[data-test-id='stock-input']",
selectOption: "[data-test-id='multi-autocomplete-select-option']"
selectOption: "[data-test-id='multi-autocomplete-select-option']",
};

View file

@ -3,56 +3,14 @@ import { PRODUCT_DETAILS } from "../../../../elements/catalog/products/product-d
import { VARIANTS_SELECTORS } from "../../../../elements/catalog/products/variants-selectors";
import { AVAILABLE_CHANNELS_FORM } from "../../../../elements/channels/available-channels-form";
import { BUTTON_SELECTORS } from "../../../../elements/shared/button-selectors";
import { SHARED_ELEMENTS } from "../../../../elements/shared/sharedElements";
import { formatDate } from "../../../formatData/formatDate";
import { selectChannelVariantInDetailsPage } from "../../channelsPage";
import { fillUpPriceList } from "./priceListComponent";
export function variantsShouldBeVisible({ price }) {
cy.get(PRODUCT_DETAILS.variantRow).should("be.visible");
cy.contains(PRODUCT_DETAILS.variantPrice, price);
}
export function createFirstVariant({
sku,
warehouseId,
price,
attribute,
quantity = 1
}) {
cy.get(PRODUCT_DETAILS.addVariantsButton).click();
cy.get(PRODUCT_DETAILS.addVariantsOptionDialog.optionMultiple).click();
cy.get(BUTTON_SELECTORS.submit).click();
cy.get(VARIANTS_SELECTORS.valueContainer).click();
cy.contains(VARIANTS_SELECTORS.selectOption, attribute)
.click()
.get(VARIANTS_SELECTORS.nextButton)
.click();
fillUpPriceList(price);
if (warehouseId) {
cy.get(`[name*='${warehouseId}']`).click();
} else {
cy.get(VARIANTS_SELECTORS.warehouseCheckboxes)
.first()
.click();
}
cy.get(VARIANTS_SELECTORS.stockInput)
.type(quantity)
.get(VARIANTS_SELECTORS.nextButton)
.click();
if (sku) {
cy.get(VARIANTS_SELECTORS.skuInput).type(sku);
}
cy.addAliasToGraphRequest("ProductVariantBulkCreate")
.get(VARIANTS_SELECTORS.nextButton)
.click()
.confirmationMessageShouldAppear()
.waitForRequestAndCheckIfNoErrors("@ProductVariantBulkCreate")
.waitForProgressBarToNotBeVisible()
.get(AVAILABLE_CHANNELS_FORM.menageChannelsButton)
.should("be.visible");
}
export function createVariant({
sku,
warehouseName,
@ -60,9 +18,9 @@ export function createVariant({
price,
costPrice = price,
channelName,
quantity = 10
quantity = 10,
}) {
cy.get(PRODUCT_DETAILS.addVariantsButton).click();
cy.get(PRODUCT_DETAILS.addVariantButton).click();
fillUpVariantDetails({ attributeName, sku, warehouseName, quantity });
cy.get(VARIANTS_SELECTORS.saveButton)
.click()
@ -76,7 +34,7 @@ export function createVariant({
channelName,
variantName: attributeName,
price,
costPrice
costPrice,
});
}
@ -84,13 +42,13 @@ export function fillUpGeneralVariantInputs({
attributeName,
warehouseName,
sku,
quantity
quantity,
}) {
fillUpVariantAttributeAndSku({ attributeName, sku });
cy.get(VARIANTS_SELECTORS.addWarehouseButton).click();
if (warehouseName) {
cy.contains(VARIANTS_SELECTORS.warehouseOption, warehouseName).click({
force: true
force: true,
});
} else {
cy.get(VARIANTS_SELECTORS.warehouseOption)
@ -105,7 +63,7 @@ export function fillUpVariantDetails({
attributeType = "DROPDOWN",
sku,
warehouseName,
quantity
quantity,
}) {
selectAttributeWithType({ attributeType, attributeName });
if (sku) {
@ -115,7 +73,7 @@ export function fillUpVariantDetails({
cy.get(VARIANTS_SELECTORS.addWarehouseButton).click();
cy.contains(VARIANTS_SELECTORS.warehouseOption, warehouseName)
.click({
force: true
force: true,
})
.get(VARIANTS_SELECTORS.stockInput)
.type(quantity);
@ -138,10 +96,10 @@ export function selectChannelForVariantAndFillUpPrices({
channelName,
variantName,
price,
costPrice = price
costPrice = price,
}) {
cy.waitForProgressBarToNotBeVisible().addAliasToGraphRequest(
"ProductChannelListingUpdate"
"ProductChannelListingUpdate",
);
selectChannelVariantInDetailsPage(channelName, variantName);
cy.get(BUTTON_SELECTORS.confirm)
@ -150,9 +108,13 @@ export function selectChannelForVariantAndFillUpPrices({
cy.contains(PRODUCT_DETAILS.variantRow, variantName)
.click()
.get(PRICE_LIST.priceInput)
.type(price)
.each(input => {
cy.wrap(input).type(price);
})
.get(PRICE_LIST.costPriceInput)
.type(costPrice)
.each(input => {
cy.wrap(input).type(costPrice);
})
.addAliasToGraphRequest("ProductVariantChannelListingUpdate")
.get(VARIANTS_SELECTORS.saveButton)
.click()

View file

@ -108,13 +108,18 @@ export function selectChannelInDetailsPages(channelName) {
export function selectChannelVariantInDetailsPage(channelName, attributeName) {
cy.get(AVAILABLE_CHANNELS_FORM.menageChannelsButton).click();
cy.contains(SELECT_CHANNELS_TO_ASSIGN.expandChannelRow, channelName)
.find(BUTTON_SELECTORS.expandIcon)
.click();
cy.contains(SELECT_CHANNELS_TO_ASSIGN.expandChannelRow, channelName)
.contains(SELECT_CHANNELS_TO_ASSIGN.channelVariantRow, attributeName)
.find(BUTTON_SELECTORS.checkbox)
.click();
const channelsNames = Array.isArray(channelName)
? channelName
: [channelName];
channelsNames.forEach(name => {
cy.contains(SELECT_CHANNELS_TO_ASSIGN.expandChannelRow, name)
.find(BUTTON_SELECTORS.expandIcon)
.click();
cy.contains(SELECT_CHANNELS_TO_ASSIGN.expandChannelRow, name)
.contains(SELECT_CHANNELS_TO_ASSIGN.channelVariantRow, attributeName)
.find(BUTTON_SELECTORS.checkbox)
.click();
});
cy.get(SELECT_CHANNELS_TO_ASSIGN.selectChannelsForm)
.find(BUTTON_SELECTORS.submit)
.click({ force: true });

View file

@ -66,10 +66,6 @@
"+a+2ug": {
"string": "Members"
},
"+bFHzi": {
"context": "button",
"string": "Next"
},
"+c/f61": {
"context": "retry installation",
"string": "Retry"
@ -185,9 +181,6 @@
"context": "section header",
"string": "Media"
},
"/Qb92c": {
"string": "Based on your selections we will create {numberOfProducts} products. Use this step to customize price and stocks for your new products"
},
"/TF6BZ": {
"string": "Search Products"
},
@ -1154,10 +1147,6 @@
"context": "checkbox gift cards label",
"string": "Automatically fulfill non shippable gift cards"
},
"7WEC+G": {
"context": "page title",
"string": "Price and SKUs"
},
"7WzUxn": {
"context": "staff member status",
"string": "Inactive"
@ -1322,10 +1311,6 @@
"context": "modal button images upload",
"string": "Upload Images"
},
"9EMudJ": {
"context": "option",
"string": "Create single variant"
},
"9IWg/f": {
"context": "button",
"string": "SETUP END DATE"
@ -1445,10 +1430,6 @@
"context": "area units type",
"string": "Area"
},
"ABgQcF": {
"context": "variant stock, header",
"string": "Stock"
},
"ADTNND": {
"context": "product type",
"string": "Physical"
@ -1596,9 +1577,6 @@
"context": "number of subcategories",
"string": "Subcategories"
},
"BIqhVQ": {
"string": "Skip stock for now"
},
"BL/Lbk": {
"context": "install app permissions",
"string": "Installing this app will give it following permissions:"
@ -1796,9 +1774,6 @@
"context": "button",
"string": "Create Category"
},
"ClFzoD": {
"string": "Selected values will be used to create variants for the configurable product."
},
"ClKKID": {
"context": "channel publication status",
"string": "Not published"
@ -1806,10 +1781,6 @@
"Co2U4u": {
"string": "No plugins found"
},
"CrbI/c": {
"context": "variant channel price",
"string": "{channel} Price"
},
"CxfKLC": {
"string": "Pages"
},
@ -1970,9 +1941,6 @@
"EEW+ND": {
"string": "Navigator"
},
"EGG8f+": {
"string": "Apply unique prices by attribute to each SKU"
},
"EHsnZX": {
"context": "dialog description",
"string": "Are you sure you want to approve this fullfillment?"
@ -2062,10 +2030,6 @@
"Ev6SEF": {
"string": "New Password"
},
"EzU7KV": {
"context": "option description",
"string": "Create new variant using variant details view"
},
"F0AXNs": {
"context": "invoice create date prefix",
"string": "created"
@ -2279,10 +2243,6 @@
"GOdq5V": {
"string": "Catalog"
},
"GQcp83": {
"context": "variant stock, header",
"string": "Stock and Warehousing"
},
"GUlwXU": {
"context": "dialog header",
"string": "Assign Attribute Value"
@ -2346,10 +2306,6 @@
"context": "link",
"string": "Get Support"
},
"Gjo89T": {
"context": "header",
"string": "Warehouses"
},
"Gkip05": {
"context": "button",
"string": "Unassign"
@ -2469,10 +2425,6 @@
"context": "table column header, sold units preorder quantity",
"string": "Sold units"
},
"HcjV6k": {
"context": "button",
"string": "Create variants"
},
"HedXnw": {
"context": "plugin channel availability status title",
"string": "{activeChannelsCount,plural, =0 {Deactivated} other {Active in {activeChannelsCount}}}"
@ -2659,9 +2611,6 @@
"J/QqOI": {
"string": "This value already exists within this attribute"
},
"J0UdxG": {
"string": "Skip pricing for now"
},
"J0lNnk": {
"context": "column label",
"string": "Warehouse"
@ -2953,9 +2902,6 @@
"context": "returned products list title",
"string": "Products returned"
},
"L5rthO": {
"string": "Apply unique stock by attribute to each SKU"
},
"L6+p8a": {
"context": "dialog title",
"string": "Delete permission group"
@ -3235,10 +3181,6 @@
"NWxomz": {
"string": "Fulfillment status"
},
"NXpFlL": {
"context": "product attribute values, page title",
"string": "Choose Values"
},
"NZtcLb": {
"context": "gift card bulk create success dialog content",
"string": "We have issued all of your requested gift cards. You can download the list of new gift cards using the button below."
@ -3375,10 +3317,6 @@
"context": "Types",
"string": "Page Types"
},
"OgFBAj": {
"context": "input label",
"string": "Price"
},
"OhFGpX": {
"context": "button",
"string": "Assign members"
@ -3566,10 +3504,6 @@
"context": "modal button url upload",
"string": "Upload URL"
},
"Q3j++G": {
"context": "create multiple variants, button",
"string": "Create"
},
"Q47ovw": {
"context": "activate app",
"string": "Are you sure you want to activate this app? Activating will start gathering events."
@ -3846,10 +3780,6 @@
"context": "default gift card delete description",
"string": "{selectedItemsCount,plural,one{Are you sure you want to delete this gift card?} other{Are you sure you want to delete {selectedItemsCount} giftCards?}}"
},
"S5PVx1": {
"context": "variant creator summary card header",
"string": "Created Variants"
},
"S7Rwl0": {
"context": "stock exceeded action question label",
"string": "Are you sure you want to fulfill those products anyway?"
@ -3900,9 +3830,6 @@
"context": "change button label",
"string": "Change"
},
"STp3Hl": {
"string": "Apply single stock to all SKUs"
},
"SUbxSK": {
"context": "product weight",
"string": "Weight"
@ -3943,10 +3870,6 @@
"context": "checkbox label, fulfillment approval",
"string": "Send shipment details to customer"
},
"Sx7QVu": {
"context": "variant creation step",
"string": "Prices and SKU"
},
"T/5OyA": {
"string": "No translation yet"
},
@ -3988,10 +3911,6 @@
"context": "order history message",
"string": "Payment failed"
},
"TDXskW": {
"context": "variant attribute",
"string": "Select Attribute"
},
"TDhHMi": {
"string": "This needs to be unique"
},
@ -4016,10 +3935,6 @@
"context": "button",
"string": "Reset"
},
"TPCRKr": {
"context": "variant price, header",
"string": "Price"
},
"TPW2tP": {
"string": "You have reached your order limit, you will be billed extra for orders above limit. If you would like to up your limit, contact your administration staff about raising your limits."
},
@ -4266,10 +4181,6 @@
"context": "dialog content",
"string": "Removed sale"
},
"V76IV7": {
"context": "variant name",
"string": "Variant"
},
"V8FhTt": {
"context": "collection label",
"string": "Hidden"
@ -4390,10 +4301,6 @@
"context": "staff member full name",
"string": "Name"
},
"W3JbSQ": {
"context": "create product variants",
"string": "How would you like to create variants:"
},
"W5SK5c": {
"string": "Select content type"
},
@ -4576,10 +4483,6 @@
"context": "discount type",
"string": "Fixed amount"
},
"XDeh5D": {
"context": "dialog header",
"string": "Create Variants"
},
"XFtKV5": {
"context": "input placeholder tag",
"string": "Tag"
@ -4663,9 +4566,6 @@
"context": "product pricing, section header",
"string": "Pricing"
},
"Xr5zxu": {
"string": "You choices will add {variantsNumber} SKUs to your catalog which will exceed your limit by {aboveLimitVariantsNumber}. If you would like to up your limit, contact your administration staff about raising your limits."
},
"Xtd0AT": {
"string": "Original String"
},
@ -5447,10 +5347,6 @@
"context": "filters error messages dependencies missing",
"string": "Filter requires other filters: {dependencies}"
},
"esg2wu": {
"context": "previous step, button",
"string": "Previous"
},
"etP0+D": {
"string": "Vouchers"
},
@ -5591,10 +5487,6 @@
"g/BrOt": {
"string": "Url has invalid format"
},
"g1WQlC": {
"context": "page title",
"string": "Summary"
},
"g3qjSf": {
"context": "button",
"string": "Assign categories"
@ -5863,9 +5755,6 @@
"context": "customer",
"string": "Join Date"
},
"iigydN": {
"string": "Based on your selections we will create 8 products. Use this step to customize price and stocks for your new products."
},
"ij7olm": {
"context": "error message",
"string": "This fulfillment cannot be cancelled"
@ -6209,10 +6098,6 @@
"context": "dialog title",
"string": "Unassign users"
},
"lVZ5n7": {
"context": "variant attribute",
"string": "Attribute"
},
"lVwmf5": {
"context": "total price of ordered products",
"string": "Total"
@ -6261,9 +6146,6 @@
"lqIzC8": {
"string": "This field needs to be unique"
},
"lra7Ej": {
"string": "Apply single price to all SKUs"
},
"lrq8O6": {
"context": "order refund amount, input label",
"string": "Amount"
@ -6540,18 +6422,10 @@
"context": "order status",
"string": "Unfulfilled"
},
"oChkS4": {
"context": "success message",
"string": "Successfully created variants"
},
"oHbgcK": {
"context": "PageTypeDeleteWarningDialog title",
"string": "Delete page {selectedTypesCount,plural,one{type} other{types}}"
},
"oIMMcO": {
"context": "no warehouses info",
"string": "There are no warehouses set up for your store. You can configure variants without providing stock quantities."
},
"oIvtua": {
"context": "attribute's editor component",
"string": "Catalog Input type for Store Owner"
@ -6864,9 +6738,6 @@
"context": "invalid date in expirydate field content",
"string": "Gift Card with past expiration date cannot be created"
},
"rHXF43": {
"string": "Here is the summary of variants that will be created. You can change prices, stocks an SKU for each one created."
},
"rHoRbE": {
"context": "Status label when object is unpublished in a channel",
"string": "Unpublished"
@ -6898,10 +6769,6 @@
"context": "page header with order number",
"string": "Order #{orderNumber}"
},
"rVaB7c": {
"context": "attribute values, variant creation step",
"string": "Select Values"
},
"rZMT44": {
"context": "created channels counter",
"string": "{count}/{max} channels used"
@ -7098,10 +6965,6 @@
"context": "button",
"string": "Accept"
},
"slKV5G": {
"context": "variant creation step",
"string": "Summary"
},
"sn2awN": {
"context": "ExitFormPrompt cancel button",
"string": "Discard changes"
@ -7316,10 +7179,6 @@
"context": "order history message",
"string": "Order was confirmed"
},
"ucYPtV": {
"context": "variant attribute",
"string": "Choose attribute"
},
"ud0w8h": {
"context": "number of postal code ranges",
"string": "{number} postal code ranges"
@ -7364,10 +7223,6 @@
"context": "button",
"string": "Assign attribute"
},
"uxZZXx": {
"context": "option",
"string": "Create multiple variant via variant creator"
},
"uy+tB8": {
"context": "voucher status",
"string": "Status"
@ -7459,10 +7314,6 @@
"context": "card title",
"string": "Order Weight"
},
"vXA5xU": {
"context": "option description",
"string": "Use variant creator to create matrix of selected attribute values to create variants"
},
"vXFPD6": {
"context": "voucher discount type",
"string": "Fixed Amount"
@ -7514,9 +7365,6 @@
"context": "button",
"string": "Create category"
},
"vuKrlW": {
"string": "Stock"
},
"vwMO04": {
"context": "draft order",
"string": "Created"
@ -7860,10 +7708,6 @@
"context": "header, dialog",
"string": "Create New Warehouse"
},
"z+wMgQ": {
"context": "window title",
"string": "Create Variants"
},
"z0gGP+": {
"context": "number of attributes",
"string": "{number} Attributes"

View file

@ -6,7 +6,7 @@ import {
getSingleDisplayValue,
} from "@saleor/components/Attributes/utils";
import SingleAutocompleteSelectField from "@saleor/components/SingleAutocompleteSelectField";
import { getBySlug } from "@saleor/products/components/ProductVariantCreatorPage/utils";
import { getBySlug } from "@saleor/misc";
import React from "react";
import { useIntl } from "react-intl";

View file

@ -8,7 +8,7 @@ import SingleAutocompleteSelectField from "@saleor/components/SingleAutocomplete
import { useChannelsQuery, useGiftCardResendMutation } from "@saleor/graphql";
import useForm from "@saleor/hooks/useForm";
import useNotifier from "@saleor/hooks/useNotifier";
import { getBySlug } from "@saleor/products/components/ProductVariantCreatorPage/utils";
import { getBySlug } from "@saleor/misc";
import { DialogProps } from "@saleor/types";
import commonErrorMessages from "@saleor/utils/errors/common";
import { mapSlugNodeToChoice } from "@saleor/utils/maps";

View file

@ -11758,42 +11758,6 @@ export function useProductBulkDeleteMutation(baseOptions?: ApolloReactHooks.Muta
export type ProductBulkDeleteMutationHookResult = ReturnType<typeof useProductBulkDeleteMutation>;
export type ProductBulkDeleteMutationResult = Apollo.MutationResult<Types.ProductBulkDeleteMutation>;
export type ProductBulkDeleteMutationOptions = Apollo.BaseMutationOptions<Types.ProductBulkDeleteMutation, Types.ProductBulkDeleteMutationVariables>;
export const ProductVariantBulkCreateDocument = gql`
mutation ProductVariantBulkCreate($id: ID!, $inputs: [ProductVariantBulkCreateInput!]!) {
productVariantBulkCreate(product: $id, variants: $inputs) {
errors {
...BulkProductError
}
}
}
${BulkProductErrorFragmentDoc}`;
export type ProductVariantBulkCreateMutationFn = Apollo.MutationFunction<Types.ProductVariantBulkCreateMutation, Types.ProductVariantBulkCreateMutationVariables>;
/**
* __useProductVariantBulkCreateMutation__
*
* To run a mutation, you first call `useProductVariantBulkCreateMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useProductVariantBulkCreateMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [productVariantBulkCreateMutation, { data, loading, error }] = useProductVariantBulkCreateMutation({
* variables: {
* id: // value for 'id'
* inputs: // value for 'inputs'
* },
* });
*/
export function useProductVariantBulkCreateMutation(baseOptions?: ApolloReactHooks.MutationHookOptions<Types.ProductVariantBulkCreateMutation, Types.ProductVariantBulkCreateMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return ApolloReactHooks.useMutation<Types.ProductVariantBulkCreateMutation, Types.ProductVariantBulkCreateMutationVariables>(ProductVariantBulkCreateDocument, options);
}
export type ProductVariantBulkCreateMutationHookResult = ReturnType<typeof useProductVariantBulkCreateMutation>;
export type ProductVariantBulkCreateMutationResult = Apollo.MutationResult<Types.ProductVariantBulkCreateMutation>;
export type ProductVariantBulkCreateMutationOptions = Apollo.BaseMutationOptions<Types.ProductVariantBulkCreateMutation, Types.ProductVariantBulkCreateMutationVariables>;
export const ProductVariantBulkDeleteDocument = gql`
mutation ProductVariantBulkDelete($ids: [ID!]!) {
productVariantBulkDelete(ids: $ids) {
@ -12633,53 +12597,6 @@ export function useGridAttributesLazyQuery(baseOptions?: ApolloReactHooks.LazyQu
export type GridAttributesQueryHookResult = ReturnType<typeof useGridAttributesQuery>;
export type GridAttributesLazyQueryHookResult = ReturnType<typeof useGridAttributesLazyQuery>;
export type GridAttributesQueryResult = Apollo.QueryResult<Types.GridAttributesQuery, Types.GridAttributesQueryVariables>;
export const CreateMultipleVariantsDataDocument = gql`
query CreateMultipleVariantsData($id: ID!, $firstValues: Int, $afterValues: String, $lastValues: Int, $beforeValues: String) {
product(id: $id) {
...ProductVariantAttributes
}
warehouses(first: 20) {
edges {
node {
...Warehouse
}
}
}
}
${ProductVariantAttributesFragmentDoc}
${WarehouseFragmentDoc}`;
/**
* __useCreateMultipleVariantsDataQuery__
*
* To run a query within a React component, call `useCreateMultipleVariantsDataQuery` and pass it any options that fit your needs.
* When your component renders, `useCreateMultipleVariantsDataQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useCreateMultipleVariantsDataQuery({
* variables: {
* id: // value for 'id'
* firstValues: // value for 'firstValues'
* afterValues: // value for 'afterValues'
* lastValues: // value for 'lastValues'
* beforeValues: // value for 'beforeValues'
* },
* });
*/
export function useCreateMultipleVariantsDataQuery(baseOptions: ApolloReactHooks.QueryHookOptions<Types.CreateMultipleVariantsDataQuery, Types.CreateMultipleVariantsDataQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return ApolloReactHooks.useQuery<Types.CreateMultipleVariantsDataQuery, Types.CreateMultipleVariantsDataQueryVariables>(CreateMultipleVariantsDataDocument, options);
}
export function useCreateMultipleVariantsDataLazyQuery(baseOptions?: ApolloReactHooks.LazyQueryHookOptions<Types.CreateMultipleVariantsDataQuery, Types.CreateMultipleVariantsDataQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return ApolloReactHooks.useLazyQuery<Types.CreateMultipleVariantsDataQuery, Types.CreateMultipleVariantsDataQueryVariables>(CreateMultipleVariantsDataDocument, options);
}
export type CreateMultipleVariantsDataQueryHookResult = ReturnType<typeof useCreateMultipleVariantsDataQuery>;
export type CreateMultipleVariantsDataLazyQueryHookResult = ReturnType<typeof useCreateMultipleVariantsDataLazyQuery>;
export type CreateMultipleVariantsDataQueryResult = Apollo.QueryResult<Types.CreateMultipleVariantsDataQuery, Types.CreateMultipleVariantsDataQueryVariables>;
export const SearchAttributesDocument = gql`
query SearchAttributes($after: String, $first: Int!, $query: String!) {
search: attributes(after: $after, first: $first, filter: {search: $query}) {

View file

@ -7690,14 +7690,6 @@ export type ProductBulkDeleteMutationVariables = Exact<{
export type ProductBulkDeleteMutation = { __typename: 'Mutation', productBulkDelete: { __typename: 'ProductBulkDelete', errors: Array<{ __typename: 'ProductError', code: ProductErrorCode, field: string | null, message: string | null }> } | null };
export type ProductVariantBulkCreateMutationVariables = Exact<{
id: Scalars['ID'];
inputs: Array<ProductVariantBulkCreateInput> | ProductVariantBulkCreateInput;
}>;
export type ProductVariantBulkCreateMutation = { __typename: 'Mutation', productVariantBulkCreate: { __typename: 'ProductVariantBulkCreate', errors: Array<{ __typename: 'BulkProductError', field: string | null, code: ProductErrorCode, index: number | null, channels: Array<string> | null, message: string | null }> } | null };
export type ProductVariantBulkDeleteMutationVariables = Exact<{
ids: Array<Scalars['ID']> | Scalars['ID'];
}>;
@ -7852,17 +7844,6 @@ export type GridAttributesQueryVariables = Exact<{
export type GridAttributesQuery = { __typename: 'Query', grid: { __typename: 'AttributeCountableConnection', edges: Array<{ __typename: 'AttributeCountableEdge', node: { __typename: 'Attribute', id: string, name: string | null } }> } | null };
export type CreateMultipleVariantsDataQueryVariables = Exact<{
id: Scalars['ID'];
firstValues?: InputMaybe<Scalars['Int']>;
afterValues?: InputMaybe<Scalars['String']>;
lastValues?: InputMaybe<Scalars['Int']>;
beforeValues?: InputMaybe<Scalars['String']>;
}>;
export type CreateMultipleVariantsDataQuery = { __typename: 'Query', product: { __typename: 'Product', id: string, attributes: Array<{ __typename: 'SelectedAttribute', attribute: { __typename: 'Attribute', id: string, slug: string | null, name: string | null, inputType: AttributeInputTypeEnum | null, entityType: AttributeEntityTypeEnum | null, valueRequired: boolean, unit: MeasurementUnitsEnum | null, choices: { __typename: 'AttributeValueCountableConnection', pageInfo: { __typename: 'PageInfo', endCursor: string | null, hasNextPage: boolean, hasPreviousPage: boolean, startCursor: string | null }, edges: Array<{ __typename: 'AttributeValueCountableEdge', cursor: string, node: { __typename: 'AttributeValue', plainText: string | null, richText: any | null, id: string, name: string | null, slug: string | null, reference: string | null, boolean: boolean | null, date: any | null, dateTime: any | null, value: string | null, file: { __typename: 'File', url: string, contentType: string | null } | null } }> } | null }, values: Array<{ __typename: 'AttributeValue', plainText: string | null, richText: any | null, id: string, name: string | null, slug: string | null, reference: string | null, boolean: boolean | null, date: any | null, dateTime: any | null, value: string | null, file: { __typename: 'File', url: string, contentType: string | null } | null }> }>, productType: { __typename: 'ProductType', id: string, variantAttributes: Array<{ __typename: 'Attribute', id: string, name: string | null, inputType: AttributeInputTypeEnum | null, valueRequired: boolean, unit: MeasurementUnitsEnum | null, choices: { __typename: 'AttributeValueCountableConnection', pageInfo: { __typename: 'PageInfo', endCursor: string | null, hasNextPage: boolean, hasPreviousPage: boolean, startCursor: string | null }, edges: Array<{ __typename: 'AttributeValueCountableEdge', cursor: string, node: { __typename: 'AttributeValue', plainText: string | null, richText: any | null, id: string, name: string | null, slug: string | null, reference: string | null, boolean: boolean | null, date: any | null, dateTime: any | null, value: string | null, file: { __typename: 'File', url: string, contentType: string | null } | null } }> } | null }> | null }, channelListings: Array<{ __typename: 'ProductChannelListing', channel: { __typename: 'Channel', id: string, name: string, currencyCode: string } }> | null } | null, warehouses: { __typename: 'WarehouseCountableConnection', edges: Array<{ __typename: 'WarehouseCountableEdge', node: { __typename: 'Warehouse', id: string, name: string } }> } | null };
export type SearchAttributesQueryVariables = Exact<{
after?: InputMaybe<Scalars['String']>;
first: Scalars['Int'];

View file

@ -531,3 +531,6 @@ export const isInDevelopment =
export type WithOptional<T, K extends keyof T> = Omit<T, K> &
Partial<Pick<T, K>>;
export const getBySlug = (slugToCompare: string) => (obj: { slug: string }) =>
obj.slug === slugToCompare;

View file

@ -73,7 +73,6 @@ const props: ProductUpdatePageProps = {
onSetDefaultVariant: () => undefined,
onSubmit,
onVariantReorder: () => undefined,
onVariantsAdd: () => undefined,
onVariantEndPreorderDialogOpen: () => undefined,
onWarehouseConfigure: () => undefined,
openChannelsModal: () => undefined,

View file

@ -114,7 +114,6 @@ export interface ProductUpdatePageProps extends ListActions, ChannelProps {
fetchAttributeValues: (query: string, attributeId: string) => void;
onAssignReferencesClick: (attribute: AttributeInput) => void;
onCloseDialog: () => void;
onVariantsAdd: () => void;
onVariantReorder: ReorderAction;
onVariantEndPreorderDialogOpen: () => void;
onImageDelete: (id: string) => () => void;
@ -177,7 +176,6 @@ export const ProductUpdatePage: React.FC<ProductUpdatePageProps> = ({
onSeoClick,
onSubmit,
channelsData,
onVariantsAdd,
onSetDefaultVariant,
onVariantReorder,
onVariantEndPreorderDialogOpen,
@ -389,7 +387,6 @@ export const ProductUpdatePage: React.FC<ProductUpdatePageProps> = ({
limits={limits}
variants={variants}
product={product}
onVariantsAdd={onVariantsAdd}
onVariantReorder={onVariantReorder}
onSetDefaultVariant={onSetDefaultVariant}
toolbar={toolbar}

View file

@ -1,107 +0,0 @@
import {
Dialog,
DialogActions,
DialogContent,
DialogTitle,
Typography,
} from "@material-ui/core";
import ConfirmButton from "@saleor/components/ConfirmButton";
import Form from "@saleor/components/Form";
import FormSpacer from "@saleor/components/FormSpacer";
import RadioGroupField from "@saleor/components/RadioGroupField";
import { buttonMessages } from "@saleor/intl";
import React from "react";
import { FormattedMessage } from "react-intl";
import { messages } from "./messages";
import { useStyles } from "./styles";
import { ProductVariantCreateOptionEnum } from "./types";
interface ProductVariantCreateDialogForm {
option: ProductVariantCreateOptionEnum;
}
export interface ProductVariantCreateDialogProps {
open: boolean;
onClose: () => void;
onConfirm: (option: ProductVariantCreateOptionEnum) => void;
}
const ProductVariantCreateDialog: React.FC<ProductVariantCreateDialogProps> = props => {
const { open, onConfirm, onClose } = props;
const classes = useStyles(props);
const initialForm = {
option: ProductVariantCreateOptionEnum.MULTIPLE,
};
const handleSubmit = (form: ProductVariantCreateDialogForm) => {
onConfirm(form.option);
};
const options = [
{
title: messages.optionMultipleTitle,
subtitle: messages.optionMultipleDescription,
type: ProductVariantCreateOptionEnum.MULTIPLE,
},
{
title: messages.optionSingleTitle,
subtitle: messages.optionSingleDescription,
type: ProductVariantCreateOptionEnum.SINGLE,
},
];
return (
<Dialog onClose={onClose} open={open}>
<Form initial={initialForm} onSubmit={handleSubmit}>
{({ change, data }) => (
<>
<DialogTitle>
<FormattedMessage {...messages.title} />
</DialogTitle>
<DialogContent className={classes.dialogContent}>
<Typography variant="body2">
<FormattedMessage {...messages.description} />
</Typography>
<FormSpacer />
<RadioGroupField
choices={options.map(option => ({
label: (
<div
className={classes.option}
data-test-id={`variant-create-option-${option.type}`}
>
<Typography variant="body1">
<FormattedMessage {...option.title} />
</Typography>
<Typography color="textSecondary" variant="caption">
<FormattedMessage {...option.subtitle} />
</Typography>
</div>
),
value: option.type,
}))}
name="option"
value={data.option}
onChange={change}
/>
</DialogContent>
<DialogActions>
<ConfirmButton
transitionState="default"
type="submit"
data-test-id="submit"
>
<FormattedMessage {...buttonMessages.create} />
</ConfirmButton>
</DialogActions>
</>
)}
</Form>
</Dialog>
);
};
ProductVariantCreateDialog.displayName = "ProductVariantCreateDialog";
export default ProductVariantCreateDialog;

View file

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

View file

@ -1,35 +0,0 @@
import { defineMessages } from "react-intl";
export const messages = defineMessages({
title: {
id: "XDeh5D",
defaultMessage: "Create Variants",
description: "dialog header",
},
description: {
id: "W3JbSQ",
defaultMessage: "How would you like to create variants:",
description: "create product variants",
},
optionMultipleTitle: {
id: "uxZZXx",
defaultMessage: "Create multiple variant via variant creator",
description: "option",
},
optionMultipleDescription: {
id: "vXA5xU",
defaultMessage:
"Use variant creator to create matrix of selected attribute values to create variants",
description: "option description",
},
optionSingleTitle: {
id: "9EMudJ",
defaultMessage: "Create single variant",
description: "option",
},
optionSingleDescription: {
id: "EzU7KV",
defaultMessage: "Create new variant using variant details view",
description: "option description",
},
});

View file

@ -1,14 +0,0 @@
import { makeStyles } from "@saleor/macaw-ui";
export const useStyles = makeStyles(
theme => ({
option: {
marginBottom: theme.spacing(2),
width: 400,
},
dialogContent: {
padding: 0,
},
}),
{ name: "ProductVariantCreateDialog" },
);

View file

@ -1,4 +0,0 @@
export enum ProductVariantCreateOptionEnum {
MULTIPLE = "multiple",
SINGLE = "single",
}

View file

@ -1,216 +0,0 @@
import { attributes } from "@saleor/attributes/fixtures";
import { productChannels } from "@saleor/channels/fixtures";
import Container from "@saleor/components/Container";
import { fetchMoreProps, limitsReached } from "@saleor/fixtures";
import { BulkProductErrorFragment, ProductErrorCode } from "@saleor/graphql";
import { warehouseList } from "@saleor/warehouses/fixtures";
import { storiesOf } from "@storybook/react";
import React from "react";
import Decorator from "../../../storybook/Decorator";
import { createVariants } from "./createVariants";
import {
ChannelPrice,
Price,
ProductVariantCreateFormData,
Stock,
} from "./form";
import ProductVariantCreatorContent, {
ProductVariantCreatorContentProps,
} from "./ProductVariantCreatorContent";
import ProductVariantCreatorPage from "./ProductVariantCreatorPage";
import { ProductVariantCreatorStep } from "./types";
const selectedAttributes = [1, 4, 5].map(index => attributes[index]);
const selectedWarehouses = [0, 1, 3].map(index => warehouseList[index]);
const channels: ChannelPrice[] = productChannels.map(channel => ({
channelId: channel.channel.id,
price: channel.pricing?.priceRange?.start?.net.amount.toString(),
}));
const price: Price = {
attribute: selectedAttributes[0].id,
channels,
mode: "attribute",
values: selectedAttributes[0].choices.edges.map(attribute => ({
slug: attribute.node.slug,
value: channels,
})),
};
const stock: Stock = {
attribute: selectedAttributes[0].id,
mode: "attribute",
value: selectedWarehouses.map(
(_, warehouseIndex) => (warehouseIndex + 2) * 3,
),
values: selectedAttributes[0].choices.edges.map(
(attribute, attributeIndex) => ({
slug: attribute.node.slug,
value: selectedWarehouses.map(
(_, warehouseIndex) =>
selectedAttributes.length * 10 - attributeIndex - warehouseIndex * 3,
),
}),
),
};
const dataAttributes = selectedAttributes.map(attribute => ({
id: attribute.id,
valueRequired: attribute.valueRequired,
values: attribute.choices.edges
.map(value => ({
slug: value.node.slug,
value: value.node,
}))
.filter((_, valueIndex) => valueIndex % 2 !== 1),
}));
const errors: BulkProductErrorFragment[] = [
{
__typename: "BulkProductError",
channels: [channels[0].channelId],
code: ProductErrorCode.UNIQUE,
field: "sku",
index: 3,
message: "Uniwue bulk product error",
},
];
const data: ProductVariantCreateFormData = {
attributes: dataAttributes,
price,
stock,
variants: createVariants({
attributes: dataAttributes,
price,
stock,
variants: [],
warehouses: selectedWarehouses.map(warehouse => warehouse.id),
}),
warehouses: selectedWarehouses.map(warehouse => warehouse.id),
};
const props: ProductVariantCreatorContentProps = {
attributes: [0, 1, 4, 6].map(index => attributes[index]),
attributeValues: [],
fetchAttributeValues: () => undefined,
fetchMoreAttributeValues: fetchMoreProps,
channelListings: productChannels.map(listing => ({
currency: listing.pricing?.priceRange?.start?.net.currency,
id: listing.channel.id,
name: listing.channel.name,
price: listing.pricing?.priceRange?.start?.net?.amount.toString() || "",
})),
data: {
...data,
variants: createVariants(data),
},
dispatchFormDataAction: () => undefined,
errors: [],
variantsLeft: 6,
step: ProductVariantCreatorStep.values,
warehouses: warehouseList,
onAttributeSelectBlur: () => undefined,
};
storiesOf("Views / Products / Create multiple variants", module)
.addDecorator(storyFn => <Container>{storyFn()}</Container>)
.addDecorator(Decorator)
.add("choose values", () => <ProductVariantCreatorContent {...props} />);
storiesOf(
"Views / Products / Create multiple variants / prices and SKUs",
module,
)
.addDecorator(storyFn => <Container>{storyFn()}</Container>)
.addDecorator(Decorator)
.add("apply to all", () => (
<ProductVariantCreatorContent
{...props}
data={{
...data,
stock: {
...data.stock,
mode: "all",
},
}}
step={ProductVariantCreatorStep.prices}
/>
))
.add("apply to all when one warehouse", () => (
<ProductVariantCreatorContent
{...props}
data={{
...data,
stock: {
...data.stock,
mode: "all",
},
warehouses: [data.warehouses[0]],
}}
step={ProductVariantCreatorStep.prices}
warehouses={[props.warehouses[0]]}
/>
))
.add("apply to attribute", () => (
<ProductVariantCreatorContent
{...props}
step={ProductVariantCreatorStep.prices}
/>
))
.add("apply to attribute when one warehouse", () => (
<ProductVariantCreatorContent
{...props}
data={{
...data,
warehouses: [data.warehouses[0]],
}}
step={ProductVariantCreatorStep.prices}
warehouses={[props.warehouses[0]]}
/>
))
.add("ship when no warehouses", () => (
<ProductVariantCreatorContent
{...props}
data={{
...data,
warehouses: [],
}}
step={ProductVariantCreatorStep.prices}
warehouses={[]}
/>
));
storiesOf("Views / Products / Create multiple variants / summary", module)
.addDecorator(storyFn => <Container>{storyFn()}</Container>)
.addDecorator(Decorator)
.add("default", () => (
<ProductVariantCreatorContent
{...props}
step={ProductVariantCreatorStep.summary}
/>
))
.add("errors", () => (
<ProductVariantCreatorContent
{...props}
step={ProductVariantCreatorStep.summary}
errors={errors}
/>
));
storiesOf("Views / Products / Create multiple variants", module)
.addDecorator(Decorator)
.add("interactive", () => (
<ProductVariantCreatorPage
{...props}
limits={{
...limitsReached,
currentUsage: {
...limitsReached.currentUsage,
productVariants: limitsReached.currentUsage.productVariants - 6,
},
}}
onSubmit={() => undefined}
/>
));

View file

@ -1,217 +0,0 @@
import { ChannelPriceData } from "@saleor/channels/utils";
import {
BulkProductErrorFragment,
ProductVariantAttributesFragment,
SearchAttributeValuesQuery,
WarehouseFragment,
} from "@saleor/graphql";
import { FetchMoreProps, RelayToFlat } from "@saleor/types";
import { isSelected } from "@saleor/utils/lists";
import React from "react";
import { ProductVariantCreateFormData } from "./form";
import ProductVariantCreatorPriceAndSku from "./ProductVariantCreatorPriceAndSku";
import ProductVariantCreatorSummary from "./ProductVariantCreatorSummary";
import ProductVariantCreatorValues from "./ProductVariantCreatorValues";
import {
ProductVariantCreateReducerAction,
ProductVariantCreateReducerActionType,
} from "./reducer";
import { ProductVariantCreatorStep } from "./types";
export interface ProductVariantCreatorContentProps {
attributes: ProductVariantAttributesFragment["productType"]["variantAttributes"];
attributeValues: RelayToFlat<
SearchAttributeValuesQuery["attribute"]["choices"]
>;
channelListings: ChannelPriceData[];
data: ProductVariantCreateFormData;
dispatchFormDataAction: React.Dispatch<ProductVariantCreateReducerAction>;
errors: BulkProductErrorFragment[];
step: ProductVariantCreatorStep;
variantsLeft: number | null;
warehouses: WarehouseFragment[];
fetchAttributeValues: (query: string, attributeId: string) => void;
fetchMoreAttributeValues?: FetchMoreProps;
onAttributeSelectBlur: () => void;
}
const ProductVariantCreatorContent: React.FC<ProductVariantCreatorContentProps> = ({
attributes,
attributeValues,
fetchAttributeValues,
fetchMoreAttributeValues,
channelListings,
data,
dispatchFormDataAction,
errors,
step,
variantsLeft,
warehouses,
onAttributeSelectBlur,
}) => {
const selectedAttributes = attributes.filter(attribute =>
isSelected(
attribute.id,
data.attributes.map(dataAttribute => dataAttribute.id),
(a, b) => a === b,
),
);
return (
<>
{step === ProductVariantCreatorStep.values && (
<ProductVariantCreatorValues
attributes={selectedAttributes}
attributeValues={attributeValues}
fetchAttributeValues={fetchAttributeValues}
fetchMoreAttributeValues={fetchMoreAttributeValues}
data={data}
variantsLeft={variantsLeft}
onValueClick={(attributeId, value) =>
dispatchFormDataAction({
selectValue: {
attributeId,
value,
},
type: ProductVariantCreateReducerActionType.selectValue,
})
}
onValueBlur={onAttributeSelectBlur}
/>
)}
{step === ProductVariantCreatorStep.prices && (
<ProductVariantCreatorPriceAndSku
attributes={selectedAttributes}
data={data}
channelListings={channelListings}
warehouses={warehouses}
onApplyToAllChange={(mode, type) =>
dispatchFormDataAction({
applyPriceOrStockToAll: {
mode,
},
type:
type === "price"
? ProductVariantCreateReducerActionType.applyPriceToAll
: ProductVariantCreateReducerActionType.applyStockToAll,
})
}
onApplyToAllPriceChange={(channelId, price) =>
dispatchFormDataAction({
changeApplyPriceToAllValue: {
channelId,
price,
},
type:
ProductVariantCreateReducerActionType.changeApplyPriceToAllValue,
})
}
onApplyToAllStockChange={(quantity, warehouseIndex) =>
dispatchFormDataAction({
changeApplyStockToAllValue: {
quantity,
warehouseIndex,
},
type:
ProductVariantCreateReducerActionType.changeApplyStockToAllValue,
})
}
onAttributeSelect={(attributeId, type) =>
dispatchFormDataAction({
changeApplyPriceOrStockToAttributeId: {
attributeId,
},
type:
type === "price"
? ProductVariantCreateReducerActionType.changeApplyPriceToAttributeId
: ProductVariantCreateReducerActionType.changeApplyStockToAttributeId,
})
}
onAttributePriceChange={(valueId, price, channelId) =>
dispatchFormDataAction({
changeAttributeValuePrice: {
channelId,
price,
valueId,
},
type:
ProductVariantCreateReducerActionType.changeAttributeValuePrice,
})
}
onAttributeStockChange={(valueId, quantity, warehouseIndex) =>
dispatchFormDataAction({
changeAttributeValueStock: {
quantity,
valueId,
warehouseIndex,
},
type:
ProductVariantCreateReducerActionType.changeAttributeValueStock,
})
}
onWarehouseToggle={warehouseId =>
dispatchFormDataAction({
changeWarehouses: {
warehouseId,
},
type: ProductVariantCreateReducerActionType.changeWarehouses,
})
}
/>
)}
{step === ProductVariantCreatorStep.summary && (
<ProductVariantCreatorSummary
attributes={selectedAttributes}
channelListings={channelListings}
data={data}
errors={errors}
onVariantSkuChange={(variantIndex, value) =>
dispatchFormDataAction({
changeVariantSku: {
value,
variantIndex,
},
type: ProductVariantCreateReducerActionType.changeVariantSku,
})
}
onVariantPriceDataChange={(variantIndex, value) =>
dispatchFormDataAction({
changeVariantPriceData: {
value,
variantIndex,
},
type:
ProductVariantCreateReducerActionType.changeVariantPriceData,
})
}
onVariantStockDataChange={(variantIndex, warehouse, value) =>
dispatchFormDataAction({
changeVariantStockData: {
stock: {
quantity: parseInt(value, 10),
warehouse,
},
variantIndex,
},
type:
ProductVariantCreateReducerActionType.changeVariantStockData,
})
}
onVariantDelete={variantIndex =>
dispatchFormDataAction({
deleteVariant: {
variantIndex,
},
type: ProductVariantCreateReducerActionType.deleteVariant,
})
}
warehouses={warehouses}
/>
)}
</>
);
};
ProductVariantCreatorContent.displayName = "ProductVariantCreatorContent";
export default ProductVariantCreatorContent;

View file

@ -1,270 +0,0 @@
import { Typography } from "@material-ui/core";
import { Button } from "@saleor/components/Button";
import Container from "@saleor/components/Container";
import Hr from "@saleor/components/Hr";
import PageHeader from "@saleor/components/PageHeader";
import {
ProductVariantBulkCreateInput,
RefreshLimitsQuery,
} from "@saleor/graphql";
import { SubmitPromise } from "@saleor/hooks/useForm";
import useWizard from "@saleor/hooks/useWizard";
import { makeStyles } from "@saleor/macaw-ui";
import { validatePrice } from "@saleor/products/utils/validation";
import React from "react";
import { FormattedMessage, IntlShape, useIntl } from "react-intl";
import { createInitialForm, ProductVariantCreateFormData } from "./form";
import ProductVariantCreatorContent, {
ProductVariantCreatorContentProps,
} from "./ProductVariantCreatorContent";
import ProductVariantCreateTabs from "./ProductVariantCreatorTabs";
import { getVariantsNumber } from "./ProductVariantCreatorValues";
import reduceProductVariantCreateFormData, {
ProductVariantCreateReducerActionType,
} from "./reducer";
import { ProductVariantCreatorStep } from "./types";
import { dedupeListings } from "./utils";
const useStyles = makeStyles(
theme => ({
button: {
marginLeft: theme.spacing(2),
},
content: {
overflowX: "visible",
},
description: {
marginTop: theme.spacing(),
},
hr: {
margin: theme.spacing(3, 0),
},
}),
{ name: "ProductVariantCreatePage" },
);
function canHitNext(
step: ProductVariantCreatorStep,
data: ProductVariantCreateFormData,
variantsLeft: number | null,
): boolean {
switch (step) {
case ProductVariantCreatorStep.values:
return (
data.attributes.every(
attribute => !attribute.valueRequired || attribute.values.length > 0,
) &&
(variantsLeft === null || getVariantsNumber(data) <= variantsLeft)
);
case ProductVariantCreatorStep.prices:
if (data.price.mode === "all") {
if (data.price.channels.some(channel => validatePrice(channel.price))) {
return false;
}
} else if (data.price.mode === "attribute") {
if (
!data.price.attribute ||
data.price.values.some(
attribute =>
attribute.value.length < data.price.channels.length ||
attribute.value.some(channel => validatePrice(channel.price)),
)
) {
return false;
}
}
if (data.stock.mode === "attribute" && data.stock.attribute === "") {
return false;
}
return true;
case ProductVariantCreatorStep.summary:
return !data.variants.some(variant =>
variant.channelListings.some(channel => validatePrice(channel.price)),
);
default:
return false;
}
}
export interface ProductVariantCreatePageProps
extends Omit<
ProductVariantCreatorContentProps,
"data" | "dispatchFormDataAction" | "step" | "variantsLeft" | "onStepClick"
> {
limits: RefreshLimitsQuery["shop"]["limits"];
onSubmit: (data: ProductVariantBulkCreateInput[]) => SubmitPromise;
}
function getTitle(step: ProductVariantCreatorStep, intl: IntlShape): string {
switch (step) {
case ProductVariantCreatorStep.values:
return intl.formatMessage({
id: "NXpFlL",
defaultMessage: "Choose Values",
description: "product attribute values, page title",
});
case ProductVariantCreatorStep.prices:
return intl.formatMessage({
id: "7WEC+G",
defaultMessage: "Price and SKUs",
description: "page title",
});
case ProductVariantCreatorStep.summary:
return intl.formatMessage({
id: "g1WQlC",
defaultMessage: "Summary",
description: "page title",
});
}
}
function getDescription(
step: ProductVariantCreatorStep,
intl: IntlShape,
): string {
switch (step) {
case ProductVariantCreatorStep.values:
return intl.formatMessage({
id: "ClFzoD",
defaultMessage:
"Selected values will be used to create variants for the configurable product.",
});
case ProductVariantCreatorStep.prices:
return intl.formatMessage({
id: "iigydN",
defaultMessage:
"Based on your selections we will create 8 products. Use this step to customize price and stocks for your new products.",
});
case ProductVariantCreatorStep.summary:
return intl.formatMessage({
id: "rHXF43",
defaultMessage:
"Here is the summary of variants that will be created. You can change prices, stocks an SKU for each one created.",
});
}
}
const ProductVariantCreatePage: React.FC<ProductVariantCreatePageProps> = props => {
const {
attributes,
channelListings,
errors,
limits,
onSubmit,
warehouses,
...contentProps
} = props;
const classes = useStyles(props);
const intl = useIntl();
const [wizardData, dispatchFormDataAction] = React.useReducer(
reduceProductVariantCreateFormData,
createInitialForm(attributes, channelListings, warehouses),
);
const [step, { next: nextStep, prev: prevStep, set: setStep }] = useWizard<
ProductVariantCreatorStep
>(
ProductVariantCreatorStep.values,
[
ProductVariantCreatorStep.values,
ProductVariantCreatorStep.prices,
ProductVariantCreatorStep.summary,
],
{
onTransition: (_, nextStep) => {
if (nextStep === ProductVariantCreatorStep.summary) {
dispatchFormDataAction({
type: ProductVariantCreateReducerActionType.rebuild,
});
}
},
},
);
const reloadForm = () =>
dispatchFormDataAction({
reload: {
data: createInitialForm(attributes, channelListings, warehouses),
},
type: ProductVariantCreateReducerActionType.reload,
});
React.useEffect(reloadForm, [attributes.length, warehouses.length]);
const variantsLeft = limits?.allowedUsage.productVariants
? limits.allowedUsage.productVariants - limits.currentUsage.productVariants
: null;
return (
<Container>
<ProductVariantCreateTabs step={step} onStepClick={setStep} />
<PageHeader
title={
<>
{getTitle(step, intl)}
<Typography className={classes.description} variant="body2">
{getDescription(step, intl)}
</Typography>
</>
}
>
{step !== ProductVariantCreatorStep.values && (
<Button className={classes.button} onClick={prevStep}>
<FormattedMessage
id="esg2wu"
defaultMessage="Previous"
description="previous step, button"
/>
</Button>
)}
{step !== ProductVariantCreatorStep.summary ? (
<Button
data-test-id="next-step"
className={classes.button}
disabled={!canHitNext(step, wizardData, variantsLeft)}
variant="primary"
onClick={nextStep}
>
<FormattedMessage
id="+bFHzi"
defaultMessage="Next"
description="button"
/>
</Button>
) : (
<Button
className={classes.button}
disabled={!canHitNext(step, wizardData, variantsLeft)}
variant="primary"
onClick={() => onSubmit(dedupeListings(wizardData).variants)}
>
<FormattedMessage
id="Q3j++G"
defaultMessage="Create"
description="create multiple variants, button"
/>
</Button>
)}
</PageHeader>
<Hr className={classes.hr} />
<div className={classes.content}>
<ProductVariantCreatorContent
{...contentProps}
attributes={attributes}
channelListings={channelListings}
data={wizardData}
dispatchFormDataAction={dispatchFormDataAction}
errors={errors}
variantsLeft={variantsLeft}
step={step}
warehouses={warehouses}
/>
</div>
</Container>
);
};
ProductVariantCreatePage.displayName = "ProductVariantCreatePage";
export default ProductVariantCreatePage;

View file

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

View file

@ -1,250 +0,0 @@
import {
Card,
CardContent,
FormControlLabel,
Radio,
RadioGroup,
Typography,
} from "@material-ui/core";
import { ChannelPriceData } from "@saleor/channels/utils";
import CardTitle from "@saleor/components/CardTitle";
import FormSpacer from "@saleor/components/FormSpacer";
import Grid from "@saleor/components/Grid";
import Hr from "@saleor/components/Hr";
import PriceField from "@saleor/components/PriceField";
import SingleSelectField from "@saleor/components/SingleSelectField";
import { ProductVariantAttributesFragment } from "@saleor/graphql";
import { makeStyles } from "@saleor/macaw-ui";
import React from "react";
import { defineMessages, FormattedMessage, useIntl } from "react-intl";
import {
ProductVariantCreateFormData,
VariantCreatorPricesAndSkuMode,
} from "./form";
import { getPriceAttributeValues } from "./utils";
const messages = defineMessages({
price: {
id: "OgFBAj",
defaultMessage: "Price",
description: "input label",
},
});
const useStyles = makeStyles(
theme => ({
attrInputsContainer: {
display: "grid",
gap: theme.spacing(2),
gridTemplateColumns:
"minmax(80px, 150px) repeat(auto-fit, minmax(150px, 250px))",
},
channelName: {
marginBottom: theme.spacing(1),
},
container: {
display: "block",
},
hr: {
marginBottom: theme.spacing(),
marginTop: theme.spacing(0.5),
},
hrAttribute: {
marginTop: theme.spacing(2),
},
inputsContainer: {
display: "grid",
gap: theme.spacing(2),
gridTemplateColumns: "repeat(auto-fit, minmax(150px, 250px))",
},
label: {
alignSelf: "center",
},
}),
{ name: "ProductVariantCreatorPrices" },
);
export interface ProductVariantCreatorPricesProps {
attributes: ProductVariantAttributesFragment["productType"]["variantAttributes"];
channelListings: ChannelPriceData[];
data: ProductVariantCreateFormData;
onApplyToAllChange: (applyToAll: VariantCreatorPricesAndSkuMode) => void;
onApplyToAllPriceChange: (channelId: string, value: string) => void;
onAttributeSelect: (id: string) => void;
onAttributeValueChange: (
id: string,
value: string,
channelId: string,
) => void;
}
const ProductVariantCreatorPrices: React.FC<ProductVariantCreatorPricesProps> = props => {
const {
attributes,
channelListings,
data,
onApplyToAllChange,
onApplyToAllPriceChange,
onAttributeSelect,
onAttributeValueChange,
} = props;
const classes = useStyles(props);
const intl = useIntl();
const attributeChoices = attributes.map(attribute => ({
label: attribute.name,
value: attribute.id,
}));
const priceAttributeValues = getPriceAttributeValues(data, attributes);
return (
<Card>
<CardTitle
title={intl.formatMessage({
id: "TPCRKr",
defaultMessage: "Price",
description: "variant price, header",
})}
/>
<CardContent>
<RadioGroup className={classes.container} value={data.price.mode}>
<FormControlLabel
value="all"
control={<Radio color="primary" />}
label={intl.formatMessage({
id: "lra7Ej",
defaultMessage: "Apply single price to all SKUs",
})}
onChange={() => onApplyToAllChange("all")}
/>
{data.price.mode === "all" && (
<>
<FormSpacer />
<div className={classes.inputsContainer}>
{channelListings?.map(listing => (
<div key={listing.id}>
<Typography
variant="caption"
className={classes.channelName}
>
{listing.name}
</Typography>
<PriceField
name={`${listing.id}-variant-channel-price`}
value={
data.price.channels.find(
channel => channel.channelId === listing.id,
)?.price
}
label={intl.formatMessage(messages.price)}
currencySymbol={listing.currency}
onChange={event =>
onApplyToAllPriceChange(listing.id, event.target.value)
}
/>
</div>
))}
</div>
</>
)}
<FormSpacer />
<FormControlLabel
value="attribute"
control={<Radio color="primary" />}
label={intl.formatMessage({
id: "EGG8f+",
defaultMessage: "Apply unique prices by attribute to each SKU",
})}
onChange={() => onApplyToAllChange("attribute")}
/>
{data.price.mode === "attribute" && (
<>
<FormSpacer />
<Grid variant="uniform">
<div className={classes.label}>
<Typography>
<FormattedMessage
id="ucYPtV"
defaultMessage="Choose attribute"
description="variant attribute"
/>
</Typography>
</div>
<div>
<SingleSelectField
choices={attributeChoices}
label={intl.formatMessage({
id: "lVZ5n7",
defaultMessage: "Attribute",
description: "variant attribute",
})}
value={data.price.attribute}
onChange={event => onAttributeSelect(event.target.value)}
/>
</div>
</Grid>
{priceAttributeValues &&
priceAttributeValues.map(attributeValue => {
const attributesChannels = data.price.values.find(
value => value.slug === attributeValue.slug,
).value;
return (
<React.Fragment key={attributeValue.id}>
<Hr className={classes.hrAttribute} />
<FormSpacer />
<div className={classes.attrInputsContainer}>
<div className={classes.label}>
<Typography>{attributeValue.name}</Typography>
</div>
{channelListings?.map(listing => (
<div key={listing.id}>
<Typography
variant="caption"
className={classes.channelName}
>
{listing.name}
</Typography>
<PriceField
label={intl.formatMessage(messages.price)}
currencySymbol={listing.currency}
value={
attributesChannels.find(
attrChannel =>
attrChannel.channelId === listing.id,
)?.price || ""
}
onChange={event =>
onAttributeValueChange(
attributeValue.slug,
event.target.value,
listing.id,
)
}
/>
</div>
))}
</div>
</React.Fragment>
);
})}
</>
)}
<FormSpacer />
<FormControlLabel
value="skip"
control={<Radio color="primary" />}
label={intl.formatMessage({
id: "J0UdxG",
defaultMessage: "Skip pricing for now",
})}
onChange={() => onApplyToAllChange("skip")}
/>
</RadioGroup>
</CardContent>
</Card>
);
};
ProductVariantCreatorPrices.displayName = "ProductVariantCreatorPrices";
export default ProductVariantCreatorPrices;

View file

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

View file

@ -1,346 +0,0 @@
import { Card, TextField } from "@material-ui/core";
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 { ChannelPriceData } from "@saleor/channels/utils";
import CardTitle from "@saleor/components/CardTitle";
import Hr from "@saleor/components/Hr";
import PriceField from "@saleor/components/PriceField";
import {
BulkProductErrorFragment,
ProductFragment,
ProductVariantBulkCreateInput,
WarehouseFragment,
} from "@saleor/graphql";
import { DeleteIcon, IconButton, makeStyles } from "@saleor/macaw-ui";
import { getFormErrors } from "@saleor/utils/errors";
import { getBulkProductErrorMessage } from "@saleor/utils/errors/product";
import classNames from "classnames";
import React from "react";
import { FormattedMessage, useIntl } from "react-intl";
import { Attribute, ChannelPrice, ProductVariantCreateFormData } from "./form";
export interface ProductVariantCreatorSummaryProps {
attributes: ProductFragment["productType"]["variantAttributes"];
channelListings: ChannelPriceData[];
data: ProductVariantCreateFormData;
errors: BulkProductErrorFragment[];
warehouses: WarehouseFragment[];
onVariantSkuChange: (variantIndex: number, value: string) => void;
onVariantStockDataChange: (
variantIndex: number,
warehouseId: string,
value: string,
) => void;
onVariantDelete: (variantIndex: number) => void;
onVariantPriceDataChange: (variantIndex: number, value: ChannelPrice) => void;
}
type ClassKey =
| "attributeValue"
| "card"
| "col"
| "colHeader"
| "colName"
| "colPrice"
| "colSku"
| "colStock"
| "delete"
| "hr"
| "input"
| "summary";
const colors = [blue, cyan, green, purple, yellow].map(color => color[800]);
const useStyles = makeStyles<ProductVariantCreatorSummaryProps, ClassKey>(
theme => ({
attributeValue: {
display: "inline-block",
marginRight: theme.spacing(1),
},
card: {
paddingBottom: theme.spacing(),
},
col: {
...theme.typography.body1,
fontSize: 14,
},
colHeader: {
...theme.typography.body1,
fontSize: 14,
paddingTop: theme.spacing(3),
},
colName: {
"&:not($colHeader)": {
paddingTop: theme.spacing(2),
},
paddingLeft: theme.spacing(3),
},
colPrice: {},
colSku: {},
colStock: {},
delete: {
marginTop: theme.spacing(0.5),
},
hr: {
gridColumn: props =>
`span ${4 +
props.data.variants[0].stocks.length +
props.data.variants[0].channelListings.length}`,
},
input: {
"& input": {
padding: "16px 12px 17px",
},
},
summary: {
columnGap: theme.spacing(3),
display: "grid",
gridTemplateColumns: props =>
`minmax(180px, auto) repeat(${props.data.variants[0].channelListings
.length +
props.data.variants[0].stocks
.length}, minmax(180px, auto)) 220px 64px`,
overflowX: "scroll",
rowGap: theme.spacing(),
paddingBottom: 3,
},
}),
{
name: "ProductVariantCreatorSummary",
},
);
function getVariantName(
variant: ProductVariantBulkCreateInput,
attributes: Attribute[],
): string[] {
return attributes.reduce(
(acc, attribute) => [
...acc,
attribute.values?.find(value => {
const variantAttributeValue = variant.attributes.find(
variantAttribute => variantAttribute.id === attribute.id,
);
return (
variantAttributeValue.values?.[0] === value?.slug ||
variantAttributeValue.boolean === value.value.boolean
);
})?.value?.name,
],
[],
);
}
const ProductVariantCreatorSummary: React.FC<ProductVariantCreatorSummaryProps> = props => {
const {
channelListings,
data,
errors,
warehouses,
onVariantPriceDataChange,
onVariantSkuChange,
onVariantDelete,
onVariantStockDataChange,
} = props;
const classes = useStyles(props);
const intl = useIntl();
return (
<Card className={classes.card}>
<CardTitle
title={intl.formatMessage({
id: "S5PVx1",
defaultMessage: "Created Variants",
description: "variant creator summary card header",
})}
/>
<div className={classes.summary}>
<div
className={classNames(
classes.col,
classes.colHeader,
classes.colName,
)}
>
<FormattedMessage
id="V76IV7"
defaultMessage="Variant"
description="variant name"
/>
</div>
{channelListings.map(listing => (
<div
key={listing.id}
className={classNames(
classes.col,
classes.colHeader,
classes.colPrice,
)}
>
<FormattedMessage
id="CrbI/c"
defaultMessage="{channel} Price"
description="variant channel price"
values={{
channel: listing.name,
}}
/>
</div>
))}
{data.warehouses.map(warehouseId => (
<div
className={classNames(
classes.col,
classes.colHeader,
classes.colStock,
)}
key={warehouseId}
>
{warehouses.find(warehouse => warehouse.id === warehouseId).name}
</div>
))}
<div
className={classNames(classes.col, classes.colHeader, classes.colSku)}
>
<FormattedMessage id="k4brJy" defaultMessage="SKU" />
</div>
<div className={classNames(classes.col, classes.colHeader)} />
<Hr className={classes.hr} />
{data.variants.map((variant, variantIndex) => {
const variantErrors = errors.filter(
error => error.index === variantIndex,
);
const variantFormErrors = getFormErrors(
["price", "quantity", "sku"],
variantErrors,
);
return (
<React.Fragment
key={variant.attributes
.map(attribute => attribute.values?.[0] ?? attribute.boolean)
.join(":")}
>
<div className={classNames(classes.col, classes.colName)}>
{getVariantName(variant, data.attributes).map(
(value, valueIndex) => (
<span
className={classes.attributeValue}
style={{
color: colors[valueIndex % colors.length],
}}
key={`${value}:${valueIndex}`}
>
{value}
</span>
),
)}
</div>
{channelListings.map(listing => {
const error = variantFormErrors.price?.channels?.find(
id => id === listing.id,
);
return (
<div
key={listing.id}
className={classNames(classes.col, classes.colPrice)}
>
<PriceField
className={classes.input}
currencySymbol={listing.currency}
error={!!error}
hint={
error
? getBulkProductErrorMessage(
variantFormErrors.price,
intl,
)
: ""
}
value={
variant.channelListings?.find(
channel => channel.channelId === listing.id,
)?.price
}
required
onChange={event =>
onVariantPriceDataChange(variantIndex, {
channelId: listing.id,
price: event.target.value,
})
}
/>
</div>
);
})}
{variant.stocks.map(stock => (
<div
className={classNames(classes.col, classes.colStock)}
key={stock.warehouse}
>
<TextField
className={classes.input}
error={!!variantFormErrors.quantity}
helperText={getBulkProductErrorMessage(
variantFormErrors.quantity,
intl,
)}
inputProps={{
min: 0,
type: "number",
}}
fullWidth
value={stock.quantity}
onChange={event =>
onVariantStockDataChange(
variantIndex,
stock.warehouse,
event.target.value,
)
}
/>
</div>
))}
<div className={classNames(classes.col, classes.colSku)}>
<TextField
name="sku"
className={classes.input}
error={!!variantFormErrors.sku}
helperText={getBulkProductErrorMessage(
variantFormErrors.sku,
intl,
)}
fullWidth
value={variant.sku}
onChange={event =>
onVariantSkuChange(variantIndex, event.target.value)
}
/>
</div>
<div className={classes.col}>
<IconButton
variant="secondary"
className={classes.delete}
color="primary"
onClick={() => onVariantDelete(variantIndex)}
>
<DeleteIcon />
</IconButton>
</div>
{variantIndex !== data.variants.length - 1 && (
<Hr className={classes.hr} />
)}
</React.Fragment>
);
})}
</div>
</Card>
);
};
ProductVariantCreatorSummary.displayName = "ProductVariantCreatorSummary";
export default ProductVariantCreatorSummary;

View file

@ -1,63 +0,0 @@
import makeCreatorSteps, { Step } from "@saleor/components/CreatorSteps";
import React from "react";
import { useIntl } from "react-intl";
import { ProductVariantCreatorStep } from "./types";
function useSteps(): Array<Step<ProductVariantCreatorStep>> {
const intl = useIntl();
return [
{
label: intl.formatMessage({
id: "rVaB7c",
defaultMessage: "Select Values",
description: "attribute values, variant creation step",
}),
value: ProductVariantCreatorStep.values,
},
{
label: intl.formatMessage({
id: "Sx7QVu",
defaultMessage: "Prices and SKU",
description: "variant creation step",
}),
value: ProductVariantCreatorStep.prices,
},
{
label: intl.formatMessage({
id: "slKV5G",
defaultMessage: "Summary",
description: "variant creation step",
}),
value: ProductVariantCreatorStep.summary,
},
];
}
const ProductVariantCreatorSteps = makeCreatorSteps<
ProductVariantCreatorStep
>();
export interface ProductVariantCreatorTabsProps {
step: ProductVariantCreatorStep;
onStepClick: (step: ProductVariantCreatorStep) => void;
}
const ProductVariantCreatorTabs: React.FC<ProductVariantCreatorTabsProps> = ({
step: currentStep,
onStepClick,
}) => {
const steps = useSteps();
return (
<ProductVariantCreatorSteps
currentStep={currentStep}
steps={steps}
onStepClick={onStepClick}
/>
);
};
ProductVariantCreatorTabs.displayName = "ProductVariantCreatorTabs";
export default ProductVariantCreatorTabs;

View file

@ -1,236 +0,0 @@
import { Card, CardContent } from "@material-ui/core";
import { getMeasurementUnitMessage } from "@saleor/attributes/components/AttributeDetails/utils";
import { getMultiChoices } from "@saleor/components/Attributes/utils";
import CardSpacer from "@saleor/components/CardSpacer";
import CardTitle from "@saleor/components/CardTitle";
import LimitReachedAlert from "@saleor/components/LimitReachedAlert";
import MultiAutocompleteSelectField from "@saleor/components/MultiAutocompleteSelectField";
import Skeleton from "@saleor/components/Skeleton";
import {
AttributeInputTypeEnum,
AttributeValueFragment,
ProductVariantAttributesFragment,
SearchAttributeValuesQuery,
} from "@saleor/graphql";
import { commonMessages } from "@saleor/intl";
import { getById } from "@saleor/orders/components/OrderReturnPage/utils";
import {
getBasicAttributeValue,
getBooleanAttributeValue,
} from "@saleor/products/components/ProductVariantCreatorPage/utils";
import { FetchMoreProps, RelayToFlat } from "@saleor/types";
import React from "react";
import {
defineMessages,
FormattedMessage,
IntlShape,
useIntl,
} from "react-intl";
import {
Attribute,
AttributeValue,
ProductVariantCreateFormData,
} from "./form";
const messages = defineMessages({
multipleValueLabel: {
id: "j8PV7E",
defaultMessage: "Values",
description: "attribute values",
},
});
export function getVariantsNumber(data: ProductVariantCreateFormData): number {
return data.attributes.reduce(
(variants, attribute) => variants * attribute.values.length,
1,
);
}
export function getMultiValues(
attributes: Attribute[],
attribute: ProductVariantAttributesFragment["productType"]["variantAttributes"][0],
) {
return attributes
.find(getById(attribute.id))
?.values?.map(value => value.slug);
}
export function getMultiDisplayValues(
attributes: Attribute[],
attribute: ProductVariantAttributesFragment["productType"]["variantAttributes"][0],
) {
return attributes.find(getById(attribute.id))?.values.map(value => ({
label: value.value?.name,
value: value.slug,
}));
}
const getBooleanDisplayValues = (
intl: IntlShape,
values: Array<AttributeValue<Partial<AttributeValueFragment>>>,
) => {
if (!values.length) {
return [];
}
const choices = getBooleanChoices(intl);
return values.map(({ value: { boolean } }) =>
choices.find(({ value }) => value === boolean),
);
};
const getBooleanChoices = (
intl: IntlShape,
values?: Array<AttributeValue<Partial<AttributeValueFragment>>>,
) => {
const selectedValues = values?.map(({ value }) => value.boolean) ?? [];
const choices = [
{ label: intl.formatMessage(commonMessages.yes), value: true },
{ label: intl.formatMessage(commonMessages.no), value: false },
];
return choices.filter(({ value }) => !selectedValues.includes(value));
};
export interface ProductVariantCreatorValuesProps {
attributes: ProductVariantAttributesFragment["productType"]["variantAttributes"];
attributeValues: RelayToFlat<
SearchAttributeValuesQuery["attribute"]["choices"]
>;
fetchAttributeValues: (query: string, attributeId: string) => void;
fetchMoreAttributeValues?: FetchMoreProps;
data: ProductVariantCreateFormData;
variantsLeft: number | null;
onValueClick: (
attributeId: string,
value: AttributeValue<Partial<AttributeValueFragment>>,
) => void;
onValueBlur: () => void;
}
const ProductVariantCreatorValues: React.FC<ProductVariantCreatorValuesProps> = props => {
const {
attributes,
attributeValues,
fetchAttributeValues,
fetchMoreAttributeValues,
data,
variantsLeft,
onValueClick,
onValueBlur,
} = props;
const intl = useIntl();
const variantsNumber = getVariantsNumber(data);
const handleValueClick = (
attributeId: string,
attributeName: string,
attributeValue: string | boolean,
) =>
onValueClick(
attributeId,
typeof attributeValue === "boolean"
? getBooleanAttributeValue(attributeName, attributeValue)
: getBasicAttributeValue(
attributeId,
attributeValue,
attributeValues,
data,
),
);
return (
<>
{variantsLeft !== null && variantsNumber > variantsLeft && (
<LimitReachedAlert
title={intl.formatMessage({
id: "FwHWUm",
defaultMessage: "SKU limit reached",
description: "alert",
})}
>
<FormattedMessage
id="Xr5zxu"
defaultMessage="You choices will add {variantsNumber} SKUs to your catalog which will exceed your limit by {aboveLimitVariantsNumber}. If you would like to up your limit, contact your administration staff about raising your limits."
values={{
variantsNumber,
aboveLimitVariantsNumber: variantsNumber - variantsLeft,
}}
/>
</LimitReachedAlert>
)}
{attributes.map(attribute => (
<React.Fragment key={attribute.id}>
<Card>
<CardTitle title={attribute?.name || <Skeleton />} />
<CardContent data-test-id="value-container">
{attribute.inputType === AttributeInputTypeEnum.BOOLEAN ? (
<MultiAutocompleteSelectField
displayValues={getBooleanDisplayValues(
intl,
data.attributes.find(({ id }) => id === attribute.id)
.values,
)}
name={`attribute:${attribute.name}`}
label={intl.formatMessage(messages.multipleValueLabel)}
value={getMultiValues(data.attributes, attribute)}
onChange={event =>
handleValueClick(
attribute.id,
attribute.name,
event.target.value,
)
}
allowCustomValues={false}
choices={getBooleanChoices(
intl,
data.attributes.find(({ id }) => id === attribute.id)
.values,
)}
/>
) : (
<MultiAutocompleteSelectField
choices={getMultiChoices(attributeValues)}
displayValues={getMultiDisplayValues(
data.attributes,
attribute,
)}
name={`attribute:${attribute.name}`}
label={intl.formatMessage(messages.multipleValueLabel)}
value={getMultiValues(data.attributes, attribute)}
onChange={event =>
handleValueClick(
attribute.id,
attribute.name,
event.target.value,
)
}
endAdornment={
attribute.unit &&
getMeasurementUnitMessage(
attribute.unit,
intl.formatMessage,
)
}
fetchOnFocus={true}
allowCustomValues={true}
fetchChoices={value =>
fetchAttributeValues(value, attribute.id)
}
onBlur={onValueBlur}
{...fetchMoreAttributeValues}
/>
)}
</CardContent>
</Card>
<CardSpacer />
</React.Fragment>
))}
</>
);
};
ProductVariantCreatorValues.displayName = "ProductVariantCreatorValues";
export default ProductVariantCreatorValues;

View file

@ -1,244 +0,0 @@
import {
createVariantFlatMatrixDimension,
createVariants,
} from "./createVariants";
import { attributes, channels, thirdStep } from "./fixtures";
import { ChannelPrice, 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 channels: ChannelPrice[] = [{ channelId: "1", price: "2" }];
const stock = [80, 40, 30];
const data: ProductVariantCreateFormData = {
...thirdStep,
price: {
...thirdStep.price,
channels,
mode: "all",
},
stock: {
...thirdStep.stock,
mode: "all",
value: stock,
},
};
const variants = createVariants(data);
expect(variants).toHaveLength(
thirdStep.attributes.reduce(
(acc, attribute) => acc * attribute.values.length,
1,
),
);
variants.forEach(variant => {
expect(variant.channelListings[0].price).toBe(channels[0].price);
variant.stocks.forEach((_, stockIndex) => {
expect(variant.stocks[stockIndex].quantity).toBe(stock[stockIndex]);
});
});
});
it("with constant stock and attribute dependent price", () => {
const stock = [80, 40, 30];
const attribute = attributes.find(
attribute => attribute.id === thirdStep.attributes[0].id,
);
const data: ProductVariantCreateFormData = {
...thirdStep,
price: {
...thirdStep.price,
attribute: attribute.id,
mode: "attribute",
values: attribute.values.map((attributeValue, index) => ({
slug: attributeValue.slug,
value: channels.map(channel => ({
channelId: channel.id,
price: (channel.price + index).toString(),
})),
})),
},
stock: {
...thirdStep.stock,
mode: "all",
value: stock,
},
};
const variants = createVariants(data);
expect(variants).toHaveLength(
thirdStep.attributes.reduce(
(acc, attribute) => acc * attribute.values.length,
1,
),
);
variants.forEach(variant => {
variant.stocks.forEach((_, stockIndex) => {
expect(variant.stocks[stockIndex].quantity).toBe(stock[stockIndex]);
});
});
attribute.values.forEach((attributeValue, attributeValueIndex) => {
variants
.filter(
variant =>
variant.attributes.find(
variantAttribute => variantAttribute.id === attribute.id,
).values[0] === attributeValue.slug,
)
.forEach(variant => {
variant.channelListings.map((channel, index) => {
expect(channel.price).toBe(
(channels[index].price + attributeValueIndex).toString(),
);
});
});
});
});
it("with constant price and attribute dependent stock", () => {
const price: ChannelPrice[] = [{ channelId: "1", price: "2" }];
const stock = [80, 40, 30];
const attribute = attributes.find(
attribute => attribute.id === thirdStep.attributes[0].id,
);
const data: ProductVariantCreateFormData = {
...thirdStep,
price: {
...thirdStep.price,
channels: price,
mode: "all",
},
stock: {
...thirdStep.stock,
attribute: attribute.id,
mode: "attribute",
values: attribute.values.map((attributeValue, attributeValueIndex) => ({
slug: attributeValue.slug,
value: stock.map(
(_, stockIndex) => stock[stockIndex] * (attributeValueIndex + 1),
),
})),
},
};
const variants = createVariants(data);
expect(variants).toHaveLength(
thirdStep.attributes.reduce(
(acc, attribute) => acc * attribute.values.length,
1,
),
);
variants.forEach(variant => {
expect(variant.channelListings).toBe(price);
});
attribute.values.forEach((attributeValue, attributeValueIndex) => {
variants
.filter(
variant =>
variant.attributes.find(
variantAttribute => variantAttribute.id === attribute.id,
).values[0] === attributeValue.slug,
)
.forEach(variant => {
variant.stocks.forEach((_, stockIndex) => {
expect(variant.stocks[stockIndex].quantity).toBe(
stock[stockIndex] * (attributeValueIndex + 1),
);
});
});
});
});
it("with attribute dependent price and stock", () => {
const stock = [80, 40, 30];
const attribute = attributes.find(
attribute => attribute.id === thirdStep.attributes[0].id,
);
const data: ProductVariantCreateFormData = {
...thirdStep,
price: {
...thirdStep.price,
attribute: attribute.id,
mode: "attribute",
values: attribute.values.map((attributeValue, index) => ({
slug: attributeValue.slug,
value: channels.map(channel => ({
channelId: channel.id,
price: (channel.price + index).toString(),
})),
})),
},
stock: {
...thirdStep.stock,
attribute: attribute.id,
mode: "attribute",
values: attribute.values.map((attributeValue, attributeValueIndex) => ({
slug: attributeValue.slug,
value: stock.map(
(_, stockIndex) => stock[stockIndex] * (attributeValueIndex + 1),
),
})),
},
};
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.slug,
)
.forEach(variant => {
variant.channelListings.map((channel, index) => {
expect(channel.price).toBe(
(channels[index].price + attributeValueIndex).toString(),
);
});
});
});
attribute.values.forEach((attributeValue, attributeValueIndex) => {
variants
.filter(
variant =>
variant.attributes.find(
variantAttribute => variantAttribute.id === attribute.id,
).values[0] === attributeValue.slug,
)
.forEach(variant => {
variant.stocks.forEach((_, stockIndex) => {
expect(variant.stocks[stockIndex].quantity).toBe(
stock[stockIndex] * (attributeValueIndex + 1),
);
});
});
});
});
});

View file

@ -1,187 +0,0 @@
import {
BulkAttributeValueInput,
ProductVariantBulkCreateInput,
} from "@saleor/graphql";
import {
Attribute,
ChannelPrice,
Price,
ProductVariantCreateFormData,
Stock,
} from "./form";
interface CreateVariantAttributeValueInput {
attributeId: string;
attributeValueSlug: string | null;
attributeBooleanValue: boolean | null;
}
type CreateVariantInput = CreateVariantAttributeValueInput[];
function findAttribute(
attributes: CreateVariantInput,
stockOrPrice: Stock | Price,
) {
return attributes.find(
attribute => attribute.attributeId === stockOrPrice.attribute,
);
}
function getAttributeValueStock(
attributes: CreateVariantInput,
stock: Stock,
): number[] {
const attribute = findAttribute(attributes, stock);
const attributeValue = stock.values.find(
attributeValue => attribute.attributeValueSlug === attributeValue.slug,
);
return attributeValue.value;
}
function getAttributeValuePrice(
attributes: CreateVariantInput,
price: Price,
): ChannelPrice[] {
const attribute = findAttribute(attributes, price);
const attributeValue = price.values.find(
attributeValue => attribute.attributeValueSlug === attributeValue.slug,
);
return attributeValue.value;
}
function getStockFromMode(
attributes: CreateVariantInput,
stock: Stock,
skipValue: number[],
): number[] {
switch (stock.mode) {
case "all":
return stock.value;
case "attribute":
return getAttributeValueStock(attributes, stock);
case "skip":
return skipValue;
}
}
function getPriceFromMode(
attributes: CreateVariantInput,
price: Price,
skipValue: ChannelPrice[],
): ChannelPrice[] {
switch (price.mode) {
case "all":
return price.channels;
case "attribute":
return getAttributeValuePrice(attributes, price);
case "skip":
return skipValue;
}
}
function getAttributeFromAttributeValueInput({
attributeId,
attributeBooleanValue,
attributeValueSlug,
}: CreateVariantAttributeValueInput): BulkAttributeValueInput {
if (attributeBooleanValue === null) {
return {
id: attributeId,
values: attributeValueSlug === null ? [] : [attributeValueSlug],
};
}
return {
id: attributeId,
boolean: attributeBooleanValue,
};
}
function createVariant(
data: ProductVariantCreateFormData,
attributes: CreateVariantInput,
): ProductVariantBulkCreateInput {
const price = getPriceFromMode(
attributes,
data.price,
data.price.channels.map(channel => ({ ...channel, price: "" })),
);
const stocks = getStockFromMode(
attributes,
data.stock,
data.warehouses.map(() => 0),
);
return {
attributes: attributes.map(getAttributeFromAttributeValueInput),
channelListings: price,
sku: "",
stocks: stocks.map((quantity, stockIndex) => ({
quantity,
warehouse: data.warehouses[stockIndex],
})),
};
}
function addAttributeToVariant(
attribute: Attribute,
variant: CreateVariantInput,
): CreateVariantInput[] {
if (attribute.values.length === 0) {
return [
[
...variant,
{
attributeId: attribute.id,
attributeValueSlug: null,
attributeBooleanValue: null,
},
],
];
}
return attribute.values.map(attributeValue => [
...variant,
{
attributeId: attribute.id,
attributeValueSlug: attributeValue.slug,
attributeBooleanValue: attributeValue.value?.boolean ?? null,
},
]);
}
export function createVariantFlatMatrixDimension(
variants: CreateVariantInput[],
attributes: Attribute[],
): CreateVariantInput[] {
if (attributes.length > 0) {
return createVariantFlatMatrixDimension(
variants.flatMap(variant =>
addAttributeToVariant(attributes[0], variant),
),
attributes.slice(1),
);
} else {
return variants;
}
}
export function createVariants(
data: ProductVariantCreateFormData,
): ProductVariantBulkCreateInput[] {
if (
(data.price.mode === "attribute" && !data.price.attribute) ||
(data.stock.mode === "attribute" && !data.stock.attribute)
) {
return [];
}
const variants = createVariantFlatMatrixDimension(
[[]],
data.attributes,
).map(variant => createVariant(data, variant));
return variants;
}

View file

@ -1,230 +0,0 @@
import { ChannelPriceData } from "@saleor/channels/utils";
import { WarehouseFragment } from "@saleor/graphql";
import { createVariants } from "./createVariants";
import {
createInitialForm,
Price,
ProductVariantCreateFormData,
Stock,
} from "./form";
export const channels: ChannelPriceData[] = [
{ currency: "USD", id: "channel-1", name: "Channel1", price: "1" },
{ currency: "USD", id: "channel-2", name: "Channel2", price: "2" },
{ currency: "USD", id: "channel-3", name: "Channel3", price: "3" },
];
export const attributes = [
{
id: "attr-1",
valueRequired: false,
values: Array(9)
.fill(0)
.map((_, index) => ({
slug: `val-1-${index + 1}`,
value: {
__typename: "AttributeValue" as "AttributeValue",
id: `val-1-${index + 1}`,
name: `val-1-${index + 1}`,
slug: `val-1-${index + 1}`,
file: null,
reference: null,
richText: null,
boolean: null,
date: null,
dateTime: null,
value: null,
},
})),
},
{
id: "attr-2",
valueRequired: false,
values: Array(6)
.fill(0)
.map((_, index) => ({
slug: `val-2-${index + 1}`,
value: {
__typename: "AttributeValue" as "AttributeValue",
id: `val-2-${index + 1}`,
name: `val-2-${index + 1}`,
slug: `val-2-${index + 1}`,
file: null,
reference: null,
richText: null,
boolean: null,
date: null,
dateTime: null,
value: null,
},
})),
},
{
id: "attr-3",
valueRequired: false,
values: Array(4)
.fill(0)
.map((_, index) => ({
slug: `val-3-${index + 1}`,
value: {
__typename: "AttributeValue" as "AttributeValue",
id: `val-3-${index + 1}`,
name: `val-3-${index + 1}`,
slug: `val-3-${index + 1}`,
file: null,
reference: null,
richText: null,
boolean: null,
date: null,
dateTime: null,
value: null,
},
})),
},
{
id: "attr-4",
valueRequired: false,
values: Array(11)
.fill(0)
.map((_, index) => ({
slug: `val-4-${index + 1}`,
value: {
__typename: "AttributeValue" as "AttributeValue",
id: `val-4-${index + 1}`,
name: `val-4-${index + 1}`,
slug: `val-4-${index + 1}`,
file: null,
reference: null,
richText: null,
boolean: null,
date: null,
dateTime: null,
value: null,
},
})),
},
];
export const warehouses: WarehouseFragment[] = [
{
__typename: "Warehouse",
id: "wh-1",
name: "Warehouse 1",
},
{
__typename: "Warehouse",
id: "wh-2",
name: "Warehouse 2",
},
{
__typename: "Warehouse",
id: "wh-3",
name: "Warehouse 3",
},
{
__typename: "Warehouse",
id: "wh-4",
name: "Warehouse 4",
},
];
export const secondStep: ProductVariantCreateFormData = {
...createInitialForm([], channels, warehouses),
attributes: [
{
id: attributes[0].id,
valueRequired: attributes[0].valueRequired,
values: [],
},
{
id: attributes[1].id,
valueRequired: attributes[1].valueRequired,
values: [],
},
{
id: attributes[3].id,
valueRequired: attributes[3].valueRequired,
values: [],
},
],
};
export const thirdStep: ProductVariantCreateFormData = {
...secondStep,
attributes: [
{
id: attributes[0].id,
valueRequired: attributes[0].valueRequired,
values: [0, 6].map(index => attributes[0].values[index]),
},
{
id: attributes[1].id,
valueRequired: attributes[1].valueRequired,
values: [1, 3].map(index => attributes[1].values[index]),
},
{
id: attributes[3].id,
valueRequired: attributes[3].valueRequired,
values: [0, 4].map(index => attributes[3].values[index]),
},
],
stock: {
...secondStep.stock,
value: warehouses.map(() => 0),
},
warehouses: warehouses.map(warehouse => warehouse.id),
};
const price: Price = {
attribute: thirdStep.attributes[1].id,
channels: [
{ channelId: channels[0].id, price: "0" },
{ channelId: channels[1].id, price: "2" },
{ channelId: channels[2].id, price: "2" },
],
mode: "attribute",
values: [
{
slug: thirdStep.attributes[1].values[0].slug,
value: [
{ channelId: channels[0].id, price: "0" },
{ channelId: channels[1].id, price: "2" },
{ channelId: channels[2].id, price: "2" },
],
},
{
slug: thirdStep.attributes[1].values[1].slug,
value: [
{ channelId: channels[0].id, price: "0" },
{ channelId: channels[1].id, price: "2" },
{ channelId: channels[2].id, price: "2" },
],
},
],
};
const stock: Stock = {
attribute: thirdStep.attributes[2].id,
mode: "attribute",
value: [],
values: [
{
slug: thirdStep.attributes[2].values[0].slug,
value: [50, 20, 45, 75],
},
{
slug: thirdStep.attributes[2].values[1].slug,
value: [80, 50, 85, 105],
},
],
};
export const fourthStep: ProductVariantCreateFormData = {
...thirdStep,
price,
stock,
variants: createVariants({
...thirdStep,
price,
stock,
}),
};

View file

@ -1,75 +0,0 @@
import { ChannelPriceData } from "@saleor/channels/utils";
import {
AttributeValueFragment,
ProductVariantAttributesFragment,
ProductVariantBulkCreateInput,
WarehouseFragment,
} from "@saleor/graphql";
export interface ChannelPrice {
channelId: string;
price: string;
}
export interface AttributeValue<T> {
slug: string;
value: T;
}
export type VariantCreatorPricesAndSkuMode = "all" | "attribute" | "skip";
export interface Price {
mode: VariantCreatorPricesAndSkuMode;
attribute: string;
channels: ChannelPrice[];
values: Array<AttributeValue<ChannelPrice[]>>;
}
export interface Stock {
mode: VariantCreatorPricesAndSkuMode;
attribute: string;
value: number[];
values: Array<AttributeValue<number[]>>;
}
export interface Attribute {
id: string;
valueRequired: boolean;
values: Array<AttributeValue<Partial<AttributeValueFragment>>>;
}
export interface ProductVariantCreateFormData {
attributes: Attribute[];
price: Price;
stock: Stock;
variants: ProductVariantBulkCreateInput[];
warehouses: string[];
}
export const createInitialForm = (
attributes: ProductVariantAttributesFragment["productType"]["variantAttributes"],
channels: ChannelPriceData[],
warehouses: WarehouseFragment[],
): ProductVariantCreateFormData => {
const channelListings =
channels?.map(channel => ({
channelId: channel.id,
price: channel.price?.toString() || "",
})) || [];
return {
attributes: attributes.map(attribute => ({
id: attribute.id,
valueRequired: attribute.valueRequired,
values: [],
})),
price: {
attribute: undefined,
channels: channelListings,
mode: "all",
values: [],
},
stock: {
attribute: undefined,
mode: "all",
value: warehouses.length === 1 ? [0] : [],
values: [],
},
variants: [],
warehouses: warehouses.length === 1 ? [warehouses[0].id] : [],
};
};

View file

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

View file

@ -1,299 +0,0 @@
import {
attributes,
channels,
fourthStep,
secondStep,
thirdStep,
warehouses,
} from "./fixtures";
import { ChannelPrice } from "./form";
import reducer, { ProductVariantCreateReducerActionType } 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, [
{
selectValue: {
attributeId: attributes[0].id,
value: attributes[0].values[0],
},
type: ProductVariantCreateReducerActionType.selectValue,
},
{
selectValue: {
attributeId: attributes[0].id,
value: attributes[0].values[6],
},
type: ProductVariantCreateReducerActionType.selectValue,
},
{
selectValue: {
attributeId: attributes[1].id,
value: attributes[1].values[1],
},
type: ProductVariantCreateReducerActionType.selectValue,
},
{
selectValue: {
attributeId: attributes[1].id,
value: attributes[1].values[3],
},
type: ProductVariantCreateReducerActionType.selectValue,
},
{
selectValue: {
attributeId: attributes[3].id,
value: attributes[3].values[0],
},
type: ProductVariantCreateReducerActionType.selectValue,
},
{
selectValue: {
attributeId: attributes[3].id,
value: attributes[3].values[4],
},
type: ProductVariantCreateReducerActionType.selectValue,
},
]);
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 price = "22.99";
const state = execActions(thirdStep, reducer, [
{
applyPriceOrStockToAll: {
mode: "all",
},
type: ProductVariantCreateReducerActionType.applyPriceToAll,
},
{
changeApplyPriceToAllValue: {
channelId: channels[0].id,
price,
},
type: ProductVariantCreateReducerActionType.changeApplyPriceToAllValue,
},
{
type: ProductVariantCreateReducerActionType.reload,
},
]);
expect(state.price.mode).toBe("all");
expect(state.price.channels[0].price).toBe(price);
expect(state).toMatchSnapshot();
});
it("select warehouses in which stock will be created", () => {
const state = execActions(thirdStep, reducer, [
{
changeWarehouses: {
warehouseId: warehouses[0].id,
},
type: ProductVariantCreateReducerActionType.changeWarehouses,
},
{
changeWarehouses: {
warehouseId: warehouses[2].id,
},
type: ProductVariantCreateReducerActionType.changeWarehouses,
},
]);
expect(state.warehouses).toHaveLength(2);
expect(state).toMatchSnapshot();
});
it("select stock for all variants", () => {
const quantity = 45;
const warehouseIndex = 1;
const state = execActions(thirdStep, reducer, [
{
applyPriceOrStockToAll: {
mode: "all",
},
type: ProductVariantCreateReducerActionType.applyStockToAll,
},
{
changeApplyStockToAllValue: {
quantity,
warehouseIndex,
},
type: ProductVariantCreateReducerActionType.changeApplyStockToAllValue,
},
{
type: ProductVariantCreateReducerActionType.reload,
},
]);
expect(state.stock.mode).toBe("all");
expect(state.stock.value[warehouseIndex]).toBe(quantity);
expect(state).toMatchSnapshot();
});
it("select price to each attribute value", () => {
const attribute = thirdStep.attributes[0];
const value = 45.99;
const state = execActions(thirdStep, reducer, [
{
applyPriceOrStockToAll: {
mode: "attribute",
},
type: ProductVariantCreateReducerActionType.applyPriceToAll,
},
{
changeApplyPriceOrStockToAttributeId: {
attributeId: attribute.id,
},
type:
ProductVariantCreateReducerActionType.changeApplyPriceToAttributeId,
},
{
changeAttributeValuePrice: {
channelId: channels[0].id,
price: value.toString(),
valueId: attribute.values[0].slug,
},
type: ProductVariantCreateReducerActionType.changeAttributeValuePrice,
},
{
changeAttributeValuePrice: {
channelId: channels[1].id,
price: (value + 6).toString(),
valueId: attribute.values[1].slug,
},
type: ProductVariantCreateReducerActionType.changeAttributeValuePrice,
},
{
type: ProductVariantCreateReducerActionType.reload,
},
]);
expect(state.price.mode).toBe("attribute");
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 quantity = 13;
const state = execActions(thirdStep, reducer, [
{
applyPriceOrStockToAll: {
mode: "attribute",
},
type: ProductVariantCreateReducerActionType.applyStockToAll,
},
{
changeApplyPriceOrStockToAttributeId: {
attributeId: attribute.id,
},
type:
ProductVariantCreateReducerActionType.changeApplyStockToAttributeId,
},
{
changeAttributeValueStock: {
quantity,
valueId: attribute.values[0].slug,
warehouseIndex: 0,
},
type: ProductVariantCreateReducerActionType.changeAttributeValueStock,
},
{
changeAttributeValueStock: {
quantity: quantity + 6,
valueId: attribute.values[1].slug,
warehouseIndex: 0,
},
type: ProductVariantCreateReducerActionType.changeAttributeValueStock,
},
{
type: ProductVariantCreateReducerActionType.reload,
},
]);
expect(state.stock.mode).toBe("attribute");
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 value: ChannelPrice = { channelId: channels[0].id, price: "7" };
const variantIndex = 3;
const state = execActions(fourthStep, reducer, [
{
changeVariantPriceData: {
value,
variantIndex,
},
type: ProductVariantCreateReducerActionType.changeVariantPriceData,
},
]);
expect(state.variants[variantIndex].channelListings[0].price).toBe(
value.price,
);
expect(state.variants[variantIndex - 1].channelListings).toBe(
fourthStep.variants[variantIndex - 1].channelListings,
);
expect(state).toMatchSnapshot();
});
it("modify individual variant stock", () => {
const quantity = 5;
const variantIndex = 3;
const state = execActions(fourthStep, reducer, [
{
changeVariantStockData: {
stock: {
quantity,
warehouse: warehouses[0].id,
},
variantIndex,
},
type: ProductVariantCreateReducerActionType.changeVariantStockData,
},
]);
expect(state.variants[variantIndex].stocks[0].quantity).toBe(quantity);
expect(state.variants[variantIndex - 1].stocks[0].quantity).toBe(
fourthStep.variants[variantIndex - 1].stocks[0].quantity,
);
expect(state).toMatchSnapshot();
});
it("delete variant", () => {
const variantIndex = 3;
const state = execActions(fourthStep, reducer, [
{
deleteVariant: {
variantIndex,
},
type: ProductVariantCreateReducerActionType.deleteVariant,
},
]);
expect(state.variants.length).toBe(fourthStep.variants.length - 1);
});
});

View file

@ -1,572 +0,0 @@
import {
AttributeValueFragment,
ProductVariantBulkCreateInput,
StockInput,
} from "@saleor/graphql";
import {
add,
remove,
removeAtIndex,
toggle,
update,
updateAtIndex,
} from "@saleor/utils/lists";
import { createVariants } from "./createVariants";
import {
AttributeValue,
ProductVariantCreateFormData,
VariantCreatorPricesAndSkuMode,
} from "./form";
export enum ProductVariantCreateReducerActionType {
applyPriceToAll,
applyPriceToAttribute,
applyStockToAll,
applyStockToAttribute,
changeApplyPriceToAllValue,
changeApplyPriceToAttributeId,
changeApplyStockToAllValue,
changeApplyStockToAttributeId,
changeAttributeValuePrice,
changeAttributeValueStock,
changeVariantSku,
changeVariantPriceData,
changeVariantStockData,
changeWarehouses,
deleteVariant,
reload,
rebuild,
selectValue,
}
export interface ProductVariantCreateReducerAction {
applyPriceOrStockToAll?: {
mode: VariantCreatorPricesAndSkuMode;
};
changeApplyPriceToAllValue?: {
channelId: string;
price: string;
};
changeApplyPriceOrStockToAttributeId?: {
attributeId: string;
};
changeApplyStockToAllValue?: Record<"quantity" | "warehouseIndex", number>;
changeAttributeValuePrice?: Record<"valueId" | "price" | "channelId", string>;
changeAttributeValueStock?: {
valueId: string;
quantity: number;
warehouseIndex: number;
};
changeVariantSku?: {
value: string;
variantIndex: number;
};
changeVariantPriceData?: {
value: { channelId: string; price: string };
variantIndex: number;
};
changeVariantStockData?: {
stock: StockInput;
variantIndex: number;
};
changeWarehouses?: {
warehouseId: string;
};
deleteVariant?: {
variantIndex: number;
};
reload?: {
data?: ProductVariantCreateFormData;
};
selectValue?: {
attributeId: string;
value: AttributeValue<Partial<AttributeValueFragment>>;
};
type: ProductVariantCreateReducerActionType;
}
function getVariantId(variant: ProductVariantBulkCreateInput): string {
return variant.attributes
.map(attribute => attribute.values.join(":"))
.join("-");
}
function merge(
prev: ProductVariantBulkCreateInput[],
update: ProductVariantBulkCreateInput[],
): ProductVariantBulkCreateInput[] {
const prevIds = prev.map(getVariantId);
const updateIds = update.map(getVariantId);
return [
...prev
.filter(variant => updateIds.includes(getVariantId(variant)))
.map(variant => {
const updatedVariant = update.find(
v => getVariantId(v) === getVariantId(variant),
);
return {
...updatedVariant,
sku: variant.sku,
};
}),
...update.filter(variant => !prevIds.includes(getVariantId(variant))),
];
}
function rebuild(
state: ProductVariantCreateFormData,
): ProductVariantCreateFormData {
return {
...state,
variants: merge(state.variants, createVariants(state)),
};
}
function selectValue(
prevState: ProductVariantCreateFormData,
attributeId: string,
value: AttributeValue<Partial<AttributeValueFragment>>,
): ProductVariantCreateFormData {
const attribute = prevState.attributes.find(
attribute => attribute.id === attributeId,
);
const values = toggle(value, attribute.values, (a, b) => a.slug === b.slug);
const updatedAttributes = add(
{
id: attributeId,
valueRequired: attribute.valueRequired,
values,
},
remove(attribute, prevState.attributes, (a, b) => a.id === b.id),
);
const priceValues =
prevState.price.attribute === attributeId
? toggle(
{
slug: value.slug,
value: [],
},
prevState.price.values,
(a, b) => a.slug === b.slug,
)
: prevState.price.values;
const stockValues =
prevState.stock.attribute === attributeId
? toggle(
{
slug: value.slug,
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,
mode: VariantCreatorPricesAndSkuMode,
): ProductVariantCreateFormData {
return {
...state,
price: {
...state.price,
mode,
},
};
}
function applyStockToAll(
state: ProductVariantCreateFormData,
mode: VariantCreatorPricesAndSkuMode,
): ProductVariantCreateFormData {
return {
...state,
stock: {
...state.stock,
mode,
},
};
}
function changeAttributeValuePrice(
state: ProductVariantCreateFormData,
attributeValueSlug: string,
price: string,
channelId: 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 channels = state.price.values[index].value;
const channelIndex = channels.findIndex(
channel => channel.channelId === channelId,
);
const values = updateAtIndex(
{
slug: attributeValueSlug,
value: updateAtIndex({ channelId, price }, channels, channelIndex),
},
state.price.values,
index,
);
return {
...state,
price: {
...state.price,
values,
},
};
}
function changeAttributeValueStock(
state: ProductVariantCreateFormData,
attributeValueSlug: string,
quantity: number,
warehouseIndex: number,
): 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(
{
...state.stock.values[index],
value: updateAtIndex(
quantity,
state.stock.values[index].value,
warehouseIndex,
),
},
state.stock.values,
index,
);
return {
...state,
stock: {
...state.stock,
values,
},
};
}
function changeApplyPriceToAttributeId(
state: ProductVariantCreateFormData,
attributeId: string,
): ProductVariantCreateFormData {
const attribute = state.attributes.find(
attribute => attribute.id === attributeId,
);
const values = attribute.values.map(value => ({
slug: value.slug,
value: [],
}));
return {
...state,
price: {
...state.price,
attribute: attributeId,
values,
},
};
}
function changeApplyStockToAttributeId(
state: ProductVariantCreateFormData,
attributeId: string,
): ProductVariantCreateFormData {
const attribute = state.attributes.find(
attribute => attribute.id === attributeId,
);
const values = attribute.values.map(value => ({
slug: value.slug,
value: [],
}));
return {
...state,
stock: {
...state.stock,
attribute: attributeId,
values,
},
};
}
function changeApplyPriceToAllValue(
state: ProductVariantCreateFormData,
channelId: string,
price: string,
): ProductVariantCreateFormData {
const prevChannels = [...state.price.channels];
const channelIndex = prevChannels?.findIndex(
channel => channelId === channel.channelId,
);
prevChannels[channelIndex] = { channelId, price };
return {
...state,
price: {
...state.price,
channels: prevChannels,
},
};
}
function changeApplyStockToAllValue(
state: ProductVariantCreateFormData,
warehouseIndex: number,
quantity: number,
): ProductVariantCreateFormData {
return {
...state,
stock: {
...state.stock,
value: updateAtIndex(quantity, state.stock.value, warehouseIndex),
},
};
}
function changeVariantSku(
state: ProductVariantCreateFormData,
value: string,
variantIndex: number,
): ProductVariantCreateFormData {
const variant = {
...state.variants[variantIndex],
};
variant.sku = value;
return {
...state,
variants: updateAtIndex(variant, state.variants, variantIndex),
};
}
function changeVariantStockData(
state: ProductVariantCreateFormData,
stock: StockInput,
variantIndex: number,
): ProductVariantCreateFormData {
const variant = {
...state.variants[variantIndex],
};
variant.stocks = update(
stock,
variant.stocks,
(a, b) => a.warehouse === b.warehouse,
);
return {
...state,
variants: updateAtIndex(variant, state.variants, variantIndex),
};
}
function changeVariantPriceData(
state: ProductVariantCreateFormData,
value: { channelId: string; price: string },
variantIndex: number,
): ProductVariantCreateFormData {
const { channelId, price } = value;
const variant = {
...state.variants[variantIndex],
};
const channelIndex = variant.channelListings.findIndex(
listing => listing.channelId === channelId,
);
const updatedVariant = {
...variant,
channelListings: updateAtIndex(
{ channelId, price },
[...variant.channelListings],
channelIndex,
),
};
return {
...state,
variants: updateAtIndex(updatedVariant, [...state.variants], variantIndex),
};
}
function changeWarehouses(
state: ProductVariantCreateFormData,
warehouseId: string,
): ProductVariantCreateFormData {
const warehouses = toggle(warehouseId, state.warehouses, (a, b) => a === b);
const added = warehouses.length > state.warehouses.length;
if (added) {
return {
...state,
stock: {
...state.stock,
value: [...state.stock.value, 0],
values: state.stock.values.map(stockValue => ({
...stockValue,
value: [...stockValue.value, 0],
})),
},
warehouses,
};
}
const warehouseIndex = state.warehouses.indexOf(warehouseId);
return {
...state,
stock: {
...state.stock,
value: removeAtIndex(state.stock.value, warehouseIndex),
values: state.stock.values.map(stockValue => ({
...stockValue,
value: removeAtIndex(stockValue.value, warehouseIndex),
})),
},
warehouses,
};
}
function deleteVariant(
state: ProductVariantCreateFormData,
variantIndex: number,
): ProductVariantCreateFormData {
const variants = removeAtIndex(state.variants, variantIndex);
return {
...state,
variants: variants.length
? variants
: createVariants({
...state,
attributes: state.attributes.map(attribute => ({
id: attribute.id,
valueRequired: attribute.valueRequired,
values: [],
})),
variants,
}),
};
}
function createVariantMatrix(
state: ProductVariantCreateFormData,
): ProductVariantCreateFormData {
return {
...state,
variants: createVariants(state),
};
}
function reduceProductVariantCreateFormData(
prevState: ProductVariantCreateFormData,
action: ProductVariantCreateReducerAction,
) {
switch (action.type) {
case ProductVariantCreateReducerActionType.selectValue:
return selectValue(
prevState,
action.selectValue.attributeId,
action.selectValue.value,
);
case ProductVariantCreateReducerActionType.applyPriceToAll:
return applyPriceToAll(prevState, action.applyPriceOrStockToAll.mode);
case ProductVariantCreateReducerActionType.applyStockToAll:
return applyStockToAll(prevState, action.applyPriceOrStockToAll.mode);
case ProductVariantCreateReducerActionType.changeAttributeValuePrice:
return changeAttributeValuePrice(
prevState,
action.changeAttributeValuePrice.valueId,
action.changeAttributeValuePrice.price,
action.changeAttributeValuePrice.channelId,
);
case ProductVariantCreateReducerActionType.changeAttributeValueStock:
return changeAttributeValueStock(
prevState,
action.changeAttributeValueStock.valueId,
action.changeAttributeValueStock.quantity,
action.changeAttributeValueStock.warehouseIndex,
);
case ProductVariantCreateReducerActionType.changeApplyPriceToAttributeId:
return changeApplyPriceToAttributeId(
prevState,
action.changeApplyPriceOrStockToAttributeId.attributeId,
);
case ProductVariantCreateReducerActionType.changeApplyStockToAttributeId:
return changeApplyStockToAttributeId(
prevState,
action.changeApplyPriceOrStockToAttributeId.attributeId,
);
case ProductVariantCreateReducerActionType.changeApplyPriceToAllValue:
return changeApplyPriceToAllValue(
prevState,
action.changeApplyPriceToAllValue.channelId,
action.changeApplyPriceToAllValue.price,
);
case ProductVariantCreateReducerActionType.changeApplyStockToAllValue:
return changeApplyStockToAllValue(
prevState,
action.changeApplyStockToAllValue.warehouseIndex,
action.changeApplyStockToAllValue.quantity,
);
case ProductVariantCreateReducerActionType.changeVariantSku:
return changeVariantSku(
prevState,
action.changeVariantSku.value,
action.changeVariantSku.variantIndex,
);
case ProductVariantCreateReducerActionType.changeVariantPriceData:
return changeVariantPriceData(
prevState,
action.changeVariantPriceData.value,
action.changeVariantPriceData.variantIndex,
);
case ProductVariantCreateReducerActionType.changeVariantStockData:
return changeVariantStockData(
prevState,
action.changeVariantStockData.stock,
action.changeVariantStockData.variantIndex,
);
case ProductVariantCreateReducerActionType.changeWarehouses:
return changeWarehouses(prevState, action.changeWarehouses.warehouseId);
case ProductVariantCreateReducerActionType.deleteVariant:
return deleteVariant(prevState, action.deleteVariant.variantIndex);
case ProductVariantCreateReducerActionType.reload:
return action.reload?.data || createVariantMatrix(prevState);
case ProductVariantCreateReducerActionType.rebuild:
return rebuild(prevState);
default:
return prevState;
}
}
export default reduceProductVariantCreateFormData;

View file

@ -1,5 +0,0 @@
export enum ProductVariantCreatorStep {
values,
prices,
summary,
}

View file

@ -1,94 +0,0 @@
import {
AttributeValueFragment,
ProductVariantAttributesFragment,
SearchAttributeValuesQuery,
} from "@saleor/graphql";
import { getById } from "@saleor/orders/components/OrderReturnPage/utils";
import { RelayToFlat } from "@saleor/types";
import uniqBy from "lodash/uniqBy";
import { AttributeValue, ProductVariantCreateFormData } from "./form";
export function getPriceAttributeValues(
data: ProductVariantCreateFormData,
attributes: ProductVariantAttributesFragment["productType"]["variantAttributes"],
): AttributeValueFragment[] {
return data.price.mode === "all"
? null
: data.price.attribute
? attributes
.find(attribute => attribute.id === data.price.attribute)
.choices.edges.filter(value =>
data.attributes
.find(attribute => attribute.id === data.price.attribute)
.values.some(
attributeValue => attributeValue.slug === value.node.slug,
),
)
.map(value => value.node)
: [];
}
export function getStockAttributeValues(
data: ProductVariantCreateFormData,
attributes: ProductVariantAttributesFragment["productType"]["variantAttributes"],
): AttributeValueFragment[] {
return data.stock.mode === "all"
? null
: data.stock.attribute
? attributes
.find(attribute => attribute.id === data.stock.attribute)
.choices.edges.filter(value =>
data.attributes
.find(attribute => attribute.id === data.stock.attribute)
.values.some(
attributeValue => attributeValue.slug === value.node.slug,
),
)
.map(value => value.node)
: [];
}
export const getBySlug = (slugToCompare: string) => (obj: { slug: string }) =>
obj.slug === slugToCompare;
export const getBooleanAttributeValue = (
attributeName: string,
attributeValue: boolean,
): AttributeValue<Partial<AttributeValueFragment>> => ({
slug: attributeValue.toString(),
value: {
boolean: attributeValue,
name: `${attributeName}: ${attributeValue ? "Yes" : "No"}`,
},
});
export const getBasicAttributeValue = (
attributeId: string,
attributeValue: string,
attributeValues: RelayToFlat<
SearchAttributeValuesQuery["attribute"]["choices"]
>,
data: ProductVariantCreateFormData,
): AttributeValue<Partial<AttributeValueFragment>> => {
const dataAttribute = data.attributes.find(getById(attributeId));
return {
slug: attributeValue,
value:
dataAttribute?.values.find(getBySlug(attributeValue))?.value ||
attributeValues.find(getBySlug(attributeValue)),
};
};
export function dedupeListings(
data: ProductVariantCreateFormData,
): ProductVariantCreateFormData {
return {
...data,
variants: data.variants.map(variant => ({
...variant,
channelListings: uniqBy(variant.channelListings, "channelId"),
})),
};
}

View file

@ -225,7 +225,6 @@ interface ProductVariantsProps extends ListActions, ChannelProps {
variants: ProductDetailsVariantFragment[];
onVariantReorder: ReorderAction;
onSetDefaultVariant(variant: ProductDetailsVariantFragment[][0]);
onVariantsAdd?();
}
const numberOfColumns = 7;
@ -237,7 +236,6 @@ export const ProductVariants: React.FC<ProductVariantsProps> = props => {
limits,
variants,
product,
onVariantsAdd,
onVariantReorder,
onSetDefaultVariant,
isChecked,
@ -263,33 +261,18 @@ export const ProductVariants: React.FC<ProductVariantsProps> = props => {
description: "section header",
})}
toolbar={
hasVariants ? (
<Button
disabled={limitReached}
href={productVariantAddUrl(productId)}
variant="tertiary"
data-test-id="button-add-variant"
>
<FormattedMessage
id="3VyHbJ"
defaultMessage="Create variant"
description="button"
/>
</Button>
) : (
<Button
disabled={limitReached}
onClick={onVariantsAdd}
variant="tertiary"
data-test-id="button-add-variants"
>
<FormattedMessage
id="HcjV6k"
defaultMessage="Create variants"
description="button"
/>
</Button>
)
<Button
disabled={limitReached}
href={productVariantAddUrl(productId)}
variant="tertiary"
data-test-id="button-add-variant"
>
<FormattedMessage
id="3VyHbJ"
defaultMessage="Create variant"
description="button"
/>
</Button>
}
/>

View file

@ -19,7 +19,6 @@ import {
ProductUrlQueryParams,
productVariantAddPath,
ProductVariantAddUrlQueryParams,
productVariantCreatorPath,
productVariantEditPath,
ProductVariantEditUrlQueryParams,
} from "./urls";
@ -29,7 +28,6 @@ import ProductListComponent from "./views/ProductList";
import ProductUpdateComponent from "./views/ProductUpdate";
import ProductVariantComponent from "./views/ProductVariant";
import ProductVariantCreateComponent from "./views/ProductVariantCreate";
import ProductVariantCreatorComponent from "./views/ProductVariantCreator";
const ProductList: React.FC<RouteComponentProps<any>> = ({ location }) => {
const qs = parseQs(location.search.substr(1));
@ -113,12 +111,6 @@ const ProductVariantCreate: React.FC<RouteComponentProps<any>> = ({
);
};
const ProductVariantCreator: React.FC<RouteComponentProps<{
id: string;
}>> = ({ match }) => (
<ProductVariantCreatorComponent id={decodeURIComponent(match.params.id)} />
);
const Component = () => {
const intl = useIntl();
@ -128,10 +120,6 @@ const Component = () => {
<Switch>
<Route exact path={productListPath} component={ProductList} />
<Route exact path={productAddPath} component={ProductCreate} />
<Route
path={productVariantCreatorPath(":id")}
component={ProductVariantCreator}
/>
<Route
exact
path={productVariantAddPath(":id")}

View file

@ -382,19 +382,6 @@ export const productBulkDeleteMutation = gql`
}
`;
export const ProductVariantBulkCreateMutation = gql`
mutation ProductVariantBulkCreate(
$id: ID!
$inputs: [ProductVariantBulkCreateInput!]!
) {
productVariantBulkCreate(product: $id, variants: $inputs) {
errors {
...BulkProductError
}
}
}
`;
export const ProductVariantBulkDeleteMutation = gql`
mutation ProductVariantBulkDelete($ids: [ID!]!) {
productVariantBulkDelete(ids: $ids) {

View file

@ -260,24 +260,3 @@ export const gridAttributes = gql`
}
}
`;
export const createMultipleVariantsData = gql`
query CreateMultipleVariantsData(
$id: ID!
$firstValues: Int
$afterValues: String
$lastValues: Int
$beforeValues: String
) {
product(id: $id) {
...ProductVariantAttributes
}
warehouses(first: 20) {
edges {
node {
...Warehouse
}
}
}
}
`;

View file

@ -74,7 +74,6 @@ export const productListUrl = (params?: ProductListUrlQueryParams): string =>
export const productPath = (id: string) => urlJoin(productSection + id);
export type ProductUrlDialog =
| "add-variants"
| "remove"
| "remove-variants"
| "assign-attribute-value"
@ -107,11 +106,6 @@ export const productVariantEditUrl = (
"?" +
stringifyQs(params);
export const productVariantCreatorPath = (productId: string) =>
urlJoin(productSection, productId, "variant-creator");
export const productVariantCreatorUrl = (productId: string) =>
productVariantCreatorPath(encodeURIComponent(productId));
export type ProductVariantAddUrlDialog = "assign-attribute-value";
export type ProductVariantAddUrlQueryParams = Dialog<
ProductVariantAddUrlDialog

View file

@ -49,7 +49,6 @@ import useShop from "@saleor/hooks/useShop";
import useStateFromProps from "@saleor/hooks/useStateFromProps";
import { commonMessages, errorMessages } from "@saleor/intl";
import { DeleteIcon, IconButton } from "@saleor/macaw-ui";
import ProductVariantCreateDialog from "@saleor/products/components/ProductVariantCreateDialog";
import ProductVariantEndPreorderDialog from "@saleor/products/components/ProductVariantEndPreorderDialog";
import useCategorySearch from "@saleor/searches/useCategorySearch";
import useCollectionSearch from "@saleor/searches/useCollectionSearch";
@ -71,8 +70,6 @@ import {
productUrl,
ProductUrlDialog,
ProductUrlQueryParams,
productVariantAddUrl,
productVariantCreatorUrl,
} from "../../urls";
import {
CHANNELS_AVAILIABILITY_MODAL_SELECTOR,
@ -401,9 +398,6 @@ export const ProductUpdate: React.FC<ProductUpdateProps> = ({ id, params }) => {
const handleBack = () => navigate(productListUrl());
const handleVariantAdd = () => navigate(productVariantAddUrl(id));
const handleVariantsAdd = () => navigate(productVariantCreatorUrl(id));
const handleImageDelete = (id: string) => () =>
deleteProductImage({ variables: { id } });
@ -598,7 +592,6 @@ export const ProductUpdate: React.FC<ProductUpdateProps> = ({ id, params }) => {
onMediaUrlUpload={handleMediaUrlUpload}
onSubmit={handleSubmit}
onWarehouseConfigure={() => navigate(warehouseAddPath)}
onVariantsAdd={() => openModal("add-variants")}
onVariantReorder={handleVariantReorder}
onVariantEndPreorderDialogOpen={() => setIsEndPreorderModalOpened(true)}
onImageUpload={handleImageUpload}
@ -678,13 +671,6 @@ export const ProductUpdate: React.FC<ProductUpdateProps> = ({ id, params }) => {
/>
</DialogContentText>
</ActionDialog>
<ProductVariantCreateDialog
open={params.action === "add-variants"}
onClose={closeModal}
onConfirm={option =>
option === "multiple" ? handleVariantsAdd() : handleVariantAdd()
}
/>
{isSimpleProduct && !!product?.variants?.[0]?.preorder && (
<ProductVariantEndPreorderDialog
confirmButtonState={deactivatePreoderOpts.status}

View file

@ -1,113 +0,0 @@
import { useShopLimitsQuery } from "@saleor/components/Shop/queries";
import { WindowTitle } from "@saleor/components/WindowTitle";
import { DEFAULT_INITIAL_SEARCH_DATA } from "@saleor/config";
import {
useCreateMultipleVariantsDataQuery,
useProductVariantBulkCreateMutation,
} from "@saleor/graphql";
import useNavigator from "@saleor/hooks/useNavigator";
import useNotifier from "@saleor/hooks/useNotifier";
import { productUrl } from "@saleor/products/urls";
import useAttributeValueSearchHandler from "@saleor/utils/handlers/attributeValueSearchHandler";
import { mapEdgesToItems } from "@saleor/utils/maps";
import React from "react";
import { useIntl } from "react-intl";
import ProductVariantCreatorPage from "../../components/ProductVariantCreatorPage";
interface ProductVariantCreatorProps {
id: string;
}
const ProductVariantCreator: React.FC<ProductVariantCreatorProps> = ({
id,
}) => {
const navigate = useNavigator();
const notify = useNotifier();
const intl = useIntl();
const { data } = useCreateMultipleVariantsDataQuery({
displayLoader: true,
variables: {
id,
firstValues: 10,
},
});
const [
bulkProductVariantCreate,
bulkProductVariantCreateOpts,
] = useProductVariantBulkCreateMutation({
onCompleted: data => {
if (data.productVariantBulkCreate.errors.length === 0) {
notify({
status: "success",
text: intl.formatMessage({
id: "oChkS4",
defaultMessage: "Successfully created variants",
description: "success message",
}),
});
navigate(productUrl(id));
}
},
});
const limitOpts = useShopLimitsQuery({
variables: {
productVariants: true,
},
});
const {
loadMore: loadMoreAttributeValues,
search: searchAttributeValues,
reset: searchAttributeReset,
result: searchAttributeValuesOpts,
} = useAttributeValueSearchHandler(DEFAULT_INITIAL_SEARCH_DATA);
const fetchMoreAttributeValues = {
hasMore: !!searchAttributeValuesOpts.data?.attribute?.choices?.pageInfo
?.hasNextPage,
loading: !!searchAttributeValuesOpts.loading,
onFetchMore: loadMoreAttributeValues,
};
const attributeValues =
mapEdgesToItems(searchAttributeValuesOpts?.data?.attribute.choices) || [];
return (
<>
<WindowTitle
title={intl.formatMessage({
id: "z+wMgQ",
defaultMessage: "Create Variants",
description: "window title",
})}
/>
<ProductVariantCreatorPage
errors={
bulkProductVariantCreateOpts.data?.productVariantBulkCreate.errors ||
[]
}
channelListings={data?.product?.channelListings?.map(listing => ({
currency: listing.channel.currencyCode,
id: listing.channel.id,
name: listing.channel.name,
price: "",
}))}
attributes={data?.product?.productType?.variantAttributes || []}
attributeValues={attributeValues}
fetchAttributeValues={searchAttributeValues}
fetchMoreAttributeValues={fetchMoreAttributeValues}
limits={limitOpts.data?.shop?.limits}
onSubmit={inputs =>
bulkProductVariantCreate({
variables: { id, inputs },
})
}
onAttributeSelectBlur={searchAttributeReset}
warehouses={mapEdgesToItems(data?.warehouses) || []}
/>
</>
);
};
ProductVariantCreator.displayName = "ProductVariantCreator";
export default ProductVariantCreator;

View file

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

File diff suppressed because it is too large Load diff

View file

@ -65,7 +65,6 @@ const props: ProductUpdatePageProps = {
onSetDefaultVariant: () => undefined,
onSubmit: () => undefined,
onVariantReorder: () => undefined,
onVariantsAdd: () => undefined,
onVariantEndPreorderDialogOpen: () => undefined,
onWarehouseConfigure: () => undefined,
openChannelsModal: () => undefined,