diff --git a/cypress/e2e/products/productsVariants.js b/cypress/e2e/products/productsVariants.js index 0e86d8c79..6e122c3a0 100644 --- a/cypress/e2e/products/productsVariants.js +++ b/cypress/e2e/products/productsVariants.js @@ -3,6 +3,9 @@ import faker from "faker"; +import { PRODUCT_DETAILS } from "../../elements/catalog/products/product-details"; +import { VARIANTS_SELECTORS } from "../../elements/catalog/products/variants-selectors"; +import { BUTTON_SELECTORS } from "../../elements/shared/button-selectors"; import { urlList } from "../../fixtures/urlList"; import { ONE_PERMISSION_USERS } from "../../fixtures/users"; import { createChannel } from "../../support/api/requests/Channels"; @@ -12,11 +15,7 @@ import { } from "../../support/api/requests/Product"; import * as productUtils from "../../support/api/utils/products/productsUtils"; import { getProductVariants } from "../../support/api/utils/storeFront/storeFrontProductUtils"; -import { - createVariant, - variantsShouldBeVisible, -} from "../../support/pages/catalog/products/VariantsPage"; -import { selectChannelInHeader } from "../../support/pages/channelsPage"; +import { createVariant } from "../../support/pages/catalog/products/VariantsPage"; describe("As an admin I should be able to create variant", () => { const startsWith = "CyCreateVariants-"; @@ -43,7 +42,7 @@ describe("As an admin I should be able to create variant", () => { defaultChannel = resp.defaultChannel; warehouse = resp.warehouse; - createChannel({ isActive: true, name, currencyCode: "USD" }); + createChannel({ isActive: true, name, currencyCode: "PLN" }); }) .then(resp => (newChannel = resp)); }); @@ -71,6 +70,7 @@ describe("As an admin I should be able to create variant", () => { }) .then(resp => { createdProduct = resp; + updateChannelInProduct({ productId: createdProduct.id, channelId: defaultChannel.id, @@ -79,32 +79,52 @@ describe("As an admin I should be able to create variant", () => { productId: createdProduct.id, channelId: newChannel.id, }); - cy.visit(`${urlList.products}${createdProduct.id}`); + cy.visit(`${urlList.products}${createdProduct.id}`) + .waitForProgressBarToNotBeVisible() + .get(PRODUCT_DETAILS.addVariantButton) + .should("exist") + .click() + .get(PRODUCT_DETAILS.dataGridTable) + .should("be.visible") + .get(PRODUCT_DETAILS.firstRowDataGrid) + .click({ force: true }) + .type(name) + .get(BUTTON_SELECTORS.confirm) + .click() + .confirmationMessageShouldAppear() + .reload() + .waitForProgressBarToNotBeVisible() + .get(PRODUCT_DETAILS.dataGridTable) + .should("be.visible") + .wait(1000) + .get(BUTTON_SELECTORS.showMoreButton) + .click() + .get(PRODUCT_DETAILS.editVariant) + .click() + .get(VARIANTS_SELECTORS.manageChannels) + .click() + .get(VARIANTS_SELECTORS.allChannels) + .check() + .get(BUTTON_SELECTORS.submit) + .click(); createVariant({ channelName: [defaultChannel.name, newChannel.name], sku: name, price, attributeName: attributeValues[0], }); - selectChannelInHeader(defaultChannel.name); - variantsShouldBeVisible({ name, price }); getProductVariants(createdProduct.id, defaultChannel.slug); }) .then(([variant]) => { - expect(variant).to.have.property("name", attributeValues[0]); - expect(variant).to.have.property("price", price); - selectChannelInHeader(newChannel.name); - variantsShouldBeVisible({ name, price }); - getProductVariants(createdProduct.id, defaultChannel.slug); - }) - .then(([variant]) => { - expect(variant).to.have.property("name", attributeValues[0]); + expect(variant).to.have.property("name", name); expect(variant).to.have.property("price", price); + expect(variant).to.have.property("currency", "USD"); getProductVariants(createdProduct.id, newChannel.slug); }) .then(([variant]) => { - expect(variant).to.have.property("name", attributeValues[0]); + expect(variant).to.have.property("name", name); expect(variant).to.have.property("price", price); + expect(variant).to.have.property("currency", "PLN"); }); }, ); @@ -130,26 +150,34 @@ describe("As an admin I should be able to create variant", () => { }) .then(({ product: productResp }) => { createdProduct = productResp; - cy.visit(`${urlList.products}${createdProduct.id}`); - createVariant({ - sku: secondVariantSku, - attributeName: variants[1].name, - price: variants[1].price, - channelName: defaultChannel.name, - }); - }) - .then(() => { - selectChannelInHeader(defaultChannel.name); - variantsShouldBeVisible({ - name: variants[1].name, - price: variants[1].price, - }); - getProductVariants(createdProduct.id, defaultChannel.slug); - }) - .then(([firstVariant, secondVariant]) => { - expect(firstVariant).to.have.property("price", variants[0].price); - expect(secondVariant).to.have.property("name", variants[1].name); - expect(secondVariant).to.have.property("price", variants[1].price); + + cy.visit(`${urlList.products}${createdProduct.id}`) + .get(BUTTON_SELECTORS.showMoreButton) + .click() + .get(PRODUCT_DETAILS.editVariant) + .click() + .get(PRODUCT_DETAILS.addVariantButton) + .click() + .then(() => { + createVariant({ + sku: secondVariantSku, + attributeName: variants[1].name, + price: variants[1].price, + channelName: defaultChannel.name, + }); + getProductVariants(createdProduct.id, defaultChannel.slug); + }) + .then(([firstVariant, secondVariant]) => { + expect(firstVariant).to.have.property("price", variants[0].price); + expect(firstVariant).to.have.property("name", "value"); + expect(firstVariant).to.have.property("currency", "USD"); + expect(secondVariant).to.have.property("name", "value2"); + expect(secondVariant).to.have.property( + "price", + variants[1].price, + ); + expect(secondVariant).to.have.property("currency", "USD"); + }); }); }, ); diff --git a/cypress/elements/catalog/products/product-details.js b/cypress/elements/catalog/products/product-details.js index b69971dfc..830fb4850 100644 --- a/cypress/elements/catalog/products/product-details.js +++ b/cypress/elements/catalog/products/product-details.js @@ -25,4 +25,7 @@ export const PRODUCT_DETAILS = { uploadSavedImagesButton: '[data-test-id="upload-images"]', uploadMediaUrlButton: '[data-test-id="upload-media-url"]', saveUploadUrlButton: '[data-test-id="upload-url-button"]', + editVariant: '[id="menu-list-grow"]', + firstRowDataGrid: "[data-testid='glide-cell-1-0']", + dataGridTable: "[data-testid='data-grid-canvas']", }; diff --git a/cypress/elements/catalog/products/variants-selectors.js b/cypress/elements/catalog/products/variants-selectors.js index 6082a6c8a..a123a80f7 100644 --- a/cypress/elements/catalog/products/variants-selectors.js +++ b/cypress/elements/catalog/products/variants-selectors.js @@ -17,4 +17,6 @@ export const VARIANTS_SELECTORS = { globalThresholdInput: "[name='globalThreshold']", stockInput: "[data-test-id='stock-input']", selectOption: "[data-test-id='multi-autocomplete-select-option']", + manageChannels: "[data-testid='manage-channels-button']", + allChannels: "[name='allChannels']", }; diff --git a/cypress/support/api/utils/storeFront/storeFrontProductUtils.js b/cypress/support/api/utils/storeFront/storeFrontProductUtils.js index 608a17a6b..4e9cbc5d2 100644 --- a/cypress/support/api/utils/storeFront/storeFrontProductUtils.js +++ b/cypress/support/api/utils/storeFront/storeFrontProductUtils.js @@ -25,6 +25,7 @@ export const getProductVariants = (productId, channelSlug) => { id: element.id, name: element.name, price: element.pricing.price.gross.amount, + currency: element.pricing.price.gross.currency, })); }); }; diff --git a/cypress/support/pages/catalog/products/VariantsPage.js b/cypress/support/pages/catalog/products/VariantsPage.js index fd0cb7a39..db9de6685 100644 --- a/cypress/support/pages/catalog/products/VariantsPage.js +++ b/cypress/support/pages/catalog/products/VariantsPage.js @@ -19,7 +19,6 @@ export function createVariant({ costPrice = price, quantity = 10, }) { - cy.get(PRODUCT_DETAILS.addVariantButton).click(); fillUpVariantDetails({ attributeName, sku, diff --git a/introspection.json b/introspection.json index a82c1cc72..5bc487419 100644 --- a/introspection.json +++ b/introspection.json @@ -7339,6 +7339,26 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "slugs", + "description": null, + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "channel", "description": "Specifies the channel by which the data should be filtered. \n\nDEPRECATED: this field will be removed in Saleor 4.0. Use root-level channel argument instead.", @@ -9110,13 +9130,9 @@ "name": "id", "description": "ID of the selected attribute.", "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - } + "kind": "SCALAR", + "name": "ID", + "ofType": null }, "defaultValue": null, "isDeprecated": false, @@ -11456,6 +11472,26 @@ "defaultValue": null, "isDeprecated": false, "deprecationReason": null + }, + { + "name": "slugs", + "description": null, + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null } ], "interfaces": null, @@ -14105,6 +14141,22 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "taxExemption", + "description": "Returns True if checkout has to be exempt from taxes.\n\nAdded in Saleor 3.8.\n\nNote: this API is currently in Feature Preview and can be subject to changes at later point.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "token", "description": "The checkout's token.", @@ -16202,6 +16254,26 @@ "defaultValue": "false", "isDeprecated": false, "deprecationReason": null + }, + { + "name": "metadata", + "description": "Fields required to update the object's metadata.\n\nAdded in Saleor 3.8.", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "MetadataInput", + "ofType": null + } + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null } ], "interfaces": null, @@ -18661,6 +18733,26 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "slugs", + "description": null, + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "channel", "description": "Specifies the channel by which the data should be filtered. \n\nDEPRECATED: this field will be removed in Saleor 4.0. Use root-level channel argument instead.", @@ -40554,6 +40646,26 @@ "defaultValue": null, "isDeprecated": false, "deprecationReason": null + }, + { + "name": "slugs", + "description": null, + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null } ], "interfaces": null, @@ -43211,6 +43323,51 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "taxExemptionManage", + "description": "Exempt checkout or order from charging the taxes. When tax exemption is enabled, taxes won't be charged for the checkout or order. Taxes may still be calculated in cases when product prices are entered with the tax included and the net price needs to be known.\n\nAdded in Saleor 3.8.\n\nNote: this API is currently in Feature Preview and can be subject to changes at later point. \n\nRequires one of the following permissions: MANAGE_TAXES.", + "args": [ + { + "name": "id", + "description": "ID of the Checkout or Order object.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "taxExemption", + "description": "Determines if a taxes should be exempt.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "TaxExemptionManage", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "staffNotificationRecipientCreate", "description": "Creates a new staff notification recipient. \n\nRequires one of the following permissions: MANAGE_SETTINGS.", @@ -50833,6 +50990,26 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "metadata", + "description": "Fields required to update the checkout metadata.\n\nAdded in Saleor 3.8.", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "MetadataInput", + "ofType": null + } + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "paymentData", "description": "Client-side generated data required to finalize the payment.", @@ -51776,6 +51953,46 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "metadata", + "description": "Fields required to update the checkout metadata.\n\nAdded in Saleor 3.8.", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "MetadataInput", + "ofType": null + } + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "privateMetadata", + "description": "Fields required to update the checkout private metadata.\n\nAdded in Saleor 3.8.", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "MetadataInput", + "ofType": null + } + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "removeCheckout", "description": "Determines if checkout should be removed after creating an order. Default true.", @@ -55925,6 +56142,22 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "taxExemption", + "description": "Returns True if order has to be exempt from taxes.\n\nAdded in Saleor 3.8.\n\nNote: this API is currently in Feature Preview and can be subject to changes at later point.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "transactions", "description": "List of transactions for the order. Requires one of the following permissions: MANAGE_ORDERS, HANDLE_PAYMENTS.\n\nAdded in Saleor 3.4.\n\nNote: this API is currently in Feature Preview and can be subject to changes at later point.", @@ -63961,6 +64194,26 @@ "defaultValue": null, "isDeprecated": false, "deprecationReason": null + }, + { + "name": "slugs", + "description": null, + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null } ], "interfaces": null, @@ -65607,6 +65860,26 @@ "defaultValue": null, "isDeprecated": false, "deprecationReason": null + }, + { + "name": "slugs", + "description": null, + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null } ], "interfaces": null, @@ -68400,6 +68673,30 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "MANAGE_CHECKOUTS", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "HANDLE_CHECKOUTS", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "HANDLE_TAXES", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "MANAGE_TAXES", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "MANAGE_CHANNELS", "description": null, @@ -68483,24 +68780,6 @@ "description": null, "isDeprecated": false, "deprecationReason": null - }, - { - "name": "MANAGE_CHECKOUTS", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "HANDLE_CHECKOUTS", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "HANDLE_TAXES", - "description": null, - "isDeprecated": false, - "deprecationReason": null } ], "possibleTypes": null @@ -73206,6 +73485,26 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "slugs", + "description": null, + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "channel", "description": "Specifies the channel by which the data should be filtered. \n\nDEPRECATED: this field will be removed in Saleor 4.0. Use root-level channel argument instead.", @@ -75847,6 +76146,26 @@ "defaultValue": null, "isDeprecated": false, "deprecationReason": null + }, + { + "name": "slugs", + "description": null, + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null } ], "interfaces": null, @@ -81858,7 +82177,7 @@ }, { "name": "filter", - "description": "Filtering options for menus.", + "description": "Filtering options for menus. \n\n`slug`: This field will be removed in Saleor 4.0. Use `slugs` instead.", "type": { "kind": "INPUT_OBJECT", "name": "MenuFilterInput", @@ -94219,6 +94538,139 @@ "enumValues": null, "possibleTypes": null }, + { + "kind": "OBJECT", + "name": "TaxExemptionManage", + "description": "Exempt checkout or order from charging the taxes. When tax exemption is enabled, taxes won't be charged for the checkout or order. Taxes may still be calculated in cases when product prices are entered with the tax included and the net price needs to be known.\n\nAdded in Saleor 3.8.\n\nNote: this API is currently in Feature Preview and can be subject to changes at later point. \n\nRequires one of the following permissions: MANAGE_TAXES.", + "fields": [ + { + "name": "taxableObject", + "description": null, + "args": [], + "type": { + "kind": "UNION", + "name": "TaxSourceObject", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "errors", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "TaxExemptionManageError", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "TaxExemptionManageError", + "description": null, + "fields": [ + { + "name": "field", + "description": "Name of a field that caused the error. A value of `null` indicates that the error isn't associated with a particular field.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "message", + "description": "The error message.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "code", + "description": "The error code.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "TaxExemptionManageErrorCode", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "TaxExemptionManageErrorCode", + "description": "An enumeration.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "GRAPHQL_ERROR", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INVALID", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "NOT_FOUND", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "NOT_EDITABLE_ORDER", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, { "kind": "UNION", "name": "TaxSourceLine", @@ -103027,6 +103479,26 @@ "defaultValue": null, "isDeprecated": false, "deprecationReason": null + }, + { + "name": "slugs", + "description": null, + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null } ], "interfaces": null, diff --git a/locale/defaultMessages.json b/locale/defaultMessages.json index 04d229ae4..490a2bc11 100644 --- a/locale/defaultMessages.json +++ b/locale/defaultMessages.json @@ -443,6 +443,10 @@ "context": "order draft total price", "string": "Total" }, + "1WbTJ5": { + "context": "product variants, title", + "string": "Variants" + }, "1div9r": { "string": "Search Attribute" }, @@ -453,10 +457,6 @@ "1gzck6": { "string": "{firstName} {lastName}" }, - "1kdQdO": { - "context": "section header", - "string": "Variants" - }, "1lk/oS": { "context": "button", "string": "Add postal code range" @@ -480,10 +480,6 @@ "context": "header", "string": "Translation Voucher \"{voucherName}\" - {languageCode}" }, - "1w06LC": { - "context": "variants count label", - "string": "{variantsCount} variants" - }, "1wyZpQ": { "string": "These passwords are too similar" }, @@ -647,10 +643,6 @@ "context": "filter range separator", "string": "and" }, - "37U5su": { - "context": "all variants label", - "string": "All variants" - }, "38dS1A": { "context": "code ending with label", "string": "Code ending with {last4CodeChars}" @@ -681,10 +673,6 @@ "context": "navigator command mode description", "string": "Search Command" }, - "3VyHbJ": { - "context": "button", - "string": "Create variant" - }, "3Z8972": { "context": "product", "string": "Stock quantity" @@ -834,6 +822,10 @@ "4gZl/n": { "string": "See DEMO STOREFRONT" }, + "4hl9rS": { + "context": "variant price in channel", + "string": "Price" + }, "4k9rMQ": { "context": "variant attribute checkbox", "string": "Variant Selection" @@ -1004,10 +996,6 @@ "context": "weight", "string": "{fromValue} {fromUnit} - {toValue} {toUnit}" }, - "6+sMz4": { - "context": "product variant inventory", - "string": "Unavailable in all locations" - }, "62T585": { "context": "button", "string": "{languageName} - {languageCode}" @@ -1191,10 +1179,6 @@ "7l5Bh9": { "string": "{counter,plural,one{Are you sure you want to delete this product?} other{Are you sure you want to delete {displayQuantity} products?}}" }, - "7mK2vs": { - "context": "product variant inventory", - "string": "Unavailable" - }, "7nKXni": { "context": "option description", "string": "This product will act as a payment method" @@ -1223,10 +1207,6 @@ "context": "section header", "string": "User Status" }, - "80FeaT": { - "context": "product variant preorder threshold", - "string": "{globalThreshold} Global threshold" - }, "86pLaG": { "context": "WarehouseSettings public stock label", "string": "Public Stock" @@ -1343,10 +1323,6 @@ "9OtpHt": { "string": "Order line deleted" }, - "9PmyrU": { - "context": "product variant inventory", - "string": "Not stocked" - }, "9RCuN3": { "string": "Payment successfully captured" }, @@ -1640,6 +1616,10 @@ "context": "sale status", "string": "Scheduled" }, + "BbP+k3": { + "context": "variant availability in channel", + "string": "Available" + }, "Be+J13": { "string": "Configurable" }, @@ -2135,10 +2115,6 @@ "context": "option title", "string": "Prioritize warehouses by sorting order" }, - "FSinkL": { - "context": "variant stock status", - "string": "Available inventory at:" - }, "FWWliu": { "context": "ExitFormPrompt title", "string": "You have unsaved changes" @@ -2552,6 +2528,9 @@ "context": "button", "string": "Create attribute" }, + "IH47ID": { + "string": "Use button above to add new product variants" + }, "IHtOda": { "context": "input label", "string": "Selected columns" @@ -2660,6 +2639,9 @@ "context": "section header", "string": "Attribute Values" }, + "J4E+jp": { + "string": "Variant name" + }, "J7mFhU": { "context": "currency code select", "string": "{code} - {countries}" @@ -2814,10 +2796,6 @@ "context": "product type is digital or physical", "string": "Type" }, - "JtZ71e": { - "context": "filtering option", - "string": "All Warehouses" - }, "Jwuu4X": { "context": "select product informations to be exported", "string": "Information exported:" @@ -3324,10 +3302,6 @@ "context": "header", "string": "{webhookName} Details" }, - "OTek3r": { - "context": "product variant name", - "string": "Variant" - }, "OVOU1z": { "context": "section header", "string": "Metadata" @@ -4045,6 +4019,9 @@ "U2mOqA": { "string": "No vouchers found" }, + "U5Da30": { + "string": "Warehouses" + }, "U5aVd8": { "context": "product", "string": "Shippable" @@ -4156,6 +4133,10 @@ "context": "label", "string": "Link" }, + "Us9cA1": { + "context": "profit margin", + "string": "Margin" + }, "Utq1fE": { "context": "customer gift cards card title", "string": "Gift Cards" @@ -4377,9 +4358,6 @@ "context": "order refund amount", "string": "Shipment Cost" }, - "WHkx+F": { - "string": "Price cannot be lower than 0" - }, "WMGoqz": { "context": "used by filter label", "string": "Used by" @@ -6021,10 +5999,6 @@ "context": "sales section name", "string": "Sales" }, - "kL3C+K": { - "context": "product variant inventory status", - "string": "Inventory" - }, "kN6SLs": { "string": "Min Value" }, @@ -6333,10 +6307,6 @@ "n+Gwbu": { "string": "Discounts" }, - "n02c9W": { - "context": "product variant price", - "string": "Price" - }, "n0RwMK": { "string": "Define types of products you sell" }, @@ -6559,10 +6529,6 @@ "context": "payment status", "string": "Payment" }, - "p/EWEZ": { - "context": "channels variants availability dialog title", - "string": "Manage Channels" - }, "p/Fd7s": { "context": "unassign products from shipping rate and save, button", "string": "Unassign and save" @@ -6744,10 +6710,6 @@ "context": "dialog header", "string": "Delete Order Drafts" }, - "qbqMpk": { - "context": "product variant preorder threshold", - "string": "In preorder" - }, "qddy2Z": { "context": "order history message", "string": "Invoice was sent to customer by {sentBy}" @@ -6820,9 +6782,6 @@ "context": "Status label when object is unpublished in a channel", "string": "Unpublished" }, - "rIJbNC": { - "string": "Use variants for products that come in a variety of versions for example different sizes or colors" - }, "rJ3lkW": { "context": "VariantDetailsChannelsAvailabilityCard item subtitle published", "string": "Published since {publicationDate}" @@ -7249,10 +7208,6 @@ "context": "order payment", "string": "Preauthorized amount" }, - "uVssds": { - "context": "product variant inventory", - "string": "{stockQuantity,plural,one{{stockQuantity} available} other{{stockQuantity} available}}" - }, "ubasgL": { "context": "order history message", "string": "Order was confirmed" @@ -7533,10 +7488,6 @@ "wWTUrM": { "string": "No activities found" }, - "wWYYBR": { - "context": "product variant inventory", - "string": "{numLocations,plural,one{{numAvailable} available at {numLocations} location} other{{numAvailable} available at {numLocations} locations}}" - }, "wgA48T": { "context": "country selection", "string": "Countries A to Z" @@ -7725,10 +7676,6 @@ "context": "dialog header", "string": "Unpublish Pages" }, - "yHaQWG": { - "context": "variants selected label", - "string": "{variantsAmount} variants selected" - }, "yHeZRQ": { "string": "Product Export Failed" }, diff --git a/package-lock.json b/package-lock.json index fb60287fe..ca690d3b3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2584,6 +2584,15 @@ } } }, + "@glideapps/glide-data-grid": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@glideapps/glide-data-grid/-/glide-data-grid-5.0.0.tgz", + "integrity": "sha512-0uSXKPiPUoQsIJ6MxgCAzVVXsSyo6gBAuiuoQR81TM9JXuzyzHtgcHXamD2j+1TumxWIU1UhXhpDRaLMtlLhMA==", + "requires": { + "canvas-hypertxt": "^0.0.3", + "react-number-format": "^4.4.1" + } + }, "@graphql-codegen/add": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/@graphql-codegen/add/-/add-3.1.1.tgz", @@ -5165,6 +5174,21 @@ "integrity": "sha512-LMMJLAXAwk1kVMy7fxTRFK6OdouvoseqJbmVUygJb2EcfuT84nC9OAtvGEL4vsVLUcnzEV400+F9t5OKa77FGQ==", "requires": { "@react-editor-js/core": "2.0.6" + }, + "dependencies": { + "immer": { + "version": "9.0.14", + "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.14.tgz", + "integrity": "sha512-ubBeqQutOSLIFCUBN03jGeOS6a3DoYlSYwYJTa+gSKEZKU5redJIqkIdZ3JVv/4RZpfcXdAWH5zCNLWPRv2WDw==" + }, + "redux": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.0.tgz", + "integrity": "sha512-oSBmcKKIuIR4ME29/AeNUnl5L+hvBq7OaJWzaptTQJAntaPvxIJqfnjbaEiCzzaIz+XmVILfqAM3Ob0aXLPfjA==", + "requires": { + "@babel/runtime": "^7.9.2" + } + } } }, "@react-editor-js/core": { @@ -10302,6 +10326,11 @@ "integrity": "sha512-OE5UE4+nBOro8Dyvv0lfx+SRtfVIOM9uhKqFmJeUbGriqhhStgp1A0OyBpgy3OUF8AhYCT+PVwPC1gMl2ZcQMA==", "dev": true }, + "canvas-hypertxt": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/canvas-hypertxt/-/canvas-hypertxt-0.0.3.tgz", + "integrity": "sha512-Wmi0bfM+T6BAwFg3wZ1q3vXmltAcO/zPPfHSblJkOeuO9fwhSDMCIbaBLok71PONoOQHYnsk4CQ/GS1Ai7+OnQ==" + }, "capital-case": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/capital-case/-/capital-case-1.0.4.tgz", @@ -11875,6 +11904,12 @@ "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", "dev": true }, + "cssfontparser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/cssfontparser/-/cssfontparser-1.2.1.tgz", + "integrity": "sha512-6tun4LoZnj7VN6YeegOVb67KBX/7JJsqvj+pv3ZA7F878/eN33AbGa5b/S/wXxS/tcp8nc40xRUrsPlxIyNUPg==", + "dev": true + }, "csso": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/csso/-/csso-4.2.0.tgz", @@ -16825,6 +16860,12 @@ "har-schema": "^2.0.0" } }, + "harmony-reflect": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/harmony-reflect/-/harmony-reflect-1.6.2.tgz", + "integrity": "sha512-HIp/n38R9kQjDEziXyDTuW3vvoxxyxjxFzXLrBr18uB47GnSt+G9D29fqrpM5ZkspMcPICud3XsBJQ4Y2URg8g==", + "dev": true + }, "has": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", @@ -17713,6 +17754,15 @@ "postcss": "^7.0.14" } }, + "identity-obj-proxy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/identity-obj-proxy/-/identity-obj-proxy-3.0.0.tgz", + "integrity": "sha512-00n6YnVHKrinT9t0d9+5yZC6UBNJANpYEQvL2LlX6Ab9lnmxzIRcEmTPuyGScvl1+jKuCICX1Z0Ab1pPKKdikA==", + "dev": true, + "requires": { + "harmony-reflect": "^1.4.6" + } + }, "ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -18769,6 +18819,16 @@ } } }, + "jest-canvas-mock": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/jest-canvas-mock/-/jest-canvas-mock-2.4.0.tgz", + "integrity": "sha512-mmMpZzpmLzn5vepIaHk5HoH3Ka4WykbSoLuG/EKoJd0x0ID/t+INo1l8ByfcUJuDM+RIsL4QDg/gDnBbrj2/IQ==", + "dev": true, + "requires": { + "cssfontparser": "^1.2.1", + "moo-color": "^1.0.2" + } + }, "jest-changed-files": { "version": "24.9.0", "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-24.9.0.tgz", @@ -19377,6 +19437,12 @@ "@jest/types": "^24.9.0" } }, + "jest-mock-random": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/jest-mock-random/-/jest-mock-random-1.1.1.tgz", + "integrity": "sha512-eLduwHIZeepHCHWTgx6MGr8k4CWxTG39SN76tszzgCjX8GNu6M/05WcXMci73z1GUVqYQAsw6inz6ndgr3Gmqw==", + "dev": true + }, "jest-pnp-resolver": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz", @@ -21803,6 +21869,11 @@ "unquote": "^1.1.0" } }, + "marked": { + "version": "4.0.17", + "resolved": "https://registry.npmjs.org/marked/-/marked-4.0.17.tgz", + "integrity": "sha512-Wfk0ATOK5iPxM4ptrORkFemqroz0ZDxp5MWfYA7H/F+wO17NRWV5Ypxi6p3g2Xmw2bKeiYOl6oVnLHKxBA0VhA==" + }, "math-random": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/math-random/-/math-random-1.0.4.tgz", @@ -22696,6 +22767,15 @@ "integrity": "sha512-I1mnb5xn4fO80BH9BLcF0yLypy2UKl+Cb01Fu0hJRkJjlCRtxZMWkTdAtDd5ZqCOxtCkhmRwyI57vWT+1iZ67w==", "dev": true }, + "moo-color": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/moo-color/-/moo-color-1.0.3.tgz", + "integrity": "sha512-i/+ZKXMDf6aqYtBhuOcej71YSlbjT3wCO/4H1j8rPvxDJEifdwgg5MaFyu6iYAT8GBZJg2z0dkgK4YMzvURALQ==", + "dev": true, + "requires": { + "color-name": "^1.1.4" + } + }, "morgan": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz", @@ -25476,13 +25556,29 @@ } }, "react-draggable": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/react-draggable/-/react-draggable-4.4.3.tgz", - "integrity": "sha512-jV4TE59MBuWm7gb6Ns3Q1mxX8Azffb7oTtDtBgFkxRvhDp38YAARmRplrj0+XGkhOJB5XziArX+4HUUABtyZ0w==", - "dev": true, + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-draggable/-/react-draggable-4.4.5.tgz", + "integrity": "sha512-OMHzJdyJbYTZo4uQE393fHcqqPYsEtkjfMgvCHr6rejT+Ezn4OZbNyGH50vv+SunC1RMvwOTSWkEODQLzw1M9g==", "requires": { - "classnames": "^2.2.5", - "prop-types": "^15.6.0" + "clsx": "^1.1.1", + "prop-types": "^15.8.1" + }, + "dependencies": { + "prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "requires": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + } } }, "react-dropzone": { @@ -25495,6 +25591,14 @@ "prop-types": "^15.7.2" } }, + "react-easy-swipe": { + "version": "0.0.21", + "resolved": "https://registry.npmjs.org/react-easy-swipe/-/react-easy-swipe-0.0.21.tgz", + "integrity": "sha512-OeR2jAxdoqUMHIn/nS9fgreI5hSpgGoL5ezdal4+oO7YSSgJR8ga+PkYGJrSrJ9MKlPcQjMQXnketrD7WNmNsg==", + "requires": { + "prop-types": "^15.5.8" + } + }, "react-editor-js": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/react-editor-js/-/react-editor-js-2.0.6.tgz", @@ -25714,6 +25818,14 @@ "resolved": "https://registry.npmjs.org/react-moment/-/react-moment-1.1.1.tgz", "integrity": "sha512-WjwvxBSnmLMRcU33do0KixDB+9vP3e84eCse+rd+HNklAMNWyRgZTDEQlay/qK6lcXFPRuEIASJTpEt6pyK7Ww==" }, + "react-number-format": { + "version": "4.9.4", + "resolved": "https://registry.npmjs.org/react-number-format/-/react-number-format-4.9.4.tgz", + "integrity": "sha512-Gq20Z3ugqPLFgeaidnx5on9cNpbQZntPN3QgNAL/WJrNNlQnNznY0LCx7g8xtssmRBw0/hw+SCqw6zAcajooiA==", + "requires": { + "prop-types": "^15.7.2" + } + }, "react-popper": { "version": "1.3.11", "resolved": "https://registry.npmjs.org/react-popper/-/react-popper-1.3.11.tgz", @@ -25756,6 +25868,16 @@ "react-popper": "^1.3.7" } }, + "react-responsive-carousel": { + "version": "3.2.23", + "resolved": "https://registry.npmjs.org/react-responsive-carousel/-/react-responsive-carousel-3.2.23.tgz", + "integrity": "sha512-pqJLsBaKHWJhw/ItODgbVoziR2z4lpcJg+YwmRlSk4rKH32VE633mAtZZ9kDXjy4wFO+pgUZmDKPsPe1fPmHCg==", + "requires": { + "classnames": "^2.2.5", + "prop-types": "^15.5.8", + "react-easy-swipe": "^0.0.21" + } + }, "react-router": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.2.0.tgz", diff --git a/package.json b/package.json index 75679ef53..e65573b8c 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "@editorjs/list": "^1.7.0", "@editorjs/paragraph": "^2.8.0", "@editorjs/quote": "^2.4.0", + "@glideapps/glide-data-grid": "^5.0.0", "@material-ui/core": "^4.11.4", "@material-ui/icons": "^4.11.2", "@material-ui/lab": "^4.0.0-alpha.58", @@ -57,10 +58,12 @@ "jwt-decode": "^3.1.2", "keycode": "^2.2.0", "lodash": "^4.17.20", + "marked": "^4.0.17", "moment-timezone": "^0.5.32", "qs": "^6.9.0", "react": "^16.12.0", "react-dom": "^16.9.0", + "react-draggable": "^4.4.5", "react-dropzone": "^11.2.4", "react-editor-js": "^2.0.6", "react-error-boundary": "^1.2.5", @@ -72,6 +75,7 @@ "react-jss": "^10.0.0", "react-markdown": "^4.3.1", "react-moment": "^1.0.0", + "react-responsive-carousel": "^3.2.23", "react-router": "^5.0.1", "react-router-dom": "^5.0.1", "react-sortable-hoc": "^1.10.1", @@ -170,9 +174,12 @@ "graphql-request": "^3.7.0", "html-webpack-plugin": "^3.2.0", "husky": "^4.3.0", + "identity-obj-proxy": "^3.0.0", "jest": "^24.8.0", + "jest-canvas-mock": "^2.4.0", "jest-file": "^1.0.0", "jest-localstorage-mock": "^2.4.3", + "jest-mock-random": "^1.1.1", "jscodeshift": "^0.13.0", "lint-staged": "^10.5.1", "mocha-junit-reporter": "^2.0.2", @@ -211,6 +218,7 @@ "jest": { "resetMocks": false, "setupFiles": [ + "jest-canvas-mock", "jest-localstorage-mock" ], "transform": { @@ -227,6 +235,7 @@ "node" ], "moduleNameMapper": { + "\\.(css)$": "identity-obj-proxy", "@assets(.*)$": "/assets/$1", "@locale(.*)$": "/locale/$1", "@saleor(?!.*macaw)(?!.*sdk)(.*)$": "/src/$1", diff --git a/schema.graphql b/schema.graphql index e5bd18a89..5bb62dc28 100644 --- a/schema.graphql +++ b/schema.graphql @@ -1423,6 +1423,7 @@ input AttributeFilterInput { type: AttributeTypeEnum inCollection: ID inCategory: ID + slugs: [String!] """ Specifies the channel by which the data should be filtered. @@ -1824,7 +1825,7 @@ input AttributeValueFilterInput { input AttributeValueInput { """ID of the selected attribute.""" - id: ID! + id: ID """ The value or slug of an attribute to resolve. If the passed value is non-existent, it will be created. @@ -2353,6 +2354,7 @@ input CategoryFilterInput { search: String metadata: [MetadataFilter!] ids: [ID!] + slugs: [String!] } input CategoryInput { @@ -3011,6 +3013,15 @@ type Checkout implements Node & ObjectWithMetadata { """The price of the checkout before shipping, with taxes included.""" subtotalPrice: TaxedMoney! + """ + Returns True if checkout has to be exempt from taxes. + + Added in Saleor 3.8. + + Note: this API is currently in Feature Preview and can be subject to changes at later point. + """ + taxExemption: Boolean! + """The checkout's token.""" token: UUID! @@ -3432,6 +3443,13 @@ input CheckoutLineInput { Note: this API is currently in Feature Preview and can be subject to changes at later point. """ forceNewLine: Boolean = false + + """ + Fields required to update the object's metadata. + + Added in Saleor 3.8. + """ + metadata: [MetadataInput!] } input CheckoutLineUpdateInput { @@ -3950,6 +3968,7 @@ input CollectionFilterInput { search: String metadata: [MetadataFilter!] ids: [ID!] + slugs: [String!] """ Specifies the channel by which the data should be filtered. @@ -8250,6 +8269,7 @@ input MenuFilterInput { search: String slug: [String!] metadata: [MetadataFilter!] + slugs: [String!] } input MenuInput { @@ -8851,6 +8871,23 @@ type Mutation { shippingZoneIds: [ID!]! ): WarehouseShippingZoneUnassign + """ + Exempt checkout or order from charging the taxes. When tax exemption is enabled, taxes won't be charged for the checkout or order. Taxes may still be calculated in cases when product prices are entered with the tax included and the net price needs to be known. + + Added in Saleor 3.8. + + Note: this API is currently in Feature Preview and can be subject to changes at later point. + + Requires one of the following permissions: MANAGE_TAXES. + """ + taxExemptionManage( + """ID of the Checkout or Order object.""" + id: ID! + + """Determines if a taxes should be exempt.""" + taxExemption: Boolean! + ): TaxExemptionManage + """ Creates a new staff notification recipient. @@ -11123,6 +11160,13 @@ type Mutation { """ id: ID + """ + Fields required to update the checkout metadata. + + Added in Saleor 3.8. + """ + metadata: [MetadataInput!] + """Client-side generated data required to finalize the payment.""" paymentData: JSONString @@ -11531,6 +11575,20 @@ type Mutation { """ID of a checkout that will be converted to an order.""" id: ID! + """ + Fields required to update the checkout metadata. + + Added in Saleor 3.8. + """ + metadata: [MetadataInput!] + + """ + Fields required to update the checkout private metadata. + + Added in Saleor 3.8. + """ + privateMetadata: [MetadataInput!] + """ Determines if checkout should be removed after creating an order. Default true. """ @@ -12520,6 +12578,15 @@ type Order implements Node & ObjectWithMetadata { """ chargeStatus: OrderChargeStatusEnum! + """ + Returns True if order has to be exempt from taxes. + + Added in Saleor 3.8. + + Note: this API is currently in Feature Preview and can be subject to changes at later point. + """ + taxExemption: Boolean! + """ List of transactions for the order. Requires one of the following permissions: MANAGE_ORDERS, HANDLE_PAYMENTS. @@ -14193,6 +14260,7 @@ input PageFilterInput { metadata: [MetadataFilter!] pageTypes: [ID!] ids: [ID!] + slugs: [String!] } """ @@ -14579,6 +14647,7 @@ type PageTypeDeleted implements Event { input PageTypeFilterInput { search: String + slugs: [String!] } """ @@ -15218,6 +15287,10 @@ enum PermissionEnum { IMPERSONATE_USER MANAGE_APPS MANAGE_OBSERVABILITY + MANAGE_CHECKOUTS + HANDLE_CHECKOUTS + HANDLE_TAXES + MANAGE_TAXES MANAGE_CHANNELS MANAGE_DISCOUNTS MANAGE_PLUGINS @@ -15232,9 +15305,6 @@ enum PermissionEnum { MANAGE_SHIPPING MANAGE_SETTINGS MANAGE_TRANSLATIONS - MANAGE_CHECKOUTS - HANDLE_CHECKOUTS - HANDLE_TAXES } """ @@ -16228,6 +16298,7 @@ input ProductFilterInput { giftCard: Boolean ids: [ID!] hasPreorderedVariants: Boolean + slugs: [String!] """ Specifies the channel by which the data should be filtered. @@ -16801,6 +16872,7 @@ input ProductTypeFilterInput { metadata: [MetadataFilter!] kind: ProductTypeKindEnum ids: [ID!] + slugs: [String!] } input ProductTypeInput { @@ -18296,7 +18368,11 @@ type Query { """Sort menus.""" sortBy: MenuSortingInput - """Filtering options for menus.""" + """ + Filtering options for menus. + + `slug`: This field will be removed in Saleor 4.0. Use `slugs` instead. + """ filter: MenuFilterInput """Return the elements in the list that come before the specified cursor.""" @@ -21239,6 +21315,41 @@ type Subscription { event: Event } +""" +Exempt checkout or order from charging the taxes. When tax exemption is enabled, taxes won't be charged for the checkout or order. Taxes may still be calculated in cases when product prices are entered with the tax included and the net price needs to be known. + +Added in Saleor 3.8. + +Note: this API is currently in Feature Preview and can be subject to changes at later point. + +Requires one of the following permissions: MANAGE_TAXES. +""" +type TaxExemptionManage { + taxableObject: TaxSourceObject + errors: [TaxExemptionManageError!]! +} + +type TaxExemptionManageError { + """ + Name of a field that caused the error. A value of `null` indicates that the error isn't associated with a particular field. + """ + field: String + + """The error message.""" + message: String + + """The error code.""" + code: TaxExemptionManageErrorCode! +} + +"""An enumeration.""" +enum TaxExemptionManageErrorCode { + GRAPHQL_ERROR + INVALID + NOT_FOUND + NOT_EDITABLE_ORDER +} + union TaxSourceLine = CheckoutLine | OrderLine union TaxSourceObject = Checkout | Order @@ -23068,6 +23179,7 @@ input WarehouseFilterInput { ids: [ID!] isPrivate: Boolean channels: [ID!] + slugs: [String!] } """ diff --git a/src/channels/ChannelsWithVariantsAvailabilityCard/ChannelWithVariantAvailabilityItemWrapper.tsx b/src/channels/ChannelsWithVariantsAvailabilityCard/ChannelWithVariantAvailabilityItemWrapper.tsx deleted file mode 100644 index e0915d3c2..000000000 --- a/src/channels/ChannelsWithVariantsAvailabilityCard/ChannelWithVariantAvailabilityItemWrapper.tsx +++ /dev/null @@ -1,135 +0,0 @@ -import { Accordion, AccordionSummary, Typography } from "@material-ui/core"; -import { ChannelData } from "@saleor/channels/utils"; -import { Messages } from "@saleor/components/ChannelsAvailabilityCard/types"; -import { ProductDetailsVariantFragment } from "@saleor/graphql"; -import IconChevronDown from "@saleor/icons/ChevronDown"; -import { makeStyles } from "@saleor/macaw-ui"; -import Label from "@saleor/orders/components/OrderHistory/Label"; -import { getById } from "@saleor/orders/components/OrderReturnPage/utils"; -import { ChannelsWithVariantsData } from "@saleor/products/views/ProductUpdate/types"; -import { areAllChannelVariantsSelected } from "@saleor/products/views/ProductUpdate/utils"; -import React from "react"; -import { defineMessages, useIntl } from "react-intl"; - -const useExpanderStyles = makeStyles( - theme => ({ - expanded: {}, - root: { - boxShadow: "none", - margin: 0, - padding: 0, - paddingBottom: theme.spacing(2), - - "&:before": { - content: "none", - }, - - "&$expanded": { - margin: 0, - border: "none", - }, - }, - }), - { name: "ChannelWithVariantAvailabilityItemWrapperExpander" }, -); - -const useSummaryStyles = makeStyles( - theme => ({ - expanded: {}, - root: { - width: "100%", - border: "none", - margin: 0, - padding: 0, - minHeight: 0, - paddingTop: theme.spacing(2), - - "&$expanded": { - minHeight: 0, - }, - }, - content: { - margin: 0, - - "&$expanded": { - margin: 0, - }, - }, - }), - { name: "ChannelWithVariantAvailabilityItemWrapperSummary" }, -); - -const useStyles = makeStyles( - () => ({ - container: { - display: "flex", - flexDirection: "column", - }, - }), - { name: "ChannelWithVariantAvailabilityItemWrapper" }, -); - -const messages = defineMessages({ - variantCountLabel: { - id: "1w06LC", - defaultMessage: "{variantsCount} variants", - description: "variants count label", - }, - allVariantsLabel: { - id: "37U5su", - defaultMessage: "All variants", - description: "all variants label", - }, -}); - -interface ChannelAvailabilityItemWrapperProps { - variants: ProductDetailsVariantFragment[]; - channelId: string; - channels: ChannelData[]; - channelsWithVariantsData: ChannelsWithVariantsData; - messages: Messages; -} - -const ChannelWithVariantsAvailabilityItemWrapper: React.FC = ({ - channels, - channelsWithVariantsData, - channelId, - variants, - messages: commonChannelMessages, - children, -}) => { - const expanderClasses = useExpanderStyles(); - const summaryClasses = useSummaryStyles(); - const classes = useStyles(); - const intl = useIntl(); - - const { name } = channels.find(getById(channelId)); - const { selectedVariantsIds } = channelsWithVariantsData[channelId]; - - const variantsCount = selectedVariantsIds.length; - - const variantsLabel = areAllChannelVariantsSelected( - variants?.map(variant => variant.id), - selectedVariantsIds, - ) - ? messages.allVariantsLabel - : messages.variantCountLabel; - - return ( - - } - classes={summaryClasses} - > -
- {name} -
-
- {children} -
- ); -}; - -export default ChannelWithVariantsAvailabilityItemWrapper; diff --git a/src/channels/ChannelsWithVariantsAvailabilityCard/ChannelsWithVariantsAvailabilityCard.stories.tsx b/src/channels/ChannelsWithVariantsAvailabilityCard/ChannelsWithVariantsAvailabilityCard.stories.tsx deleted file mode 100644 index 24e20da50..000000000 --- a/src/channels/ChannelsWithVariantsAvailabilityCard/ChannelsWithVariantsAvailabilityCard.stories.tsx +++ /dev/null @@ -1,108 +0,0 @@ -import { ProductDetailsVariantFragment } from "@saleor/graphql"; -import CentralPlacementDecorator from "@saleor/storybook/CentralPlacementDecorator"; -import CommonDecorator from "@saleor/storybook/Decorator"; -import { storiesOf } from "@storybook/react"; -import React from "react"; - -import ChannelsWithVariantsAvailabilityCard, { - ChannelsWithVariantsAvailabilityCardProps, -} from "./ChannelsWithVariantsAvailabilityCard"; - -const props: ChannelsWithVariantsAvailabilityCardProps = { - channels: [ - { - id: "1", - name: "Channel 1", - isAvailableForPurchase: false, - isPublished: true, - publicationDate: "2020-07-30", - availableForPurchase: null, - visibleInListings: false, - currency: "EUR", - variantsIds: ["variantA"], - costPrice: "5", - price: "15", - }, - { - id: "2", - name: "Channel 2", - isAvailableForPurchase: false, - isPublished: true, - publicationDate: "2020-07-30", - availableForPurchase: null, - visibleInListings: false, - currency: "EUR", - variantsIds: ["variantA"], - costPrice: "5", - price: "15", - }, - { - id: "3", - name: "Channel 3", - isAvailableForPurchase: false, - isPublished: true, - publicationDate: "2020-07-30", - availableForPurchase: null, - visibleInListings: false, - currency: "EUR", - variantsIds: ["variantA"], - costPrice: "5", - price: "15", - }, - ], - variants: [ - { - id: "variantA", - name: "Variant A", - media: [ - { - url: - "https://test-envs-stack-testenvsmediabucket050c0d50-qdkqrzgoumxv.s3.amazonaws.com/feature-assing-variants-to-channel-listings/products/saleordemoproduct_fd_juice_06.png", - }, - ], - }, - { - id: "variantB", - name: "Variant B", - media: [ - { - url: - "https://test-envs-stack-testenvsmediabucket050c0d50-qdkqrzgoumxv.s3.amazonaws.com/feature-assing-variants-to-channel-listings/products/saleordemoproduct_fd_juice_05.png", - }, - ], - }, - { - id: "variantC", - name: "Variant C", - media: [], - }, - ] as ProductDetailsVariantFragment[], - channelsWithVariantsData: { - ["1"]: { - selectedVariantsIds: ["variantA", "variantB"], - variantsIdsToRemove: [], - variantsIdsToAdd: [], - }, - ["2"]: { - selectedVariantsIds: ["variantA", "variantC"], - variantsIdsToRemove: [], - variantsIdsToAdd: [], - }, - ["3"]: { - selectedVariantsIds: [], - variantsIdsToRemove: [], - variantsIdsToAdd: [], - }, - }, - messages: { - hiddenLabel: "Not published", - visibleLabel: "Published", - }, - onChange: () => undefined, - openModal: () => undefined, -}; - -storiesOf("Channels / Channels with variants availability card", module) - .addDecorator(CommonDecorator) - .addDecorator(CentralPlacementDecorator) - .add("default", () => ); diff --git a/src/channels/ChannelsWithVariantsAvailabilityCard/ChannelsWithVariantsAvailabilityCard.tsx b/src/channels/ChannelsWithVariantsAvailabilityCard/ChannelsWithVariantsAvailabilityCard.tsx deleted file mode 100644 index 6a0f65a45..000000000 --- a/src/channels/ChannelsWithVariantsAvailabilityCard/ChannelsWithVariantsAvailabilityCard.tsx +++ /dev/null @@ -1,100 +0,0 @@ -import { ChannelData } from "@saleor/channels/utils"; -import ChannelAvailabilityItemContent from "@saleor/components/ChannelsAvailabilityCard/Channel/ChannelAvailabilityItemContent"; -import ChannelsAvailabilityCardWrapper, { - ChannelsAvailabilityWrapperProps, -} from "@saleor/components/ChannelsAvailabilityCard/ChannelsAvailabilityCardWrapper"; -import { - ChannelOpts, - ChannelsAvailabilityError, - Messages, -} from "@saleor/components/ChannelsAvailabilityCard/types"; -import { getChannelsAvailabilityMessages } from "@saleor/components/ChannelsAvailabilityCard/utils"; -import { PermissionEnum, ProductDetailsVariantFragment } from "@saleor/graphql"; -import useDateLocalize from "@saleor/hooks/useDateLocalize"; -import { getById } from "@saleor/orders/components/OrderReturnPage/utils"; -import { ChannelsWithVariantsData } from "@saleor/products/views/ProductUpdate/types"; -import { - areAnyChannelVariantsSelected, - getTotalSelectedChannelsCount, -} from "@saleor/products/views/ProductUpdate/utils"; -import React from "react"; -import { useIntl } from "react-intl"; - -import ChannelWithVariantsAvailabilityItemWrapper from "./ChannelWithVariantAvailabilityItemWrapper"; - -type CommonChannelsAvailabilityProps = Omit< - ChannelsAvailabilityWrapperProps, - | "children" - | "selectedChannelsCount" - | "allChannelsCount" - | "managePermissions" ->; - -export interface ChannelsWithVariantsAvailabilityCardProps - extends CommonChannelsAvailabilityProps { - channelsWithVariantsData: ChannelsWithVariantsData; - channels: ChannelData[]; - variants: ProductDetailsVariantFragment[]; - errors?: ChannelsAvailabilityError[]; - messages: Messages; - onChange: (id: string, data: ChannelOpts) => void; -} - -const ChannelsWithVariantsAvailabilityCard: React.FC = ({ - channels, - channelsWithVariantsData, - openModal, - variants, - errors = [], - messages, - onChange, -}) => { - const intl = useIntl(); - const localizeDate = useDateLocalize(); - - const channelsMessages = getChannelsAvailabilityMessages({ - messages, - channels, - intl, - localizeDate, - }); - - const allChannelsCount = channels.length; - - const selectedChannelsCount = getTotalSelectedChannelsCount( - channelsWithVariantsData, - ); - - return ( - - {channels - .filter(({ id }) => - areAnyChannelVariantsSelected(channelsWithVariantsData[id]), - ) - .map(({ id }) => ( - - - - ))} - - ); -}; - -export default ChannelsWithVariantsAvailabilityCard; diff --git a/src/channels/components/ChannelsWithVariantsAvailabilityDialog/ChannelsWithVariantsAvailabilityDialog.stories.tsx b/src/channels/components/ChannelsWithVariantsAvailabilityDialog/ChannelsWithVariantsAvailabilityDialog.stories.tsx deleted file mode 100644 index db7eb1a34..000000000 --- a/src/channels/components/ChannelsWithVariantsAvailabilityDialog/ChannelsWithVariantsAvailabilityDialog.stories.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { ProductDetailsVariantFragment } from "@saleor/graphql"; -import CommonDecorator from "@saleor/storybook/Decorator"; -import { storiesOf } from "@storybook/react"; -import React from "react"; - -import ChannelsWithVariantsAvailabilityDialog, { - ChannelsAvailabilityDialogProps, -} from "./ChannelsWithVariantsAvailabilityDialog"; - -const props: ChannelsAvailabilityDialogProps = { - channels: [ - { - id: "1", - name: "Channel 1", - variantsIds: [], - }, - { - id: "2", - name: "Channel 2", - variantsIds: [], - }, - { - id: "3", - name: "Channel 3", - variantsIds: [], - }, - ], - variants: [ - { - id: "variantA", - name: "Variant A", - media: [ - { - url: - "https://test-envs-stack-testenvsmediabucket050c0d50-qdkqrzgoumxv.s3.amazonaws.com/feature-assing-variants-to-channel-listings/products/saleordemoproduct_fd_juice_06.png", - }, - ], - }, - { - id: "variantB", - name: "Variant B", - media: [ - { - url: - "https://test-envs-stack-testenvsmediabucket050c0d50-qdkqrzgoumxv.s3.amazonaws.com/feature-assing-variants-to-channel-listings/products/saleordemoproduct_fd_juice_05.png", - }, - ], - }, - { - id: "variantC", - name: "Variant C", - media: [], - }, - ] as ProductDetailsVariantFragment[], - onClose: () => undefined, - onConfirm: () => undefined, - open: true, -}; - -storiesOf("Channels / Channels with Variants Availability Dialog", module) - .addDecorator(CommonDecorator) - .add("default", () => ); diff --git a/src/channels/components/ChannelsWithVariantsAvailabilityDialog/ChannelsWithVariantsAvailabilityDialog.tsx b/src/channels/components/ChannelsWithVariantsAvailabilityDialog/ChannelsWithVariantsAvailabilityDialog.tsx deleted file mode 100644 index 3d55c8a50..000000000 --- a/src/channels/components/ChannelsWithVariantsAvailabilityDialog/ChannelsWithVariantsAvailabilityDialog.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import { ChannelData } from "@saleor/channels/utils"; -import ActionDialog from "@saleor/components/ActionDialog"; -import { ProductDetailsVariantFragment } from "@saleor/graphql"; -import useModalDialogOpen from "@saleor/hooks/useModalDialogOpen"; -import { ChannelVariantListing } from "@saleor/products/views/ProductUpdate/types"; -import useChannelsWithProductVariants from "@saleor/products/views/ProductUpdate/useChannelsWithProductVariants"; -import { - areAllVariantsAtAllChannelsSelected, - areAnyChannelVariantsSelected, - channelVariantListingDiffToDict, -} from "@saleor/products/views/ProductUpdate/utils"; -import { DialogProps } from "@saleor/types"; -import React from "react"; -import { defineMessages, useIntl } from "react-intl"; - -import { useChannelsSearch } from "../../../components/ChannelsAvailabilityDialog/utils"; -import ChannelsAvailabilityContentWrapper from "../../../components/ChannelsAvailabilityDialogWrapper/ChannelsAvailabilityDialogWrapper"; -import ChannelsWithVariantsAvailabilityDialogContent from "./ChannelsWithVariantsAvailabilityDialogContent"; - -const messages = defineMessages({ - title: { - id: "p/EWEZ", - defaultMessage: "Manage Channels", - description: "channels variants availability dialog title", - }, -}); - -export interface ChannelsAvailabilityDialogProps extends DialogProps { - channels: ChannelData[]; - contentType?: string; - variants: ProductDetailsVariantFragment[]; - onConfirm: (listings: ChannelVariantListing) => void; -} - -export const ChannelsWithVariantsAvailabilityDialog: React.FC = ({ - channels, - contentType, - variants, - open, - onClose, - onConfirm, -}) => { - const intl = useIntl(); - const { - channelsWithVariantsData, - toggleAllChannels, - addVariantToChannel, - removeVariantFromChannel, - toggleAllChannelVariants, - channelVariantListing, - reset, - } = useChannelsWithProductVariants( - channels, - variants?.map(variant => variant.id), - ); - - useModalDialogOpen(open, { - onClose: reset, - }); - - const { query, onQueryChange, filteredChannels } = useChannelsSearch( - channels, - ); - - const hasAllChannelsSelected = areAllVariantsAtAllChannelsSelected( - variants.map(variant => variant.id), - channelVariantListingDiffToDict(channelsWithVariantsData), - ); - - const isChannelSelected = (channelId: string) => - areAnyChannelVariantsSelected(channelsWithVariantsData[channelId]); - - return ( - onConfirm(channelVariantListing)} - title={intl.formatMessage(messages.title)} - > - - - - - ); -}; - -export default ChannelsWithVariantsAvailabilityDialog; diff --git a/src/channels/components/ChannelsWithVariantsAvailabilityDialog/ChannelsWithVariantsAvailabilityDialogContent.tsx b/src/channels/components/ChannelsWithVariantsAvailabilityDialog/ChannelsWithVariantsAvailabilityDialogContent.tsx deleted file mode 100644 index b80782bc5..000000000 --- a/src/channels/components/ChannelsWithVariantsAvailabilityDialog/ChannelsWithVariantsAvailabilityDialogContent.tsx +++ /dev/null @@ -1,228 +0,0 @@ -import placeholderImage from "@assets/images/placeholder60x60.png"; -import { - Accordion, - AccordionSummary, - Divider, - Typography, -} from "@material-ui/core"; -import { ChannelData } from "@saleor/channels/utils"; -import { ProductDetailsVariantFragment } from "@saleor/graphql"; -import IconChevronDown from "@saleor/icons/ChevronDown"; -import { makeStyles } from "@saleor/macaw-ui"; -import Label from "@saleor/orders/components/OrderHistory/Label"; -import { getById } from "@saleor/orders/components/OrderReturnPage/utils"; -import { ChannelsWithVariantsData } from "@saleor/products/views/ProductUpdate/types"; -import { - areAllChannelVariantsSelected, - channelVariantListingDiffToDict, -} from "@saleor/products/views/ProductUpdate/utils"; -import map from "lodash/map"; -import React, { ChangeEvent } from "react"; -import { defineMessages, useIntl } from "react-intl"; - -import ControlledCheckbox from "../../../components/ControlledCheckbox"; -import Avatar from "../../../components/TableCellAvatar/Avatar"; - -const useStyles = makeStyles( - theme => ({ - variantContainer: { - padding: theme.spacing(2, 0, 2, 4), - }, - channelContainer: { - width: "100%", - }, - channelCheckboxContainer: { - width: "100%", - padding: theme.spacing(2, 0), - }, - channelTitleContainer: { - display: "flex", - flexDirection: "column", - }, - variantTitleContainer: { - display: "flex", - flexDirection: "row", - alignItems: "center", - }, - }), - { name: "ChannelsWithVariantsAvailabilityDialogContent" }, -); - -const useExpanderStyles = makeStyles( - () => ({ - expanded: {}, - root: { - boxShadow: "none", - - "&:before": { - content: "none", - }, - - "&$expanded": { - margin: 0, - border: "none", - }, - }, - }), - { name: "ChannelsWithVariantsAvailabilityExpander" }, -); - -const useSummaryStyles = makeStyles( - theme => ({ - expanded: {}, - root: { - width: "100%", - border: "none", - height: theme.spacing(10), - padding: 0, - margin: 0, - minHeight: 0, - paddingRight: theme.spacing(2.5), - }, - content: { - margin: 0, - }, - }), - { name: "ChannelsWithVariantsAvailabilityChannelSummary" }, -); - -const messages = defineMessages({ - variantsSelectedLabel: { - id: "yHaQWG", - defaultMessage: "{variantsAmount} variants selected", - description: "variants selected label", - }, -}); - -interface ChannelsWithVariantsAvailabilityDialogContentProps { - addVariantToChannel: (channelId: string, variantId: string) => void; - removeVariantFromChannel: (channelId: string, variantId: string) => void; - channelsWithVariants: ChannelsWithVariantsData; - toggleAllChannelVariants: (channelId: string) => void; - isChannelSelected: (channelId: string) => boolean; - channels: ChannelData[]; - allVariants: ProductDetailsVariantFragment[]; -} - -const ChannelsWithVariantsAvailabilityDialogContent: React.FC = ({ - channelsWithVariants, - addVariantToChannel, - removeVariantFromChannel, - toggleAllChannelVariants, - isChannelSelected, - channels, - allVariants, -}) => { - const intl = useIntl(); - const classes = useStyles({}); - const expanderClasses = useExpanderStyles({}); - const summaryClasses = useSummaryStyles({}); - - const handleVariantChange = (channelId: string, variantId: string) => ( - event: ChangeEvent, - ) => - event.target.value - ? addVariantToChannel(channelId, variantId) - : removeVariantFromChannel(channelId, variantId); - - const isChannelPartiallySelected = (channelId: string) => { - const selectedVariants = channelVariantListingDiffToDict( - channelsWithVariants, - )[channelId]; - - if (selectedVariants.length === 0) { - return false; - } - return !areAllChannelVariantsSelected( - allVariants?.map(variant => variant.id), - selectedVariants, - ); - }; - - return ( - <> - {map(channelsWithVariants, ({ selectedVariantsIds }, channelId) => { - const filteredChannel = channels.find(getById(channelId)); - - if (!filteredChannel) { - return null; - } - - const { name } = filteredChannel; - - const isVariantSelected = (variantId: string) => - selectedVariantsIds.includes(variantId); - - const getVariantThumbnailSrc = (variantId: string) => - allVariants.find(getById(variantId)).media[0]?.url || - placeholderImage; - - return ( - - } - classes={summaryClasses} - > -
event.stopPropagation()} - > -
- - {name} -
- } - onChange={() => toggleAllChannelVariants(channelId)} - /> -
- - -
- {allVariants.map(({ id: variantId, name }) => ( - -
- - - {name} -
- } - onChange={handleVariantChange(channelId, variantId)} - /> - - -
- ))} -
- ); - })} - - ); -}; - -export default ChannelsWithVariantsAvailabilityDialogContent; diff --git a/src/channels/components/ChannelsWithVariantsAvailabilityDialog/index.tsx b/src/channels/components/ChannelsWithVariantsAvailabilityDialog/index.tsx deleted file mode 100644 index ff569276d..000000000 --- a/src/channels/components/ChannelsWithVariantsAvailabilityDialog/index.tsx +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./ChannelsWithVariantsAvailabilityDialog"; -export { default } from "./ChannelsWithVariantsAvailabilityDialog"; diff --git a/src/channels/fixtures.ts b/src/channels/fixtures.ts index 693d0e6dd..089086a41 100644 --- a/src/channels/fixtures.ts +++ b/src/channels/fixtures.ts @@ -19,7 +19,7 @@ export const channelCreateErrors: ChannelErrorFragment[] = [ export const channelsList: ChannelDetailsFragment[] = [ { __typename: "Channel", - currencyCode: "euro", + currencyCode: "EUR", hasOrders: false, id: "Q2hhbm5lcDoy", isActive: true, @@ -49,7 +49,7 @@ export const channelsList: ChannelDetailsFragment[] = [ }, { __typename: "Channel", - currencyCode: "euro", + currencyCode: "EUR", hasOrders: false, id: "Q2hhbm7lbDoy213", isActive: true, @@ -79,7 +79,7 @@ export const channelsList: ChannelDetailsFragment[] = [ }, { __typename: "Channel", - currencyCode: "euro", + currencyCode: "EUR", hasOrders: false, id: "Q2hhbn5lbDoytr", isActive: true, @@ -109,7 +109,7 @@ export const channelsList: ChannelDetailsFragment[] = [ }, { __typename: "Channel", - currencyCode: "euro", + currencyCode: "EUR", hasOrders: false, id: "Q2hhbm5lbDo5bot", isActive: true, @@ -139,7 +139,7 @@ export const channelsList: ChannelDetailsFragment[] = [ }, { __typename: "Channel", - currencyCode: "euro", + currencyCode: "EUR", hasOrders: false, id: "Q2hhbm7lbDoyr0tr", isActive: true, @@ -169,7 +169,7 @@ export const channelsList: ChannelDetailsFragment[] = [ }, { __typename: "Channel", - currencyCode: "euro", + currencyCode: "EUR", hasOrders: false, id: "Q2hhbn5lbDoyya", isActive: true, @@ -199,7 +199,7 @@ export const channelsList: ChannelDetailsFragment[] = [ }, { __typename: "Channel", - currencyCode: "euro", + currencyCode: "EUR", hasOrders: false, id: "Q2hhbm5lbDo5w0z", isActive: true, diff --git a/src/channels/utils.ts b/src/channels/utils.ts index 116c64db9..c10e0c91f 100644 --- a/src/channels/utils.ts +++ b/src/channels/utils.ts @@ -42,6 +42,15 @@ export interface ChannelData { unitsSold?: number; } +export interface ProductChannelListingData extends Channel { + isPublished: boolean; + publicationDate: string | null; + availableForPurchase: string; + isAvailableForPurchase: boolean; + visibleInListings: boolean; + currency?: string; +} + export interface ChannelPriceData { id: string; name: string; diff --git a/src/components/CardMenu/CardMenu.tsx b/src/components/CardMenu/CardMenu.tsx index b44a6c582..a28f7d6c5 100644 --- a/src/components/CardMenu/CardMenu.tsx +++ b/src/components/CardMenu/CardMenu.tsx @@ -8,7 +8,7 @@ import { Popper, Typography, } from "@material-ui/core"; -import { makeStyles, SettingsIcon } from "@saleor/macaw-ui"; +import { IconButtonProps, makeStyles, SettingsIcon } from "@saleor/macaw-ui"; import classNames from "classnames"; import React, { useEffect, useRef, useState } from "react"; import { FormattedMessage } from "react-intl"; @@ -32,6 +32,8 @@ export interface CardMenuProps { disabled?: boolean; menuItems: CardMenuItem[]; outlined?: boolean; + Icon?: React.ElementType<{}>; + IconButtonProps?: IconButtonProps; } const useStyles = makeStyles( @@ -64,7 +66,15 @@ const useStyles = makeStyles( ); const CardMenu: React.FC = props => { - const { className, disabled, menuItems, outlined, ...rest } = props; + const { + className, + disabled, + menuItems, + outlined, + Icon: icon, + IconButtonProps = {}, + ...rest + } = props; const classes = useStyles(props); const anchorRef = useRef(null); @@ -120,6 +130,8 @@ const CardMenu: React.FC = props => { const isWithLoading = menuItems.some(({ withLoading }) => withLoading); + const Icon = icon ?? SettingsIcon; + return (
= props => { onClick={handleToggle} variant={outlined ? "primary" : "secondary"} state={open ? "active" : "default"} + {...IconButtonProps} > - + { channels: ChannelData[]; + /** Channels that have no settings */ channelsList: ChannelList[]; errors?: ChannelsAvailabilityError[]; disabled?: boolean; @@ -71,9 +72,9 @@ export const ChannelsAvailability: React.FC = pro return ( void; } @@ -44,6 +51,7 @@ const ColumnPicker: React.FC = props => { initialOpen = false, onSave, query, + IconButtonProps = {}, ...rest } = props; const classes = useStyles(props); @@ -99,6 +107,7 @@ const ColumnPicker: React.FC = props => { setExpansionState(false)}>
setExpansionState(prevState => !prevState)} > diff --git a/src/components/Datagrid/Datagrid.stories.tsx b/src/components/Datagrid/Datagrid.stories.tsx new file mode 100644 index 000000000..0ed503ee9 --- /dev/null +++ b/src/components/Datagrid/Datagrid.stories.tsx @@ -0,0 +1,151 @@ +import { GridCell, Item } from "@glideapps/glide-data-grid"; +import { Button } from "@saleor/macaw-ui"; +import Decorator from "@saleor/storybook/Decorator"; +import { storiesOf } from "@storybook/react"; +import { score } from "fuzzaldrin"; +import sortBy from "lodash/sortBy"; +import throttle from "lodash/throttle"; +import React from "react"; + +import { + booleanCell, + dropdownCell, + moneyCell, + numberCell, + textCell, +} from "./cells"; +import Datagrid, { GetCellContentOpts } from "./Datagrid"; +import { DropdownChoice } from "./DropdownCell"; +import { initialData } from "./fixtures"; +import { numberCellEmptyValue } from "./NumberCell"; +import { + DatagridChangeStateContext, + useDatagridChangeState, +} from "./useDatagridChange"; + +const availableColumns = [ + { title: "ID", id: "uuid", width: 300 }, + { title: "Loaned", id: "loan-active", width: 70 }, + { title: "Loaned Amount", id: "loan", width: 200 }, + { title: "Name", id: "name", width: 200 }, + { title: "Job", id: "job", width: 200 }, + { title: "Balance", id: "balance", width: 200 }, + { title: "Eye color", id: "eyeColor", width: 200 }, + { title: "Age", id: "age", width: 80 }, +] as const; + +const jobChoices = [ + { label: "QA", value: "qa" }, + { label: "Engineer", value: "eng" }, + { label: "Designer", value: "designer" }, + { label: "Director", value: "director" }, +]; + +const getJobChoices = throttle( + (text: string) => + new Promise(resolve => + setTimeout(() => { + resolve( + sortBy(jobChoices, choice => -score(choice.label, text)).slice(0, 2), + ); + }, 500), + ), + 500, +); + +const DefaultStory: React.FC<{ error?: boolean }> = ({ error }) => { + const changeProps = useDatagridChangeState(); + const getCellContent = React.useCallback( + ( + [column, row]: Item, + { changes, getChangeIndex, added, removed }: GetCellContentOpts, + ): GridCell => { + const columnId = availableColumns[column].id; + const change = changes.current[getChangeIndex(columnId, row)]?.data; + const dataRow = added.includes(row) + ? undefined + : initialData[row + removed.filter(r => r <= row).length]; + + if (columnId === "loan-active") { + return booleanCell(change ?? dataRow?.loan.active ?? null); + } + + if (columnId === "loan") { + return moneyCell( + change?.value ?? dataRow?.loan.amount ?? null, + dataRow?.loan.currency ?? "USD", + ); + } + + if (columnId === "balance") { + return moneyCell( + change?.value ?? dataRow?.balance.amount ?? null, + dataRow?.balance.currency ?? "USD", + ); + } + + if (columnId === "job") { + return dropdownCell(change?.value ?? dataRow?.job, { + update: getJobChoices, + }); + } + + if (columnId === "age") { + return numberCell( + change?.value ?? dataRow?.age ?? numberCellEmptyValue, + ); + } + + const data = change ?? (dataRow ? dataRow[columnId] : ""); + return textCell(data); + }, + [], + ); + + const getCellError = React.useCallback(([column, row]: Item): boolean => { + if (!error) { + return false; + } + const columnId = availableColumns[column].id; + + return row === 3 && columnId === "uuid"; + }, []); + + return ( + +
+ [ + { + label: "Do something", + onSelect: () => undefined, + }, + ]} + selectionActions={selection => ( + + )} + rows={initialData.length} + title="Datagrid" + /> +
+
+ ); +}; + +storiesOf("Generics / Datagrid", module) + .addDecorator(Decorator) + .add("default", () => ) + .add("with errors", () => ); diff --git a/src/components/Datagrid/Datagrid.tsx b/src/components/Datagrid/Datagrid.tsx new file mode 100644 index 000000000..1f2f4b2b4 --- /dev/null +++ b/src/components/Datagrid/Datagrid.tsx @@ -0,0 +1,329 @@ +import "@glideapps/glide-data-grid/dist/index.css"; + +import DataEditor, { + DataEditorRef, + EditableGridCell, + GridCell, + GridSelection, + Item, +} from "@glideapps/glide-data-grid"; +import { Card, CardContent, Typography } from "@material-ui/core"; +import { usePreventHistoryBack } from "@saleor/hooks/usePreventHistoryBack"; +import { + Button, + MoreHorizontalIcon, + PlusSmallIcon, + useTheme, +} from "@saleor/macaw-ui"; +import classNames from "classnames"; +import range from "lodash/range"; +import throttle from "lodash/throttle"; +import React from "react"; + +import CardMenu, { CardMenuItem } from "../CardMenu"; +import CardTitle from "../CardTitle"; +import ColumnPicker from "../ColumnPicker"; +import useStyles, { useDatagridTheme } from "./styles"; +import { AvailableColumn } from "./types"; +import useCells from "./useCells"; +import useColumns from "./useColumns"; +import useDatagridChange, { + DatagridChange, + OnDatagridChange, +} from "./useDatagridChange"; + +export interface GetCellContentOpts { + changes: React.MutableRefObject; + added: number[]; + removed: number[]; + getChangeIndex: (column: string, row: number) => number; +} + +export interface MenuItemsActions { + removeRows: (indexes: number[]) => void; +} + +export interface DatagridProps { + addButtonLabel: string; + availableColumns: readonly AvailableColumn[]; + emptyText: string; + getCellError: (item: Item, opts: GetCellContentOpts) => boolean; + getCellContent: (item: Item, opts: GetCellContentOpts) => GridCell; + menuItems: (index: number) => CardMenuItem[]; + rows: number; + title: string; + selectionActions: ( + selection: number[], + actions: MenuItemsActions, + ) => React.ReactNode; + onChange?: OnDatagridChange; +} + +export const Datagrid: React.FC = ({ + addButtonLabel, + availableColumns, + emptyText, + getCellContent, + getCellError, + menuItems, + rows, + selectionActions, + title, + onChange, +}): React.ReactElement => { + const classes = useStyles(); + const datagridTheme = useDatagridTheme(); + const editor = React.useRef(); + + const { + availableColumnsChoices, + columns, + columnChoices, + defaultColumns, + displayedColumns, + onColumnMoved, + onColumnResize, + onColumnsChange, + picker, + } = useColumns(availableColumns); + + const { + added, + onCellEdited, + onRowsRemoved, + changes, + removed, + getChangeIndex, + onRowAdded, + } = useDatagridChange(availableColumns, rows, onChange); + + const theme = useTheme(); + + const [scrolledToRight, setScrolledToRight] = React.useState(false); + const scroller: HTMLDivElement = document.querySelector(".dvn-scroller"); + const scrollerInner: HTMLDivElement = document.querySelector( + ".dvn-scroll-inner", + ); + + usePreventHistoryBack(scroller); + + React.useEffect(() => { + if (!(scroller && scrollerInner)) { + return; + } + + const handler = throttle(() => { + const isScrolledToRight = + scroller.scrollWidth - scroller.clientWidth - scroller.scrollLeft < 2; + setScrolledToRight(isScrolledToRight); + }, 100); + scroller.addEventListener("scroll", handler); + + return () => scroller.removeEventListener("scroll", handler); + }, [scroller, scrollerInner]); + + const getCellContentEnh = React.useCallback( + ([column, row]: Item): GridCell => { + const item = [ + availableColumns.findIndex(ac => ac.id === displayedColumns[column]), + row, + ] as const; + const opts = { changes, added, removed, getChangeIndex }; + const columnId = availableColumns[column].id; + const changed = !!changes.current[getChangeIndex(columnId, row)]?.data; + + return { + ...getCellContent(item, opts), + ...(changed + ? { themeOverride: { bgCell: theme.palette.saleor.active[5] } } + : {}), + ...(getCellError(item, opts) + ? { + themeOverride: { + bgCell: + theme.palette.saleor.theme === "light" + ? theme.palette.saleor.fail.light + : theme.palette.saleor.errorAction[5], + }, + } + : {}), + }; + }, + [getCellContent, availableColumns, displayedColumns, added, removed], + ); + + const onCellEditedEnh = React.useCallback( + ([column, row]: Item, newValue: EditableGridCell): void => { + onCellEdited( + [ + availableColumns.findIndex(ac => ac.id === displayedColumns[column]), + row, + ], + newValue, + ); + editor.current.updateCells( + range(displayedColumns.length).map(offset => ({ + cell: [column + offset, row], + })), + ); + }, + [onCellEdited, getCellContent, availableColumns, displayedColumns], + ); + + const [selection, setSelection] = React.useState(); + + const props = useCells(); + + const removeRows = React.useCallback( + (rows: number[]) => { + if (selection?.rows) { + onRowsRemoved(rows); + setSelection(undefined); + } + }, + [selection, onRowsRemoved], + ); + + const selectionActionsComponent = React.useMemo( + () => + selection?.rows.length > 0 + ? selectionActions(Array.from(selection.rows), { removeRows }) + : null, + [selection, selectionActions, removeRows], + ); + + const rowsTotal = rows - removed.length + added.length; + const hasColumnGroups = columns.some(col => col.group); + + return ( + + + +
+ } + /> + + {rowsTotal > 0 ? ( + <> + {selection?.rows.length > 0 && ( +
+ {selectionActionsComponent} +
+ )} +
+ +
+
+ undefined} + onQueryChange={picker.setQuery} + query={picker.query} + /> +
+ {hasColumnGroups && ( +
+ )} + {Array(rowsTotal) + .fill(0) + .map((_, index) => ( +
+ = rowsTotal - added.length} + Icon={MoreHorizontalIcon} + IconButtonProps={{ + className: classes.columnPickerBtn, + hoverOutline: false, + state: "default", + }} + menuItems={menuItems(index)} + /> +
+ ))} +
+ } + rowMarkerWidth={48} + /> + {/* FIXME: https://github.com/glideapps/glide-data-grid/issues/505 */} + {hasColumnGroups &&
} +
+ + ) : ( + {emptyText} + )} + +
+ + ); +}; +Datagrid.displayName = "Datagrid"; +export default Datagrid; diff --git a/src/components/Datagrid/DropdownCell.tsx b/src/components/Datagrid/DropdownCell.tsx new file mode 100644 index 000000000..7d86e9368 --- /dev/null +++ b/src/components/Datagrid/DropdownCell.tsx @@ -0,0 +1,128 @@ +import { + CustomCell, + CustomCellRenderer, + getMiddleCenterBias, + ProvideEditorCallback, +} from "@glideapps/glide-data-grid"; +import { makeStyles } from "@saleor/macaw-ui"; +import pick from "lodash/pick"; +import React from "react"; + +import SingleAutocompleteSelectField, { + SingleAutocompleteSelectFieldProps, +} from "../SingleAutocompleteSelectField"; +import { Choice } from "../SingleSelectField"; + +export type DropdownChoice = Choice; +export type DropdownCellContentProps = Pick< + SingleAutocompleteSelectFieldProps, + "allowCustomValues" | "emptyOption" +>; + +export type DropdownCellGetSuggestionsFn = ( + text: string, +) => Promise; +interface DropdownCellProps extends DropdownCellContentProps { + readonly choices?: DropdownChoice[]; + readonly update?: DropdownCellGetSuggestionsFn; + readonly kind: "dropdown-cell"; + readonly value: DropdownChoice | null; +} + +export type DropdownCell = CustomCell; + +export const emptyDropdownCellValue: DropdownChoice = { + label: "", + value: null, +}; + +const useStyles = makeStyles( + { + root: { + "& > div": { + padding: 0, + }, + "& input": { + height: "unset", + }, + }, + }, + { name: "DropdownCell" }, +); + +const DropdownCellEdit: ReturnType> = ({ + value: cell, + onFinishedEditing, +}) => { + const [data, setData] = React.useState([]); + const getChoices = React.useCallback( + async (text: string) => { + setData(await cell.data.update(text)); + }, + [cell.data], + ); + const classes = useStyles(); + + const userProps = pick(cell.data, ["allowCustomValues", "emptyOption"]); + const props = cell.data.update + ? { fetchOnFocus: true, fetchChoices: getChoices, choices: data } + : { choices: cell.data.choices }; + + return ( + + onFinishedEditing({ + ...cell, + data: { + ...cell.data, + value: props.choices.find(c => c.value === event.target.value) ?? { + label: event.target.value, + value: event.target.value, + }, + }, + }) + } + name="" + displayValue={cell.data.value.label} + value={cell.data.value.value} + /> + ); +}; + +export const dropdownCellRenderer: CustomCellRenderer = { + isMatch: (c): c is DropdownCell => (c.data as any).kind === "dropdown-cell", + draw: (args, cell) => { + const { ctx, theme, rect } = args; + const { value } = cell.data; + + ctx.fillStyle = theme.textDark; + ctx.fillText( + value.label, + rect.x + 8, + rect.y + rect.height / 2 + getMiddleCenterBias(ctx, theme), + ); + + return true; + }, + provideEditor: () => ({ + editor: DropdownCellEdit, + disablePadding: true, + deletedValue: cell => ({ + ...cell, + copyData: "", + data: { + ...cell.data, + display: "", + value: null, + }, + }), + }), + onPaste: (value, data) => ({ + ...data, + value: { value, label: value } ?? null, + }), +}; diff --git a/src/components/Datagrid/MoneyCell.tsx b/src/components/Datagrid/MoneyCell.tsx new file mode 100644 index 000000000..0272d2a4f --- /dev/null +++ b/src/components/Datagrid/MoneyCell.tsx @@ -0,0 +1,117 @@ +import { + CustomCell, + CustomCellRenderer, + getMiddleCenterBias, + ProvideEditorCallback, +} from "@glideapps/glide-data-grid"; +import React from "react"; + +import { Locale } from "../Locale"; +import { usePriceField } from "../PriceField/usePriceField"; + +interface MoneyCellProps { + readonly kind: "money-cell"; + readonly currency: string; + readonly value: number | null; +} + +export type MoneyCell = CustomCell; + +const MoneyCellEdit: ReturnType> = ({ + value: cell, + onChange: onChangeBase, +}) => { + const { onChange, onKeyDown, minValue, step } = usePriceField( + cell.data.currency, + event => + onChangeBase({ + ...cell, + data: { + ...cell.data, + value: event.target.value, + }, + }), + ); + + return ( + + ); +}; + +const getFractionDigits = (locale: Locale, currency: string) => { + try { + const numberFormat = new Intl.NumberFormat(locale, { + style: "currency", + currency, + }); + + return numberFormat.resolvedOptions().maximumFractionDigits; + } catch (e) { + return 2; + } +}; + +export const moneyCellRenderer = ( + locale: Locale, +): CustomCellRenderer => ({ + isMatch: (c): c is MoneyCell => (c.data as any).kind === "money-cell", + draw: (args, cell) => { + const { ctx, theme, rect } = args; + const { currency, value } = cell.data; + const hasValue = value === 0 ? true : !!value; + const currencyFractionDigits = getFractionDigits(locale, currency); + const formatted = + value?.toLocaleString(locale, { + maximumFractionDigits: currencyFractionDigits, + minimumFractionDigits: currencyFractionDigits, + }) ?? "-"; + + ctx.fillStyle = theme.textDark; + ctx.textAlign = "right"; + ctx.fillText( + formatted, + rect.x + rect.width - 8, + rect.y + rect.height / 2 + getMiddleCenterBias(ctx, theme), + ); + + ctx.save(); + ctx.fillStyle = theme.textMedium; + ctx.textAlign = "left"; + ctx.font = [ + theme.baseFontStyle.replace(/bold/g, "normal"), + theme.fontFamily, + ].join(" "); + ctx.fillText( + hasValue ? currency : "-", + rect.x + 8, + rect.y + rect.height / 2 + getMiddleCenterBias(ctx, theme), + ); + ctx.restore(); + + return true; + }, + provideEditor: () => ({ + editor: MoneyCellEdit, + disablePadding: true, + deletedValue: cell => ({ + ...cell, + copyData: "", + data: { + ...cell.data, + value: cell.data.value ?? null, + }, + }), + }), + onPaste: (value, data) => ({ + ...data, + value: parseFloat(value), + }), +}); diff --git a/src/components/Datagrid/NumberCell.tsx b/src/components/Datagrid/NumberCell.tsx new file mode 100644 index 000000000..f06244f4b --- /dev/null +++ b/src/components/Datagrid/NumberCell.tsx @@ -0,0 +1,74 @@ +import { + CustomCell, + CustomCellRenderer, + getMiddleCenterBias, + ProvideEditorCallback, +} from "@glideapps/glide-data-grid"; +import React from "react"; + +import { Locale } from "../Locale"; + +export const numberCellEmptyValue = Symbol(); +interface NumberCellProps { + readonly kind: "number-cell"; + readonly value: number | typeof numberCellEmptyValue; +} + +export type NumberCell = CustomCell; + +const NumberCellEdit: ReturnType> = ({ + value: cell, + onChange, +}) => ( + + onChange({ + ...cell, + data: { + ...cell.data, + value: event.target.value ? parseFloat(event.target.value) : null, + }, + }) + } + value={cell.data.value === numberCellEmptyValue ? "" : cell.data.value} + autoFocus + /> +); + +export const numberCellRenderer = ( + locale: Locale, +): CustomCellRenderer => ({ + isMatch: (c): c is NumberCell => (c.data as any).kind === "number-cell", + draw: (args, cell) => { + const { ctx, theme, rect } = args; + const { value } = cell.data; + const formatted = + value === numberCellEmptyValue ? "-" : value.toLocaleString(locale); + ctx.fillStyle = theme.textDark; + ctx.textAlign = "right"; + ctx.fillText( + formatted, + rect.x + rect.width - 8, + rect.y + rect.height / 2 + getMiddleCenterBias(ctx, theme), + ); + + return true; + }, + provideEditor: () => ({ + editor: NumberCellEdit, + disablePadding: true, + deletedValue: cell => ({ + ...cell, + copyData: "", + data: { + ...cell.data, + value: numberCellEmptyValue, + }, + }), + }), + onPaste: (value, data) => ({ + ...data, + value: value ? parseFloat(value) : numberCellEmptyValue, + }), +}); diff --git a/src/components/Datagrid/cells.ts b/src/components/Datagrid/cells.ts new file mode 100644 index 000000000..49dbf79f8 --- /dev/null +++ b/src/components/Datagrid/cells.ts @@ -0,0 +1,82 @@ +import { GridCell, GridCellKind } from "@glideapps/glide-data-grid"; +import { + NumberCell, + numberCellEmptyValue, +} from "@saleor/components/Datagrid/NumberCell"; + +import { + DropdownCell, + DropdownCellContentProps, + DropdownChoice, +} from "./DropdownCell"; +import { MoneyCell } from "./MoneyCell"; + +const common = { + allowOverlay: true, + readonly: false, +}; + +export function textCell(value: string): GridCell { + return { + ...common, + data: value, + displayData: value, + kind: GridCellKind.Text, + }; +} + +export function booleanCell(value: boolean): GridCell { + return { + ...common, + allowOverlay: false, + kind: GridCellKind.Boolean, + data: value, + }; +} + +export function numberCell( + value: number | typeof numberCellEmptyValue, +): NumberCell { + return { + ...common, + data: { + kind: "number-cell", + value, + }, + kind: GridCellKind.Custom, + copyData: value !== numberCellEmptyValue ? value.toString() : "", + }; +} + +export function moneyCell(value: number | null, currency: string): MoneyCell { + return { + ...common, + kind: GridCellKind.Custom, + data: { + kind: "money-cell", + value, + currency, + }, + copyData: value?.toString() ?? "", + }; +} + +export function dropdownCell( + value: DropdownChoice, + opts: DropdownCellContentProps & + ( + | { choices: DropdownChoice[] } + | { update: (text: string) => Promise } + ), +): DropdownCell { + return { + ...common, + data: { + ...opts, + kind: "dropdown-cell", + value, + }, + kind: GridCellKind.Custom, + copyData: value.label, + }; +} diff --git a/src/components/Datagrid/datagrid.md b/src/components/Datagrid/datagrid.md new file mode 100644 index 000000000..1056d79de --- /dev/null +++ b/src/components/Datagrid/datagrid.md @@ -0,0 +1,74 @@ +# Datagrid + +Datagrid component is a wrapper for NPM's package `@glideapps/glide-data-grid`, providing easy (ok, at least easiest than implementing it from scratch) integration with Saleor Dashboard's forms. + +## Forms + +A good and easy example would be `src/components/Datagrid/Datagrid.stories.tsx` file as it represents minimal setup to get datagrid component up and running. + +The most important thing is to provide React Context (avoid prop-drilling through multiple intermediate components) in form component definition: + +```jsx +const Form: React.FC = ({ children }) => { + // This one is not needed by datagrid component and is put here just because + // we assumed it's a form component + const formProps = useForm({}); + const datagridProps = useDatagridChangeState(); + + return ( + + {children} + + ); +}; +``` + +It creates a state which holds _all changes_ made in datagrid. Please not that it holds only changes and not whole state, which greatly simplifies pruning identity mutations (updating object with the same data). When the context is set, the next step begins. + +## Accessors + +Original component from `@glideapps/glide-data-grid` requires user to provide callback function to get data, instead of passing 2D array. [Docs can be found here](https://glideapps.github.io/glide-data-grid/?path=/story/glide-data-grid-docs--getting-started), but we introduced a few additional concepts. + +- `getCellContent` is augmented with additional data denoted by following interface: + + ```typescript + export interface GetCellContentOpts { + changes: React.MutableRefObject; + added: number[]; + removed: number[]; + getChangeIndex: (column: string, row: number) => number; + } + ``` + + It allows us to correctly map indices used by datagrid to original data from API (or any other source) and to display updated data instead of original one. + + Note that `getCellContent` should return not only display value but all settings regarding particular cell. To make this easier and less mundane work, we created utility functions which can be found in `src/components/Datagrid/cells.ts` file. + +- we introduces `getCellError` accessor to be able to map validation errors to their respective cells - this way we can handle partial save (let's say save 3 out of 4 updated variants) and display red background where validation failed. It uses the same `GetCellContentOpts` object as second argument as `getCellContent`. + +## Adding and removing rows + +Datagrid component fully supports adding and removing rows on the fly. + +- Adding rows work out-of-the-box by passing `addButtonLabel` as prop to component. +- `selectionActions` prop is used for all actions that appear after selecting rows, such as deletion or publication. You can pass a list of buttons that use actions from `MenuItemsActions` interface. Currently only deleting rows is supported out of the box. + +## API + +After we set up form and accessors, we need a way to send this data to API. This will vary depending on available API (especially if we can use bulk update API or not). Implementation stitching five different mutations can be found in `src/products/views/ProductUpdate/handlers/useProductUpdateHandler.ts` file. It's important to reduce response size, time and render count by requesting only error list and refetch whole list after all mutations are fulfilled. + +## Error Handling + +This part is also greatly affected by the API as it requires mapping potentially various GraphQL objects to combination of row and column ID. What we found useful during implementing datagrid in variants list is creating objects, which contain data like variant ID, warehouse/channel ID and error code, which then `getCellError` can easily interpret and pin to particular cell. This code can be found in `src/products/views/ProductUpdate/handlers/errors.ts` file. + +After successful submission to API we need to clean all changes that were saved and leave those which weren't. In `src/products/components/ProductUpdatePage/form.tsx` we can see that after submission changes are cleared if no error was found. Beacuse of possibility that data in one row can be saved using multiple mutations, developers should write their own logic to specify which fields were saved and which were not, given the list of errors. + +## Summary + +Wrapping everything up - to use datagrid component, you need to take care of: + +1. connecting it to form +2. create accessors `getCellContent` +3. create save handler +4. update list of errors +5. clean changes array from already saved ones. diff --git a/src/components/Datagrid/fixtures.ts b/src/components/Datagrid/fixtures.ts new file mode 100644 index 000000000..cbda02e28 --- /dev/null +++ b/src/components/Datagrid/fixtures.ts @@ -0,0 +1,172 @@ +export const initialData = [ + { + id: "1", + uuid: "627bd0e7476cd1000ecef6b4", + balance: { amount: 3067.88, currency: "EUR" }, + age: 40, + eyeColor: "green", + name: "Melba Keller", + loan: { + active: true, + amount: 10.0, + currency: "USD", + }, + job: { + label: "Engineer", + value: "eng", + }, + }, + { + id: "2", + uuid: "627bd0e763eb9e68f3800b9f", + balance: { amount: 3523.57, currency: "EUR" }, + age: 39, + eyeColor: "green", + name: "Dixie Rivera", + loan: { + active: true, + amount: 151.0, + currency: "USD", + }, + job: { + label: "Engineer", + value: "eng", + }, + }, + { + id: "3", + uuid: "627bd0e785b17c7a9b406c62", + balance: { amount: 3240.22, currency: "EUR" }, + age: 38, + eyeColor: "brown", + name: "Lamb Clayton", + loan: { + active: true, + amount: 21.0, + currency: "USD", + }, + job: { + label: "Director", + value: "director", + }, + }, + { + id: "4", + uuid: "627bd0e7c6460044c736e00a", + balance: { amount: 3450.59, currency: "EUR" }, + age: 30, + eyeColor: "blue", + name: "Charity Mcgee", + loan: { + active: true, + amount: 52.0, + currency: "USD", + }, + job: { + label: "QA", + value: "qa", + }, + }, + { + id: "5", + uuid: "627bd0e77792e92a1b00d09c", + balance: { amount: 3196.74, currency: "EUR" }, + age: 39, + eyeColor: "brown", + name: "Ward Hayden", + loan: { + active: true, + amount: 991.0, + currency: "USD", + }, + job: { + label: "QA", + value: "qa", + }, + }, + { + id: "6", + uuid: "627bd0e78ff0fc7e94570ecd", + balance: { amount: 2222.02, currency: "EUR" }, + age: 30, + eyeColor: "blue", + name: "Emily Parrish", + loan: { + active: true, + amount: 82.0, + currency: "USD", + }, + job: { + label: "QA", + value: "qa", + }, + }, + { + id: "7", + uuid: "627bd0e7d0b1a0050a83619b", + balance: { amount: 3558.55, currency: "EUR" }, + age: 37, + eyeColor: "blue", + name: "Wood Hensley", + loan: { + active: false, + amount: null, + currency: "USD", + }, + job: { + label: "Engineer", + value: "eng", + }, + }, + { + id: "8", + uuid: "627bd0e7138637a41c457158", + balance: { amount: 1955.85, currency: "EUR" }, + age: 29, + eyeColor: "green", + name: "Karin Cote", + loan: { + active: true, + amount: 1.1, + currency: "USD", + }, + job: { + label: "Designer", + value: "designer", + }, + }, + { + id: "9", + uuid: "627bd0e7c3502435bc459efe", + balance: { amount: -2358.54, currency: "EUR" }, + age: 37, + eyeColor: "green", + name: "Roberta Rollins", + loan: { + active: true, + amount: 1000.0, + currency: "USD", + }, + job: { + label: "Designer", + value: "designer", + }, + }, + { + id: "10", + uuid: "627bd0e7da2e10b6154b85cf", + balance: { amount: 2048.1, currency: "EUR" }, + age: 26, + eyeColor: "green", + name: "Francis Talley", + loan: { + active: false, + amount: null, + currency: "USD", + }, + job: { + label: "Manager", + value: "manager", + }, + }, +]; diff --git a/src/components/Datagrid/styles.ts b/src/components/Datagrid/styles.ts new file mode 100644 index 000000000..09df78b7c --- /dev/null +++ b/src/components/Datagrid/styles.ts @@ -0,0 +1,211 @@ +import { Theme } from "@glideapps/glide-data-grid"; +import { Typography } from "@material-ui/core/styles/createTypography"; +import { makeStyles, useTheme } from "@saleor/macaw-ui"; +import { useMemo } from "react"; + +const useStyles = makeStyles( + theme => { + const rowActionSelected = { + background: theme.palette.background.paper, + color: theme.palette.saleor.main[1], + }; + const activeBorderColor = + theme.palette.saleor.theme === "light" ? "#D4D4D4" : "#232323"; + + return { + actionBtnBar: { + position: "absolute", + zIndex: 1, + background: theme.palette.background.paper, + borderRadius: 8, + // Right and left toolbars + width: "calc(100% - 64px - 48px - 1px)", + marginTop: 1, + marginLeft: 50, + height: 48, + display: "flex", + alignItems: "center", + justifyContent: "flex-end", + padding: theme.spacing(1), + }, + btnContainer: { + display: "flex", + flexDirection: "row-reverse", + }, + addBtn: { + marginBottom: theme.spacing(2), + }, + columnPicker: { + display: "flex", + alignItems: "center", + justifyContent: "center", + height: 48, + }, + columnPickerBtn: { + "&:hover": { + color: theme.palette.saleor.main[1], + }, + }, + portal: { + "& input::-webkit-outer-spin-button, input::-webkit-inner-spin-button": { + appearance: "none", + margin: 0, + }, + "& input[type=number]": { + appearance: "textfield", + }, + "& .clip-region": { + border: `1px solid ${theme.palette.saleor.main[1]}`, + }, + "& .gdg-growing-entry": { + flex: 1, + marginTop: 0, + }, + "& .gdg-style": { + background: theme.palette.background.paper, + border: "none", + // Setting these with !important because we never intend to style + // this particular element, like, never ever + boxShadow: "none !important", + padding: "0 !important", + }, + "& input, & textarea": { + ...theme.typography.body1, + appearance: "none", + background: "none", + border: "none", + fontSize: theme.typography.body1.fontSize, + letterSpacing: "0.44px", + padding: `1.4rem ${theme.spacing(1)}`, + outline: 0, + }, + '& input[type="number"]': { + textAlign: "right", + width: "100%", + }, + position: "fixed", + top: 0, + left: 0, + }, + datagrid: { + "& .dvn-scroller": { + overscrollBehaviorX: "none", + }, + borderTop: `1px solid ${theme.palette.divider}`, + borderBottom: `1px solid ${theme.palette.divider}`, + borderRadius: 0, + boxSizing: "content-box", + width: "100%", + paddingBottom: "1px", + }, + root: { + position: "relative", + }, + rowActionBar: { + height: "100%", + background: theme.palette.background.paper, + borderLeft: `1px solid ${activeBorderColor}`, + width: 48, + }, + rowActionBarScrolledToRight: { + borderLeftColor: theme.palette.divider, + }, + rowAction: { + "&:hover, $rowActionSelected": { + rowActionSelected, + }, + "&:not(:last-child)": { + marginBottom: -1, + }, + border: `1px solid ${theme.palette.divider}`, + borderLeftColor: activeBorderColor, + borderRight: "none", + cursor: "pointer", + color: theme.palette.saleor.main[5], + marginLeft: -1, + display: "flex", + alignItems: "center", + justifyContent: "center", + height: 47, + }, + rowActionScrolledToRight: { + borderLeftColor: theme.palette.divider, + }, + columnGroupFixer: { + position: "absolute", + top: 1, + left: 0, + height: 48, + width: 10, + borderLeft: 0, + background: theme.palette.background.paper, + }, + editorContainer: { + position: "relative", + }, + rowActionBarShadow: { + height: "100%", + width: 1, + position: "absolute", + zIndex: -1, + transition: theme.transitions.create("box-shadow", { + duration: theme.transitions.duration.short, + }), + boxShadow: "-1px 0px 12px transparent", + }, + rowActionBarShadowActive: { + boxShadow: "-1px 0px 12px rgba(0, 0, 0, 0.80)", + }, + rowActionSelected, + cardContentRoot: { + padding: "0 0 2.4rem 0", + }, + }; + }, + { name: "Datagrid" }, +); + +const calculateFontToPx = (remValue: string | number, base: number) => { + if (typeof remValue === "string") { + return `${parseFloat(remValue) * base}px`; + } + + return `${remValue * base}px`; +}; + +type HtmlTypography = Typography & { htmlFontSize: number }; + +export function useDatagridTheme() { + const theme = useTheme(); + + const base = (theme.typography as HtmlTypography).htmlFontSize * 0.625; + + const datagridTheme = useMemo( + (): Partial => ({ + accentColor: theme.palette.primary.main, + accentLight: theme.palette.background.default, + accentFg: "transparent", + bgCell: theme.palette.background.paper, + bgHeader: theme.palette.background.paper, + bgHeaderHasFocus: theme.palette.background.paper, + bgHeaderHovered: theme.palette.background.paper, + bgBubbleSelected: theme.palette.background.paper, + textHeader: theme.palette.text.secondary, + borderColor: theme.palette.divider, + fontFamily: theme.typography.fontFamily, + baseFontStyle: calculateFontToPx(theme.typography.body1.fontSize, base), + headerFontStyle: calculateFontToPx(theme.typography.body2.fontSize, base), + editorFontSize: calculateFontToPx(theme.typography.body1.fontSize, base), + textMedium: theme.palette.text.primary, + textGroupHeader: theme.palette.text.secondary, + textBubble: theme.palette.text.primary, + textDark: theme.palette.text.primary, + textLight: theme.palette.text.primary, + }), + [theme], + ); + + return datagridTheme; +} + +export default useStyles; diff --git a/src/components/Datagrid/types.ts b/src/components/Datagrid/types.ts new file mode 100644 index 000000000..9ae45df0e --- /dev/null +++ b/src/components/Datagrid/types.ts @@ -0,0 +1,6 @@ +export interface AvailableColumn { + id: string; + title: string; + width: number; + group?: string; +} diff --git a/src/components/Datagrid/useCells.ts b/src/components/Datagrid/useCells.ts new file mode 100644 index 000000000..c395ec840 --- /dev/null +++ b/src/components/Datagrid/useCells.ts @@ -0,0 +1,23 @@ +import { useCustomCells } from "@glideapps/glide-data-grid"; +import useLocale from "@saleor/hooks/useLocale"; +import { useMemo } from "react"; + +import { dropdownCellRenderer } from "./DropdownCell"; +import { moneyCellRenderer } from "./MoneyCell"; +import { numberCellRenderer } from "./NumberCell"; + +function useCells() { + const { locale } = useLocale(); + const value = useMemo( + () => [ + moneyCellRenderer(locale), + numberCellRenderer(locale), + dropdownCellRenderer, + ], + [locale], + ); + + return useCustomCells(value); +} + +export default useCells; diff --git a/src/components/Datagrid/useColumns.ts b/src/components/Datagrid/useColumns.ts new file mode 100644 index 000000000..761ff4b45 --- /dev/null +++ b/src/components/Datagrid/useColumns.ts @@ -0,0 +1,85 @@ +import { GridColumn } from "@glideapps/glide-data-grid"; +import useStateFromProps from "@saleor/hooks/useStateFromProps"; +import { addAtIndex, removeAtIndex } from "@saleor/utils/lists"; +import { useCallback, useMemo, useState } from "react"; + +import { AvailableColumn } from "./types"; + +function useColumns(availableColumns: readonly AvailableColumn[]) { + const [query, setQuery] = useState(""); + const [displayedColumns, setDisplayedColumns] = useStateFromProps( + availableColumns.map(({ id }) => id), + ); + const [columnState, setColumnState] = useStateFromProps(availableColumns); + + const onColumnMoved = useCallback( + (startIndex: number, endIndex: number): void => { + setDisplayedColumns(old => + addAtIndex(old[startIndex], removeAtIndex(old, startIndex), endIndex), + ); + }, + [], + ); + const onColumnResize = useCallback( + (column: GridColumn, newSize: number) => + setColumnState(prevColumns => + prevColumns.map(prevColumn => + prevColumn.id === column.id + ? { ...prevColumn, width: newSize } + : prevColumn, + ), + ), + [], + ); + const onColumnsChange = useCallback( + (picked: string[]) => + setDisplayedColumns(prevColumns => [ + ...prevColumns.filter(column => picked.includes(column)), + ...picked + .filter(column => !prevColumns.find(c => c === column)) + .map(column => availableColumns.find(ac => ac.id === column).id), + ]), + [availableColumns], + ); + + const columns = useMemo( + () => displayedColumns.map(id => columnState.find(ac => ac.id === id)), + [displayedColumns, columnState], + ); + const columnChoices = useMemo( + () => + columns.map(({ id, title }) => ({ + label: title, + value: id, + })), + [columns], + ); + const availableColumnsChoices = useMemo( + () => + availableColumns.map(({ id, title }) => ({ + label: title, + value: id, + })), + [availableColumns], + ); + const defaultColumns = useMemo(() => availableColumns.map(({ id }) => id), [ + availableColumns, + ]); + + return { + availableColumnsChoices, + columns, + columnChoices, + defaultColumns, + displayedColumns, + onColumnMoved, + onColumnResize, + onColumnsChange, + picker: { + query, + setQuery, + }, + }; +} + +export default useColumns; diff --git a/src/components/Datagrid/useDatagridChange.test.tsx b/src/components/Datagrid/useDatagridChange.test.tsx new file mode 100644 index 000000000..670691624 --- /dev/null +++ b/src/components/Datagrid/useDatagridChange.test.tsx @@ -0,0 +1,131 @@ +import { act, renderHook } from "@testing-library/react-hooks"; +import React from "react"; + +import { AvailableColumn } from "./types"; +import useDatagridChange, { + DatagridChangeStateContext, + useDatagridChangeState, +} from "./useDatagridChange"; + +const columns: AvailableColumn[] = [ + { id: "name", title: "Name", width: 100 }, + { id: "sku", title: "SKU", width: 100 }, + { id: "size", title: "Size", width: 100 }, +]; + +const GridContext = ({ children }) => { + const stateProps = useDatagridChangeState(); + + return ( + + {children} + + ); +}; + +function setupHook() { + return renderHook(() => useDatagridChange(columns, 10), { + wrapper: GridContext, + }); +} + +describe("useDatagridChange", () => { + it("properly adds row", () => { + const { result } = setupHook(); + + act(result.current.onRowAdded); + + expect(result.current.added).toHaveLength(1); + expect(result.current.added[0]).toBe(10); + }); + + it("properly removes rows", () => { + const { result } = setupHook(); + + act(() => { + result.current.onRowsRemoved([7]); + }); + act(() => { + result.current.onRowsRemoved([7]); + }); + + expect(result.current.removed).toHaveLength(2); + expect(result.current.removed[0]).toBe(7); + expect(result.current.removed[1]).toBe(8); + }); + + it("properly removes added rows", () => { + const { result } = setupHook(); + + act(result.current.onRowAdded); + act(() => { + result.current.onRowsRemoved([7]); + }); + act(() => { + result.current.onRowsRemoved([9]); + }); + + expect(result.current.added).toHaveLength(0); + expect(result.current.removed).toHaveLength(1); + expect(result.current.removed[0]).toBe(7); + }); + + it("properly removes added rows 2", () => { + const { result } = setupHook(); + + act(result.current.onRowAdded); + act(result.current.onRowAdded); + act(() => { + result.current.onRowsRemoved([10]); + }); + + expect(result.current.added).toHaveLength(1); + expect(result.current.added[0]).toBe(10); + expect(result.current.removed).toHaveLength(0); + }); + + it("properly removes added rows 3", () => { + const { result } = setupHook(); + + act(result.current.onRowAdded); + act(result.current.onRowAdded); + act(() => { + result.current.onRowsRemoved([10]); + }); + act(() => { + result.current.onRowsRemoved([10]); + }); + + expect(result.current.added).toHaveLength(0); + expect(result.current.removed).toHaveLength(0); + }); + + it("properly updates changes after row removal", () => { + const { result } = setupHook(); + + act(() => { + result.current.onCellEdited([1, 1], {} as any); + }); + act(() => { + result.current.onRowsRemoved([0]); + }); + + expect(result.current.changes.current).toHaveLength(1); + expect(result.current.changes.current[0].row).toBe(0); + }); + + it("properly updates changes after row removal 2", () => { + const { result } = setupHook(); + + act(result.current.onRowAdded); + act(() => { + result.current.onCellEdited([1, 10], {} as any); + }); + act(() => { + result.current.onRowsRemoved([0]); + }); + + expect(result.current.changes.current).toHaveLength(1); + expect(result.current.changes.current[0].row).toBe(9); + }); +}); diff --git a/src/components/Datagrid/useDatagridChange.ts b/src/components/Datagrid/useDatagridChange.ts new file mode 100644 index 000000000..c196a599b --- /dev/null +++ b/src/components/Datagrid/useDatagridChange.ts @@ -0,0 +1,147 @@ +import { EditableGridCell, Item } from "@glideapps/glide-data-grid"; +import { updateAtIndex } from "@saleor/utils/lists"; +import { + createContext, + Dispatch, + MutableRefObject, + SetStateAction, + useCallback, + useContext, + useRef, + useState, +} from "react"; + +import { AvailableColumn } from "./types"; + +export interface DatagridChange { + data: any; + row: number; + column: string; +} + +export interface DatagridChangeOpts { + added: number[]; + removed: number[]; + updates: DatagridChange[]; +} +export type OnDatagridChange = (opts: DatagridChangeOpts) => void; + +export interface UseDatagridChangeState { + added: number[]; + setAdded: Dispatch>; + removed: number[]; + setRemoved: Dispatch>; + changes: MutableRefObject; +} +export function useDatagridChangeState(): UseDatagridChangeState { + const [added, setAdded] = useState([]); + const [removed, setRemoved] = useState([]); + const changes = useRef([]); + + return { + added, + setAdded, + removed, + setRemoved, + changes, + }; +} + +export const DatagridChangeStateContext = createContext( + undefined, +); +export const useDatagridChangeStateContext = () => + useContext(DatagridChangeStateContext); + +function useDatagridChange( + availableColumns: readonly AvailableColumn[], + rows: number, + onChange?: OnDatagridChange, +) { + const { + added, + setAdded, + removed, + setRemoved, + changes, + } = useDatagridChangeStateContext(); + const getChangeIndex = useCallback( + (column: string, row: number): number => + changes.current.findIndex( + change => change.column === column && change.row === row, + ), + [], + ); + + const notify = useCallback( + (updates: DatagridChange[], added: number[], removed: number[]) => { + if (onChange) { + onChange({ + updates, + removed, + added, + }); + } + }, + [onChange], + ); + + const onCellEdited = useCallback( + ([column, row]: Item, newValue: EditableGridCell): void => { + const columnId = availableColumns[column].id; + const existingIndex = getChangeIndex(columnId, row); + const update = { data: newValue.data, column: columnId, row }; + changes.current = + existingIndex === -1 + ? [...changes.current, update] + : updateAtIndex(update, changes.current, existingIndex); + notify(changes.current, added, removed); + }, + [availableColumns, notify, added, removed], + ); + + const onRowsRemoved = useCallback( + (rows: number[]) => { + const getRowOffset = (row: number) => rows.filter(r => r < row).length; + const newAdded = added + .filter(row => !rows.includes(row)) + .map(row => row - getRowOffset(row)); + const newRemoved = [ + ...removed, + ...rows + .filter(row => !added.includes(row)) + .map(row => row + removed.filter(r => r <= row).length), + ]; + + setRemoved(newRemoved); + changes.current = changes.current + .filter(change => !rows.includes(change.row)) + .map(change => ({ + ...change, + row: change.row - getRowOffset(change.row), + })); + setAdded(newAdded); + + notify(changes.current, newAdded, newRemoved); + }, + [added, removed, notify], + ); + + const onRowAdded = useCallback(() => { + const newAdded = [...added, rows - removed.length + added.length]; + setAdded(newAdded); + notify(changes.current, newAdded, removed); + }, [added, notify, removed, rows]); + + return { + added, + changes, + removed, + getChangeIndex, + onCellEdited, + onRowsRemoved, + onRowAdded, + }; +} + +export default useDatagridChange; diff --git a/src/components/Grid/Grid.tsx b/src/components/Grid/Grid.tsx index 47f671394..0b3ef3f38 100644 --- a/src/components/Grid/Grid.tsx +++ b/src/components/Grid/Grid.tsx @@ -20,7 +20,7 @@ const useStyles = makeStyles( }, root: { "& > div": { - overflow: "hidden", + minWidth: 0, }, display: "grid", gridColumnGap: theme.spacing(3), @@ -33,11 +33,8 @@ const useStyles = makeStyles( uniform: { gridTemplateColumns: "1fr 1fr", }, - richText: { - "&& > div": { - overflow: "visible", - }, - }, + // TODO: Remove me + richText: {}, }), { name: "Grid" }, ); diff --git a/src/components/PriceField/PriceField.tsx b/src/components/PriceField/PriceField.tsx index 523cdbc02..2cee83ac7 100644 --- a/src/components/PriceField/PriceField.tsx +++ b/src/components/PriceField/PriceField.tsx @@ -1,12 +1,9 @@ -import { InputAdornment, TextField, TextFieldProps } from "@material-ui/core"; +import { InputAdornment, TextField } from "@material-ui/core"; import { InputProps } from "@material-ui/core/Input"; -import { FormChange } from "@saleor/hooks/useForm"; import { makeStyles } from "@saleor/macaw-ui"; -import React, { useMemo } from "react"; -import { FormattedMessage } from "react-intl"; +import React from "react"; -import { SEPARATOR_CHARACTERS } from "./consts"; -import { findPriceSeparator, getCurrencyDecimalPoints } from "./utils"; +import { usePriceField } from "./usePriceField"; const useStyles = makeStyles( theme => ({ @@ -40,7 +37,7 @@ interface PriceFieldProps { hint?: string; label?: string; name?: string; - value?: string | number; + value?: string; InputProps?: InputProps; inputProps?: InputProps["inputProps"]; required?: boolean; @@ -56,7 +53,7 @@ export const PriceField: React.FC = props => { hint = "", currencySymbol, name, - onChange, + onChange: onChangeBase, required, value, InputProps, @@ -64,66 +61,16 @@ export const PriceField: React.FC = props => { } = props; const classes = useStyles(props); - const minValue = 0; - - const maxDecimalLength = useMemo( - () => getCurrencyDecimalPoints(currencySymbol), - [currencySymbol], + const { onChange, onKeyDown, minValue, step } = usePriceField( + currencySymbol, + onChangeBase, ); - const handleChange: FormChange = e => { - let value = e.target.value; - const splitCharacter = findPriceSeparator(value); - const [integerPart, decimalPart] = value.split(splitCharacter); - - if (maxDecimalLength === 0 && decimalPart) { - // this shouldn't happen - decimal character should be ignored - value = integerPart; - } - - if (decimalPart?.length > maxDecimalLength) { - const shortenedDecimalPart = decimalPart.slice(0, maxDecimalLength); - value = `${integerPart}${splitCharacter}${shortenedDecimalPart}`; - } - - onChange({ - target: { - name: e.target.name, - value, - }, - }); - }; - - const handleKeyPress: TextFieldProps["onKeyDown"] = e => { - // disallow entering e (exponent) - if (e.key === "e" || e.key === "E") { - e.preventDefault(); - } - // ignore separator input when currency doesn't support decimal values - if ( - maxDecimalLength === 0 && - SEPARATOR_CHARACTERS.some(separator => e.key === separator) - ) { - e.preventDefault(); - } - }; - return ( - ) : ( - "" - ) - } + error={error} + helperText={hint} label={label} fullWidth value={value} @@ -138,7 +85,7 @@ export const PriceField: React.FC = props => { ), inputProps: { min: 0, - step: 1 / Math.pow(10, maxDecimalLength), + step, ...InputProps?.inputProps, }, type: "number", @@ -151,8 +98,8 @@ export const PriceField: React.FC = props => { name={name} disabled={disabled} required={required} - onChange={handleChange} - onKeyDown={handleKeyPress} + onChange={onChange} + onKeyDown={onKeyDown} /> ); }; diff --git a/src/components/PriceField/usePriceField.ts b/src/components/PriceField/usePriceField.ts new file mode 100644 index 000000000..a3f770da1 --- /dev/null +++ b/src/components/PriceField/usePriceField.ts @@ -0,0 +1,60 @@ +import { TextFieldProps } from "@material-ui/core"; +import { FormChange } from "@saleor/hooks/useForm"; +import { useMemo } from "react"; + +import { SEPARATOR_CHARACTERS } from "./consts"; +import { findPriceSeparator, getCurrencyDecimalPoints } from "./utils"; + +export function usePriceField(currency: string, onChange: FormChange) { + const minValue = 0; + + const maxDecimalLength = useMemo(() => getCurrencyDecimalPoints(currency), [ + currency, + ]); + + const handleChange: FormChange = e => { + let value = e.target.value; + const splitCharacter = findPriceSeparator(value); + const [integerPart, decimalPart] = value.split(splitCharacter); + + if (maxDecimalLength === 0 && decimalPart) { + // This shouldn't happen - decimal character should be ignored + value = integerPart; + } + + if (decimalPart?.length > maxDecimalLength) { + const shortenedDecimalPart = decimalPart.slice(0, maxDecimalLength); + value = `${integerPart}${splitCharacter}${shortenedDecimalPart}`; + } + + onChange({ + target: { + name: e.target.name, + value: value ? parseFloat(value) : null, + }, + }); + }; + + const handleKeyDown: TextFieldProps["onKeyDown"] = e => { + // Disallow entering e (exponent) + if (e.key === "e" || e.key === "E" || e.key === "-") { + e.preventDefault(); + } + // ignore separator input when currency doesn't support decimal values + if ( + maxDecimalLength === 0 && + SEPARATOR_CHARACTERS.some(separator => e.key === separator) + ) { + e.preventDefault(); + } + }; + + const step = 1 / Math.pow(10, maxDecimalLength); + + return { + onChange: handleChange, + onKeyDown: handleKeyDown, + minValue, + step, + }; +} diff --git a/src/components/SingleAutocompleteSelectField/SingleAutocompleteSelectField.tsx b/src/components/SingleAutocompleteSelectField/SingleAutocompleteSelectField.tsx index ea7f2c798..0b108a059 100644 --- a/src/components/SingleAutocompleteSelectField/SingleAutocompleteSelectField.tsx +++ b/src/components/SingleAutocompleteSelectField/SingleAutocompleteSelectField.tsx @@ -92,10 +92,10 @@ const SingleAutocompleteSelectFieldComponent: React.FC - {debounceFn => ( + {fetchChoicesDebounced => ( displayValue || ""} - onInputValueChange={value => debounceFn(value)} + onInputValueChange={value => fetchChoicesDebounced(value)} onSelect={handleChange} selectedItem={value || ""} // this is to prevent unwanted state updates when the dropdown is closed with an empty value, @@ -213,7 +213,11 @@ const SingleAutocompleteSelectFieldComponent: React.FC +
+ forward(operation).map(data => ({ + ...data, + extensions: { + ...data.extensions, + variables: operation.variables, + }, + })), +); + +export const link = attachVariablesLink.concat( + createUploadLink({ + credentials: "include", + uri: API_URI, + fetch: createFetch(), + }), +); export const apolloClient = new ApolloClient({ cache: new InMemoryCache({ diff --git a/src/graphql/hooks.generated.ts b/src/graphql/hooks.generated.ts index 3206202d6..811e4f112 100644 --- a/src/graphql/hooks.generated.ts +++ b/src/graphql/hooks.generated.ts @@ -1901,7 +1901,7 @@ export const ProductVariantAttributesFragmentDoc = gql` } productType { id - variantAttributes(variantSelection: VARIANT_SELECTION) { + variantAttributes { id name inputType @@ -1968,7 +1968,16 @@ export const ProductDetailsVariantFragmentDoc = gql` id sku name - margin + attributes { + attribute { + id + name + } + values { + id + name + } + } media { url(size: 200) } @@ -2113,6 +2122,7 @@ export const ProductVariantFragmentDoc = gql` url } channelListings { + id publicationDate isPublished channel { @@ -11272,18 +11282,14 @@ export type ProductVariantSetDefaultMutationHookResult = ReturnType; export type ProductVariantSetDefaultMutationOptions = Apollo.BaseMutationOptions; export const ProductUpdateDocument = gql` - mutation ProductUpdate($id: ID!, $input: ProductInput!, $firstValues: Int, $afterValues: String, $lastValues: Int, $beforeValues: String) { + mutation ProductUpdate($id: ID!, $input: ProductInput!) { productUpdate(id: $id, input: $input) { errors { ...ProductErrorWithAttributes } - product { - ...Product - } } } - ${ProductErrorWithAttributesFragmentDoc} -${ProductFragmentDoc}`; + ${ProductErrorWithAttributesFragmentDoc}`; export type ProductUpdateMutationFn = Apollo.MutationFunction; /** @@ -11301,10 +11307,6 @@ export type ProductUpdateMutationFn = Apollo.MutationFunction; export type ProductUpdateMutationResult = Apollo.MutationResult; export type ProductUpdateMutationOptions = Apollo.BaseMutationOptions; -export const SimpleProductUpdateDocument = gql` - mutation SimpleProductUpdate($id: ID!, $input: ProductInput!, $productVariantId: ID!, $productVariantInput: ProductVariantInput!, $addStocks: [StockInput!]!, $deleteStocks: [ID!]!, $updateStocks: [StockInput!]!, $firstValues: Int, $afterValues: String, $lastValues: Int, $beforeValues: String) { - productUpdate(id: $id, input: $input) { - errors { - ...ProductErrorWithAttributes - } - product { - ...Product - } - } - productVariantUpdate(id: $productVariantId, input: $productVariantInput) { - errors { - ...ProductErrorWithAttributes - } - productVariant { - ...ProductVariant - } - } - productVariantStocksCreate(stocks: $addStocks, variantId: $productVariantId) { - errors { - ...BulkStockError - } - productVariant { - ...ProductVariant - } - } - productVariantStocksDelete( - warehouseIds: $deleteStocks - variantId: $productVariantId - ) { - errors { - ...StockError - } - productVariant { - ...ProductVariant - } - } - productVariantStocksUpdate(stocks: $updateStocks, variantId: $productVariantId) { - errors { - ...BulkStockError - } - productVariant { - ...ProductVariant - } - } -} - ${ProductErrorWithAttributesFragmentDoc} -${ProductFragmentDoc} -${ProductVariantFragmentDoc} -${BulkStockErrorFragmentDoc} -${StockErrorFragmentDoc}`; -export type SimpleProductUpdateMutationFn = Apollo.MutationFunction; - -/** - * __useSimpleProductUpdateMutation__ - * - * To run a mutation, you first call `useSimpleProductUpdateMutation` within a React component and pass it any options that fit your needs. - * When your component renders, `useSimpleProductUpdateMutation` 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 [simpleProductUpdateMutation, { data, loading, error }] = useSimpleProductUpdateMutation({ - * variables: { - * id: // value for 'id' - * input: // value for 'input' - * productVariantId: // value for 'productVariantId' - * productVariantInput: // value for 'productVariantInput' - * addStocks: // value for 'addStocks' - * deleteStocks: // value for 'deleteStocks' - * updateStocks: // value for 'updateStocks' - * firstValues: // value for 'firstValues' - * afterValues: // value for 'afterValues' - * lastValues: // value for 'lastValues' - * beforeValues: // value for 'beforeValues' - * }, - * }); - */ -export function useSimpleProductUpdateMutation(baseOptions?: ApolloReactHooks.MutationHookOptions) { - const options = {...defaultOptions, ...baseOptions} - return ApolloReactHooks.useMutation(SimpleProductUpdateDocument, options); - } -export type SimpleProductUpdateMutationHookResult = ReturnType; -export type SimpleProductUpdateMutationResult = Apollo.MutationResult; -export type SimpleProductUpdateMutationOptions = Apollo.BaseMutationOptions; export const ProductCreateDocument = gql` mutation ProductCreate($input: ProductCreateInput!) { productCreate(input: $input) { @@ -11478,6 +11393,121 @@ export function useVariantDeleteMutation(baseOptions?: ApolloReactHooks.Mutation export type VariantDeleteMutationHookResult = ReturnType; export type VariantDeleteMutationResult = Apollo.MutationResult; export type VariantDeleteMutationOptions = Apollo.BaseMutationOptions; +export const VariantDatagridUpdateDocument = gql` + mutation VariantDatagridUpdate($id: ID!, $input: ProductVariantInput!) { + productVariantUpdate(id: $id, input: $input) { + errors { + ...ProductErrorWithAttributes + } + } +} + ${ProductErrorWithAttributesFragmentDoc}`; +export type VariantDatagridUpdateMutationFn = Apollo.MutationFunction; + +/** + * __useVariantDatagridUpdateMutation__ + * + * To run a mutation, you first call `useVariantDatagridUpdateMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useVariantDatagridUpdateMutation` 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 [variantDatagridUpdateMutation, { data, loading, error }] = useVariantDatagridUpdateMutation({ + * variables: { + * id: // value for 'id' + * input: // value for 'input' + * }, + * }); + */ +export function useVariantDatagridUpdateMutation(baseOptions?: ApolloReactHooks.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return ApolloReactHooks.useMutation(VariantDatagridUpdateDocument, options); + } +export type VariantDatagridUpdateMutationHookResult = ReturnType; +export type VariantDatagridUpdateMutationResult = Apollo.MutationResult; +export type VariantDatagridUpdateMutationOptions = Apollo.BaseMutationOptions; +export const VariantDatagridStockUpdateDocument = gql` + mutation VariantDatagridStockUpdate($stocks: [StockInput!]!, $removeStocks: [ID!]!, $id: ID!) { + productVariantStocksDelete(warehouseIds: $removeStocks, variantId: $id) { + errors { + ...ProductVariantStocksDeleteError + } + } + productVariantStocksUpdate(stocks: $stocks, variantId: $id) { + errors { + ...BulkStockError + } + } +} + ${ProductVariantStocksDeleteErrorFragmentDoc} +${BulkStockErrorFragmentDoc}`; +export type VariantDatagridStockUpdateMutationFn = Apollo.MutationFunction; + +/** + * __useVariantDatagridStockUpdateMutation__ + * + * To run a mutation, you first call `useVariantDatagridStockUpdateMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useVariantDatagridStockUpdateMutation` 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 [variantDatagridStockUpdateMutation, { data, loading, error }] = useVariantDatagridStockUpdateMutation({ + * variables: { + * stocks: // value for 'stocks' + * removeStocks: // value for 'removeStocks' + * id: // value for 'id' + * }, + * }); + */ +export function useVariantDatagridStockUpdateMutation(baseOptions?: ApolloReactHooks.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return ApolloReactHooks.useMutation(VariantDatagridStockUpdateDocument, options); + } +export type VariantDatagridStockUpdateMutationHookResult = ReturnType; +export type VariantDatagridStockUpdateMutationResult = Apollo.MutationResult; +export type VariantDatagridStockUpdateMutationOptions = Apollo.BaseMutationOptions; +export const VariantDatagridChannelListingUpdateDocument = gql` + mutation VariantDatagridChannelListingUpdate($id: ID!, $input: [ProductVariantChannelListingAddInput!]!) { + productVariantChannelListingUpdate(id: $id, input: $input) { + errors { + ...ProductChannelListingError + } + } +} + ${ProductChannelListingErrorFragmentDoc}`; +export type VariantDatagridChannelListingUpdateMutationFn = Apollo.MutationFunction; + +/** + * __useVariantDatagridChannelListingUpdateMutation__ + * + * To run a mutation, you first call `useVariantDatagridChannelListingUpdateMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useVariantDatagridChannelListingUpdateMutation` 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 [variantDatagridChannelListingUpdateMutation, { data, loading, error }] = useVariantDatagridChannelListingUpdateMutation({ + * variables: { + * id: // value for 'id' + * input: // value for 'input' + * }, + * }); + */ +export function useVariantDatagridChannelListingUpdateMutation(baseOptions?: ApolloReactHooks.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return ApolloReactHooks.useMutation(VariantDatagridChannelListingUpdateDocument, options); + } +export type VariantDatagridChannelListingUpdateMutationHookResult = ReturnType; +export type VariantDatagridChannelListingUpdateMutationResult = Apollo.MutationResult; +export type VariantDatagridChannelListingUpdateMutationOptions = Apollo.BaseMutationOptions; export const VariantUpdateDocument = gql` mutation VariantUpdate($addStocks: [StockInput!]!, $removeStocks: [ID!]!, $id: ID!, $attributes: [AttributeValueInput!], $sku: String, $quantityLimitPerCustomer: Int, $trackInventory: Boolean!, $stocks: [StockInput!]!, $preorder: PreorderSettingsInput, $weight: WeightScalar, $firstValues: Int, $afterValues: String, $lastValues: Int, $beforeValues: String) { productVariantStocksDelete(warehouseIds: $removeStocks, variantId: $id) { @@ -11842,6 +11872,45 @@ export function useProductBulkDeleteMutation(baseOptions?: ApolloReactHooks.Muta export type ProductBulkDeleteMutationHookResult = ReturnType; export type ProductBulkDeleteMutationResult = Apollo.MutationResult; export type ProductBulkDeleteMutationOptions = Apollo.BaseMutationOptions; +export const ProductVariantBulkCreateDocument = gql` + mutation ProductVariantBulkCreate($id: ID!, $inputs: [ProductVariantBulkCreateInput!]!) { + productVariantBulkCreate(product: $id, variants: $inputs) { + errors { + ...BulkProductError + } + productVariants { + id + } + } +} + ${BulkProductErrorFragmentDoc}`; +export type ProductVariantBulkCreateMutationFn = Apollo.MutationFunction; + +/** + * __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) { + const options = {...defaultOptions, ...baseOptions} + return ApolloReactHooks.useMutation(ProductVariantBulkCreateDocument, options); + } +export type ProductVariantBulkCreateMutationHookResult = ReturnType; +export type ProductVariantBulkCreateMutationResult = Apollo.MutationResult; +export type ProductVariantBulkCreateMutationOptions = Apollo.BaseMutationOptions; export const ProductVariantBulkDeleteDocument = gql` mutation ProductVariantBulkDelete($ids: [ID!]!) { productVariantBulkDelete(ids: $ids) { @@ -11919,26 +11988,12 @@ export type ProductExportMutationOptions = Apollo.BaseMutationOptions; /** diff --git a/src/graphql/typePolicies.generated.ts b/src/graphql/typePolicies.generated.ts index 28a2fd82d..250f6c433 100644 --- a/src/graphql/typePolicies.generated.ts +++ b/src/graphql/typePolicies.generated.ts @@ -813,7 +813,7 @@ export type ChannelUpdatedFieldPolicy = { recipient?: FieldPolicy | FieldReadFunction, channel?: FieldPolicy | FieldReadFunction }; -export type CheckoutKeySpecifier = ('id' | 'privateMetadata' | 'privateMetafield' | 'privateMetafields' | 'metadata' | 'metafield' | 'metafields' | 'created' | 'lastChange' | 'user' | 'channel' | 'billingAddress' | 'shippingAddress' | 'note' | 'discount' | 'discountName' | 'translatedDiscountName' | 'voucherCode' | 'availableShippingMethods' | 'shippingMethods' | 'availableCollectionPoints' | 'availablePaymentGateways' | 'email' | 'giftCards' | 'isShippingRequired' | 'quantity' | 'stockReservationExpires' | 'lines' | 'shippingPrice' | 'shippingMethod' | 'deliveryMethod' | 'subtotalPrice' | 'token' | 'totalPrice' | 'languageCode' | 'transactions' | CheckoutKeySpecifier)[]; +export type CheckoutKeySpecifier = ('id' | 'privateMetadata' | 'privateMetafield' | 'privateMetafields' | 'metadata' | 'metafield' | 'metafields' | 'created' | 'lastChange' | 'user' | 'channel' | 'billingAddress' | 'shippingAddress' | 'note' | 'discount' | 'discountName' | 'translatedDiscountName' | 'voucherCode' | 'availableShippingMethods' | 'shippingMethods' | 'availableCollectionPoints' | 'availablePaymentGateways' | 'email' | 'giftCards' | 'isShippingRequired' | 'quantity' | 'stockReservationExpires' | 'lines' | 'shippingPrice' | 'shippingMethod' | 'deliveryMethod' | 'subtotalPrice' | 'taxExemption' | 'token' | 'totalPrice' | 'languageCode' | 'transactions' | CheckoutKeySpecifier)[]; export type CheckoutFieldPolicy = { id?: FieldPolicy | FieldReadFunction, privateMetadata?: FieldPolicy | FieldReadFunction, @@ -847,6 +847,7 @@ export type CheckoutFieldPolicy = { shippingMethod?: FieldPolicy | FieldReadFunction, deliveryMethod?: FieldPolicy | FieldReadFunction, subtotalPrice?: FieldPolicy | FieldReadFunction, + taxExemption?: FieldPolicy | FieldReadFunction, token?: FieldPolicy | FieldReadFunction, totalPrice?: FieldPolicy | FieldReadFunction, languageCode?: FieldPolicy | FieldReadFunction, @@ -2255,7 +2256,7 @@ export type MoneyRangeFieldPolicy = { start?: FieldPolicy | FieldReadFunction, stop?: FieldPolicy | FieldReadFunction }; -export type MutationKeySpecifier = ('webhookCreate' | 'webhookDelete' | 'webhookUpdate' | 'eventDeliveryRetry' | 'createWarehouse' | 'updateWarehouse' | 'deleteWarehouse' | 'assignWarehouseShippingZone' | 'unassignWarehouseShippingZone' | 'staffNotificationRecipientCreate' | 'staffNotificationRecipientUpdate' | 'staffNotificationRecipientDelete' | 'shopDomainUpdate' | 'shopSettingsUpdate' | 'shopFetchTaxRates' | 'shopSettingsTranslate' | 'shopAddressUpdate' | 'orderSettingsUpdate' | 'giftCardSettingsUpdate' | 'shippingMethodChannelListingUpdate' | 'shippingPriceCreate' | 'shippingPriceDelete' | 'shippingPriceBulkDelete' | 'shippingPriceUpdate' | 'shippingPriceTranslate' | 'shippingPriceExcludeProducts' | 'shippingPriceRemoveProductFromExclude' | 'shippingZoneCreate' | 'shippingZoneDelete' | 'shippingZoneBulkDelete' | 'shippingZoneUpdate' | 'productAttributeAssign' | 'productAttributeAssignmentUpdate' | 'productAttributeUnassign' | 'categoryCreate' | 'categoryDelete' | 'categoryBulkDelete' | 'categoryUpdate' | 'categoryTranslate' | 'collectionAddProducts' | 'collectionCreate' | 'collectionDelete' | 'collectionReorderProducts' | 'collectionBulkDelete' | 'collectionRemoveProducts' | 'collectionUpdate' | 'collectionTranslate' | 'collectionChannelListingUpdate' | 'productCreate' | 'productDelete' | 'productBulkDelete' | 'productUpdate' | 'productTranslate' | 'productChannelListingUpdate' | 'productMediaCreate' | 'productVariantReorder' | 'productMediaDelete' | 'productMediaBulkDelete' | 'productMediaReorder' | 'productMediaUpdate' | 'productTypeCreate' | 'productTypeDelete' | 'productTypeBulkDelete' | 'productTypeUpdate' | 'productTypeReorderAttributes' | 'productReorderAttributeValues' | 'digitalContentCreate' | 'digitalContentDelete' | 'digitalContentUpdate' | 'digitalContentUrlCreate' | 'productVariantCreate' | 'productVariantDelete' | 'productVariantBulkCreate' | 'productVariantBulkDelete' | 'productVariantStocksCreate' | 'productVariantStocksDelete' | 'productVariantStocksUpdate' | 'productVariantUpdate' | 'productVariantSetDefault' | 'productVariantTranslate' | 'productVariantChannelListingUpdate' | 'productVariantReorderAttributeValues' | 'productVariantPreorderDeactivate' | 'variantMediaAssign' | 'variantMediaUnassign' | 'paymentCapture' | 'paymentRefund' | 'paymentVoid' | 'paymentInitialize' | 'paymentCheckBalance' | 'transactionCreate' | 'transactionUpdate' | 'transactionRequestAction' | 'pageCreate' | 'pageDelete' | 'pageBulkDelete' | 'pageBulkPublish' | 'pageUpdate' | 'pageTranslate' | 'pageTypeCreate' | 'pageTypeUpdate' | 'pageTypeDelete' | 'pageTypeBulkDelete' | 'pageAttributeAssign' | 'pageAttributeUnassign' | 'pageTypeReorderAttributes' | 'pageReorderAttributeValues' | 'draftOrderComplete' | 'draftOrderCreate' | 'draftOrderDelete' | 'draftOrderBulkDelete' | 'draftOrderLinesBulkDelete' | 'draftOrderUpdate' | 'orderAddNote' | 'orderCancel' | 'orderCapture' | 'orderConfirm' | 'orderFulfill' | 'orderFulfillmentCancel' | 'orderFulfillmentApprove' | 'orderFulfillmentUpdateTracking' | 'orderFulfillmentRefundProducts' | 'orderFulfillmentReturnProducts' | 'orderLinesCreate' | 'orderLineDelete' | 'orderLineUpdate' | 'orderDiscountAdd' | 'orderDiscountUpdate' | 'orderDiscountDelete' | 'orderLineDiscountUpdate' | 'orderLineDiscountRemove' | 'orderMarkAsPaid' | 'orderRefund' | 'orderUpdate' | 'orderUpdateShipping' | 'orderVoid' | 'orderBulkCancel' | 'deleteMetadata' | 'deletePrivateMetadata' | 'updateMetadata' | 'updatePrivateMetadata' | 'assignNavigation' | 'menuCreate' | 'menuDelete' | 'menuBulkDelete' | 'menuUpdate' | 'menuItemCreate' | 'menuItemDelete' | 'menuItemBulkDelete' | 'menuItemUpdate' | 'menuItemTranslate' | 'menuItemMove' | 'invoiceRequest' | 'invoiceRequestDelete' | 'invoiceCreate' | 'invoiceDelete' | 'invoiceUpdate' | 'invoiceSendNotification' | 'giftCardActivate' | 'giftCardCreate' | 'giftCardDelete' | 'giftCardDeactivate' | 'giftCardUpdate' | 'giftCardResend' | 'giftCardAddNote' | 'giftCardBulkCreate' | 'giftCardBulkDelete' | 'giftCardBulkActivate' | 'giftCardBulkDeactivate' | 'pluginUpdate' | 'externalNotificationTrigger' | 'saleCreate' | 'saleDelete' | 'saleBulkDelete' | 'saleUpdate' | 'saleCataloguesAdd' | 'saleCataloguesRemove' | 'saleTranslate' | 'saleChannelListingUpdate' | 'voucherCreate' | 'voucherDelete' | 'voucherBulkDelete' | 'voucherUpdate' | 'voucherCataloguesAdd' | 'voucherCataloguesRemove' | 'voucherTranslate' | 'voucherChannelListingUpdate' | 'exportProducts' | 'exportGiftCards' | 'fileUpload' | 'checkoutAddPromoCode' | 'checkoutBillingAddressUpdate' | 'checkoutComplete' | 'checkoutCreate' | 'checkoutCustomerAttach' | 'checkoutCustomerDetach' | 'checkoutEmailUpdate' | 'checkoutLineDelete' | 'checkoutLinesDelete' | 'checkoutLinesAdd' | 'checkoutLinesUpdate' | 'checkoutRemovePromoCode' | 'checkoutPaymentCreate' | 'checkoutShippingAddressUpdate' | 'checkoutShippingMethodUpdate' | 'checkoutDeliveryMethodUpdate' | 'checkoutLanguageCodeUpdate' | 'orderCreateFromCheckout' | 'channelCreate' | 'channelUpdate' | 'channelDelete' | 'channelActivate' | 'channelDeactivate' | 'channelReorderWarehouses' | 'attributeCreate' | 'attributeDelete' | 'attributeUpdate' | 'attributeTranslate' | 'attributeBulkDelete' | 'attributeValueBulkDelete' | 'attributeValueCreate' | 'attributeValueDelete' | 'attributeValueUpdate' | 'attributeValueTranslate' | 'attributeReorderValues' | 'appCreate' | 'appUpdate' | 'appDelete' | 'appTokenCreate' | 'appTokenDelete' | 'appTokenVerify' | 'appInstall' | 'appRetryInstall' | 'appDeleteFailedInstallation' | 'appFetchManifest' | 'appActivate' | 'appDeactivate' | 'tokenCreate' | 'tokenRefresh' | 'tokenVerify' | 'tokensDeactivateAll' | 'externalAuthenticationUrl' | 'externalObtainAccessTokens' | 'externalRefresh' | 'externalLogout' | 'externalVerify' | 'requestPasswordReset' | 'confirmAccount' | 'setPassword' | 'passwordChange' | 'requestEmailChange' | 'confirmEmailChange' | 'accountAddressCreate' | 'accountAddressUpdate' | 'accountAddressDelete' | 'accountSetDefaultAddress' | 'accountRegister' | 'accountUpdate' | 'accountRequestDeletion' | 'accountDelete' | 'addressCreate' | 'addressUpdate' | 'addressDelete' | 'addressSetDefault' | 'customerCreate' | 'customerUpdate' | 'customerDelete' | 'customerBulkDelete' | 'staffCreate' | 'staffUpdate' | 'staffDelete' | 'staffBulkDelete' | 'userAvatarUpdate' | 'userAvatarDelete' | 'userBulkSetActive' | 'permissionGroupCreate' | 'permissionGroupUpdate' | 'permissionGroupDelete' | MutationKeySpecifier)[]; +export type MutationKeySpecifier = ('webhookCreate' | 'webhookDelete' | 'webhookUpdate' | 'eventDeliveryRetry' | 'createWarehouse' | 'updateWarehouse' | 'deleteWarehouse' | 'assignWarehouseShippingZone' | 'unassignWarehouseShippingZone' | 'taxExemptionManage' | 'staffNotificationRecipientCreate' | 'staffNotificationRecipientUpdate' | 'staffNotificationRecipientDelete' | 'shopDomainUpdate' | 'shopSettingsUpdate' | 'shopFetchTaxRates' | 'shopSettingsTranslate' | 'shopAddressUpdate' | 'orderSettingsUpdate' | 'giftCardSettingsUpdate' | 'shippingMethodChannelListingUpdate' | 'shippingPriceCreate' | 'shippingPriceDelete' | 'shippingPriceBulkDelete' | 'shippingPriceUpdate' | 'shippingPriceTranslate' | 'shippingPriceExcludeProducts' | 'shippingPriceRemoveProductFromExclude' | 'shippingZoneCreate' | 'shippingZoneDelete' | 'shippingZoneBulkDelete' | 'shippingZoneUpdate' | 'productAttributeAssign' | 'productAttributeAssignmentUpdate' | 'productAttributeUnassign' | 'categoryCreate' | 'categoryDelete' | 'categoryBulkDelete' | 'categoryUpdate' | 'categoryTranslate' | 'collectionAddProducts' | 'collectionCreate' | 'collectionDelete' | 'collectionReorderProducts' | 'collectionBulkDelete' | 'collectionRemoveProducts' | 'collectionUpdate' | 'collectionTranslate' | 'collectionChannelListingUpdate' | 'productCreate' | 'productDelete' | 'productBulkDelete' | 'productUpdate' | 'productTranslate' | 'productChannelListingUpdate' | 'productMediaCreate' | 'productVariantReorder' | 'productMediaDelete' | 'productMediaBulkDelete' | 'productMediaReorder' | 'productMediaUpdate' | 'productTypeCreate' | 'productTypeDelete' | 'productTypeBulkDelete' | 'productTypeUpdate' | 'productTypeReorderAttributes' | 'productReorderAttributeValues' | 'digitalContentCreate' | 'digitalContentDelete' | 'digitalContentUpdate' | 'digitalContentUrlCreate' | 'productVariantCreate' | 'productVariantDelete' | 'productVariantBulkCreate' | 'productVariantBulkDelete' | 'productVariantStocksCreate' | 'productVariantStocksDelete' | 'productVariantStocksUpdate' | 'productVariantUpdate' | 'productVariantSetDefault' | 'productVariantTranslate' | 'productVariantChannelListingUpdate' | 'productVariantReorderAttributeValues' | 'productVariantPreorderDeactivate' | 'variantMediaAssign' | 'variantMediaUnassign' | 'paymentCapture' | 'paymentRefund' | 'paymentVoid' | 'paymentInitialize' | 'paymentCheckBalance' | 'transactionCreate' | 'transactionUpdate' | 'transactionRequestAction' | 'pageCreate' | 'pageDelete' | 'pageBulkDelete' | 'pageBulkPublish' | 'pageUpdate' | 'pageTranslate' | 'pageTypeCreate' | 'pageTypeUpdate' | 'pageTypeDelete' | 'pageTypeBulkDelete' | 'pageAttributeAssign' | 'pageAttributeUnassign' | 'pageTypeReorderAttributes' | 'pageReorderAttributeValues' | 'draftOrderComplete' | 'draftOrderCreate' | 'draftOrderDelete' | 'draftOrderBulkDelete' | 'draftOrderLinesBulkDelete' | 'draftOrderUpdate' | 'orderAddNote' | 'orderCancel' | 'orderCapture' | 'orderConfirm' | 'orderFulfill' | 'orderFulfillmentCancel' | 'orderFulfillmentApprove' | 'orderFulfillmentUpdateTracking' | 'orderFulfillmentRefundProducts' | 'orderFulfillmentReturnProducts' | 'orderLinesCreate' | 'orderLineDelete' | 'orderLineUpdate' | 'orderDiscountAdd' | 'orderDiscountUpdate' | 'orderDiscountDelete' | 'orderLineDiscountUpdate' | 'orderLineDiscountRemove' | 'orderMarkAsPaid' | 'orderRefund' | 'orderUpdate' | 'orderUpdateShipping' | 'orderVoid' | 'orderBulkCancel' | 'deleteMetadata' | 'deletePrivateMetadata' | 'updateMetadata' | 'updatePrivateMetadata' | 'assignNavigation' | 'menuCreate' | 'menuDelete' | 'menuBulkDelete' | 'menuUpdate' | 'menuItemCreate' | 'menuItemDelete' | 'menuItemBulkDelete' | 'menuItemUpdate' | 'menuItemTranslate' | 'menuItemMove' | 'invoiceRequest' | 'invoiceRequestDelete' | 'invoiceCreate' | 'invoiceDelete' | 'invoiceUpdate' | 'invoiceSendNotification' | 'giftCardActivate' | 'giftCardCreate' | 'giftCardDelete' | 'giftCardDeactivate' | 'giftCardUpdate' | 'giftCardResend' | 'giftCardAddNote' | 'giftCardBulkCreate' | 'giftCardBulkDelete' | 'giftCardBulkActivate' | 'giftCardBulkDeactivate' | 'pluginUpdate' | 'externalNotificationTrigger' | 'saleCreate' | 'saleDelete' | 'saleBulkDelete' | 'saleUpdate' | 'saleCataloguesAdd' | 'saleCataloguesRemove' | 'saleTranslate' | 'saleChannelListingUpdate' | 'voucherCreate' | 'voucherDelete' | 'voucherBulkDelete' | 'voucherUpdate' | 'voucherCataloguesAdd' | 'voucherCataloguesRemove' | 'voucherTranslate' | 'voucherChannelListingUpdate' | 'exportProducts' | 'exportGiftCards' | 'fileUpload' | 'checkoutAddPromoCode' | 'checkoutBillingAddressUpdate' | 'checkoutComplete' | 'checkoutCreate' | 'checkoutCustomerAttach' | 'checkoutCustomerDetach' | 'checkoutEmailUpdate' | 'checkoutLineDelete' | 'checkoutLinesDelete' | 'checkoutLinesAdd' | 'checkoutLinesUpdate' | 'checkoutRemovePromoCode' | 'checkoutPaymentCreate' | 'checkoutShippingAddressUpdate' | 'checkoutShippingMethodUpdate' | 'checkoutDeliveryMethodUpdate' | 'checkoutLanguageCodeUpdate' | 'orderCreateFromCheckout' | 'channelCreate' | 'channelUpdate' | 'channelDelete' | 'channelActivate' | 'channelDeactivate' | 'channelReorderWarehouses' | 'attributeCreate' | 'attributeDelete' | 'attributeUpdate' | 'attributeTranslate' | 'attributeBulkDelete' | 'attributeValueBulkDelete' | 'attributeValueCreate' | 'attributeValueDelete' | 'attributeValueUpdate' | 'attributeValueTranslate' | 'attributeReorderValues' | 'appCreate' | 'appUpdate' | 'appDelete' | 'appTokenCreate' | 'appTokenDelete' | 'appTokenVerify' | 'appInstall' | 'appRetryInstall' | 'appDeleteFailedInstallation' | 'appFetchManifest' | 'appActivate' | 'appDeactivate' | 'tokenCreate' | 'tokenRefresh' | 'tokenVerify' | 'tokensDeactivateAll' | 'externalAuthenticationUrl' | 'externalObtainAccessTokens' | 'externalRefresh' | 'externalLogout' | 'externalVerify' | 'requestPasswordReset' | 'confirmAccount' | 'setPassword' | 'passwordChange' | 'requestEmailChange' | 'confirmEmailChange' | 'accountAddressCreate' | 'accountAddressUpdate' | 'accountAddressDelete' | 'accountSetDefaultAddress' | 'accountRegister' | 'accountUpdate' | 'accountRequestDeletion' | 'accountDelete' | 'addressCreate' | 'addressUpdate' | 'addressDelete' | 'addressSetDefault' | 'customerCreate' | 'customerUpdate' | 'customerDelete' | 'customerBulkDelete' | 'staffCreate' | 'staffUpdate' | 'staffDelete' | 'staffBulkDelete' | 'userAvatarUpdate' | 'userAvatarDelete' | 'userBulkSetActive' | 'permissionGroupCreate' | 'permissionGroupUpdate' | 'permissionGroupDelete' | MutationKeySpecifier)[]; export type MutationFieldPolicy = { webhookCreate?: FieldPolicy | FieldReadFunction, webhookDelete?: FieldPolicy | FieldReadFunction, @@ -2266,6 +2267,7 @@ export type MutationFieldPolicy = { deleteWarehouse?: FieldPolicy | FieldReadFunction, assignWarehouseShippingZone?: FieldPolicy | FieldReadFunction, unassignWarehouseShippingZone?: FieldPolicy | FieldReadFunction, + taxExemptionManage?: FieldPolicy | FieldReadFunction, staffNotificationRecipientCreate?: FieldPolicy | FieldReadFunction, staffNotificationRecipientUpdate?: FieldPolicy | FieldReadFunction, staffNotificationRecipientDelete?: FieldPolicy | FieldReadFunction, @@ -2549,7 +2551,7 @@ export type ObjectWithMetadataFieldPolicy = { metafield?: FieldPolicy | FieldReadFunction, metafields?: FieldPolicy | FieldReadFunction }; -export type OrderKeySpecifier = ('id' | 'privateMetadata' | 'privateMetafield' | 'privateMetafields' | 'metadata' | 'metafield' | 'metafields' | 'created' | 'updatedAt' | 'status' | 'user' | 'trackingClientId' | 'billingAddress' | 'shippingAddress' | 'shippingMethodName' | 'collectionPointName' | 'channel' | 'fulfillments' | 'lines' | 'actions' | 'availableShippingMethods' | 'shippingMethods' | 'availableCollectionPoints' | 'invoices' | 'number' | 'original' | 'origin' | 'isPaid' | 'paymentStatus' | 'paymentStatusDisplay' | 'authorizeStatus' | 'chargeStatus' | 'transactions' | 'payments' | 'total' | 'undiscountedTotal' | 'shippingMethod' | 'shippingPrice' | 'shippingTaxRate' | 'token' | 'voucher' | 'giftCards' | 'displayGrossPrices' | 'customerNote' | 'weight' | 'redirectUrl' | 'subtotal' | 'statusDisplay' | 'canFinalize' | 'totalAuthorized' | 'totalCaptured' | 'events' | 'totalBalance' | 'userEmail' | 'isShippingRequired' | 'deliveryMethod' | 'languageCode' | 'languageCodeEnum' | 'discount' | 'discountName' | 'translatedDiscountName' | 'discounts' | 'errors' | OrderKeySpecifier)[]; +export type OrderKeySpecifier = ('id' | 'privateMetadata' | 'privateMetafield' | 'privateMetafields' | 'metadata' | 'metafield' | 'metafields' | 'created' | 'updatedAt' | 'status' | 'user' | 'trackingClientId' | 'billingAddress' | 'shippingAddress' | 'shippingMethodName' | 'collectionPointName' | 'channel' | 'fulfillments' | 'lines' | 'actions' | 'availableShippingMethods' | 'shippingMethods' | 'availableCollectionPoints' | 'invoices' | 'number' | 'original' | 'origin' | 'isPaid' | 'paymentStatus' | 'paymentStatusDisplay' | 'authorizeStatus' | 'chargeStatus' | 'taxExemption' | 'transactions' | 'payments' | 'total' | 'undiscountedTotal' | 'shippingMethod' | 'shippingPrice' | 'shippingTaxRate' | 'token' | 'voucher' | 'giftCards' | 'displayGrossPrices' | 'customerNote' | 'weight' | 'redirectUrl' | 'subtotal' | 'statusDisplay' | 'canFinalize' | 'totalAuthorized' | 'totalCaptured' | 'events' | 'totalBalance' | 'userEmail' | 'isShippingRequired' | 'deliveryMethod' | 'languageCode' | 'languageCodeEnum' | 'discount' | 'discountName' | 'translatedDiscountName' | 'discounts' | 'errors' | OrderKeySpecifier)[]; export type OrderFieldPolicy = { id?: FieldPolicy | FieldReadFunction, privateMetadata?: FieldPolicy | FieldReadFunction, @@ -2583,6 +2585,7 @@ export type OrderFieldPolicy = { paymentStatusDisplay?: FieldPolicy | FieldReadFunction, authorizeStatus?: FieldPolicy | FieldReadFunction, chargeStatus?: FieldPolicy | FieldReadFunction, + taxExemption?: FieldPolicy | FieldReadFunction, transactions?: FieldPolicy | FieldReadFunction, payments?: FieldPolicy | FieldReadFunction, total?: FieldPolicy | FieldReadFunction, @@ -4646,6 +4649,17 @@ export type SubscriptionKeySpecifier = ('event' | SubscriptionKeySpecifier)[]; export type SubscriptionFieldPolicy = { event?: FieldPolicy | FieldReadFunction }; +export type TaxExemptionManageKeySpecifier = ('taxableObject' | 'errors' | TaxExemptionManageKeySpecifier)[]; +export type TaxExemptionManageFieldPolicy = { + taxableObject?: FieldPolicy | FieldReadFunction, + errors?: FieldPolicy | FieldReadFunction +}; +export type TaxExemptionManageErrorKeySpecifier = ('field' | 'message' | 'code' | TaxExemptionManageErrorKeySpecifier)[]; +export type TaxExemptionManageErrorFieldPolicy = { + field?: FieldPolicy | FieldReadFunction, + message?: FieldPolicy | FieldReadFunction, + code?: FieldPolicy | FieldReadFunction +}; export type TaxTypeKeySpecifier = ('description' | 'taxCode' | TaxTypeKeySpecifier)[]; export type TaxTypeFieldPolicy = { description?: FieldPolicy | FieldReadFunction, @@ -7446,6 +7460,14 @@ export type StrictTypedTypePolicies = { keyFields?: false | SubscriptionKeySpecifier | (() => undefined | SubscriptionKeySpecifier), fields?: SubscriptionFieldPolicy, }, + TaxExemptionManage?: Omit & { + keyFields?: false | TaxExemptionManageKeySpecifier | (() => undefined | TaxExemptionManageKeySpecifier), + fields?: TaxExemptionManageFieldPolicy, + }, + TaxExemptionManageError?: Omit & { + keyFields?: false | TaxExemptionManageErrorKeySpecifier | (() => undefined | TaxExemptionManageErrorKeySpecifier), + fields?: TaxExemptionManageErrorFieldPolicy, + }, TaxType?: Omit & { keyFields?: false | TaxTypeKeySpecifier | (() => undefined | TaxTypeKeySpecifier), fields?: TaxTypeFieldPolicy, diff --git a/src/graphql/types.generated.ts b/src/graphql/types.generated.ts index 24854ee05..0d450ade9 100644 --- a/src/graphql/types.generated.ts +++ b/src/graphql/types.generated.ts @@ -361,6 +361,7 @@ export type AttributeFilterInput = { type?: InputMaybe; inCollection?: InputMaybe; inCategory?: InputMaybe; + slugs?: InputMaybe>; /** * Specifies the channel by which the data should be filtered. * @@ -486,7 +487,7 @@ export type AttributeValueFilterInput = { export type AttributeValueInput = { /** ID of the selected attribute. */ - id: Scalars['ID']; + id?: InputMaybe; /** The value or slug of an attribute to resolve. If the passed value is non-existent, it will be created. */ values?: InputMaybe>; /** URL of the file attribute. Every time, a new value is created. */ @@ -575,6 +576,7 @@ export type CategoryFilterInput = { search?: InputMaybe; metadata?: InputMaybe>; ids?: InputMaybe>; + slugs?: InputMaybe>; }; export type CategoryInput = { @@ -808,6 +810,12 @@ export type CheckoutLineInput = { * Note: this API is currently in Feature Preview and can be subject to changes at later point. */ forceNewLine?: InputMaybe; + /** + * Fields required to update the object's metadata. + * + * Added in Saleor 3.8. + */ + metadata?: InputMaybe>; }; export type CheckoutLineUpdateInput = { @@ -910,6 +918,7 @@ export type CollectionFilterInput = { search?: InputMaybe; metadata?: InputMaybe>; ids?: InputMaybe>; + slugs?: InputMaybe>; /** * Specifies the channel by which the data should be filtered. * @@ -2703,6 +2712,7 @@ export type MenuFilterInput = { search?: InputMaybe; slug?: InputMaybe>; metadata?: InputMaybe>; + slugs?: InputMaybe>; }; export type MenuInput = { @@ -3298,6 +3308,7 @@ export type PageFilterInput = { metadata?: InputMaybe>; pageTypes?: InputMaybe>; ids?: InputMaybe>; + slugs?: InputMaybe>; }; export type PageInput = { @@ -3384,6 +3395,7 @@ export type PageTypeCreateInput = { export type PageTypeFilterInput = { search?: InputMaybe; + slugs?: InputMaybe>; }; export enum PageTypeSortField { @@ -3489,6 +3501,10 @@ export enum PermissionEnum { IMPERSONATE_USER = 'IMPERSONATE_USER', MANAGE_APPS = 'MANAGE_APPS', MANAGE_OBSERVABILITY = 'MANAGE_OBSERVABILITY', + MANAGE_CHECKOUTS = 'MANAGE_CHECKOUTS', + HANDLE_CHECKOUTS = 'HANDLE_CHECKOUTS', + HANDLE_TAXES = 'HANDLE_TAXES', + MANAGE_TAXES = 'MANAGE_TAXES', MANAGE_CHANNELS = 'MANAGE_CHANNELS', MANAGE_DISCOUNTS = 'MANAGE_DISCOUNTS', MANAGE_PLUGINS = 'MANAGE_PLUGINS', @@ -3502,10 +3518,7 @@ export enum PermissionEnum { MANAGE_PRODUCT_TYPES_AND_ATTRIBUTES = 'MANAGE_PRODUCT_TYPES_AND_ATTRIBUTES', MANAGE_SHIPPING = 'MANAGE_SHIPPING', MANAGE_SETTINGS = 'MANAGE_SETTINGS', - MANAGE_TRANSLATIONS = 'MANAGE_TRANSLATIONS', - MANAGE_CHECKOUTS = 'MANAGE_CHECKOUTS', - HANDLE_CHECKOUTS = 'HANDLE_CHECKOUTS', - HANDLE_TAXES = 'HANDLE_TAXES' + MANAGE_TRANSLATIONS = 'MANAGE_TRANSLATIONS' } export type PermissionGroupCreateInput = { @@ -3796,6 +3809,7 @@ export type ProductFilterInput = { giftCard?: InputMaybe; ids?: InputMaybe>; hasPreorderedVariants?: InputMaybe; + slugs?: InputMaybe>; /** * Specifies the channel by which the data should be filtered. * @@ -3948,6 +3962,7 @@ export type ProductTypeFilterInput = { metadata?: InputMaybe>; kind?: InputMaybe; ids?: InputMaybe>; + slugs?: InputMaybe>; }; export type ProductTypeInput = { @@ -4551,6 +4566,14 @@ export enum StorePaymentMethodEnum { NONE = 'NONE' } +/** An enumeration. */ +export enum TaxExemptionManageErrorCode { + GRAPHQL_ERROR = 'GRAPHQL_ERROR', + INVALID = 'INVALID', + NOT_FOUND = 'NOT_FOUND', + NOT_EDITABLE_ORDER = 'NOT_EDITABLE_ORDER' +} + /** An enumeration. */ export enum ThumbnailFormatEnum { WEBP = 'WEBP' @@ -4957,6 +4980,7 @@ export type WarehouseFilterInput = { ids?: InputMaybe>; isPrivate?: InputMaybe; channels?: InputMaybe>; + slugs?: InputMaybe>; }; export enum WarehouseSortField { @@ -6747,15 +6771,15 @@ export type ProductWithChannelListingsFragment = { __typename: 'Product', id: st export type ProductVariantAttributesFragment = { __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 }; -export type ProductDetailsVariantFragment = { __typename: 'ProductVariant', id: string, sku: string | null, name: string, margin: number | null, trackInventory: boolean, quantityLimitPerCustomer: number | null, media: Array<{ __typename: 'ProductMedia', url: string }> | null, stocks: Array<{ __typename: 'Stock', id: string, quantity: number, quantityAllocated: number, warehouse: { __typename: 'Warehouse', id: string, name: string } }> | null, preorder: { __typename: 'PreorderData', globalThreshold: number | null, globalSoldUnits: number, endDate: any | null } | null, channelListings: Array<{ __typename: 'ProductVariantChannelListing', channel: { __typename: 'Channel', id: string, name: string, currencyCode: string }, price: { __typename: 'Money', amount: number, currency: string } | null, costPrice: { __typename: 'Money', amount: number, currency: string } | null, preorderThreshold: { __typename: 'PreorderThreshold', quantity: number | null, soldUnits: number } | null }> | null }; +export type ProductDetailsVariantFragment = { __typename: 'ProductVariant', id: string, sku: string | null, name: string, trackInventory: boolean, quantityLimitPerCustomer: number | null, attributes: Array<{ __typename: 'SelectedAttribute', attribute: { __typename: 'Attribute', id: string, name: string | null }, values: Array<{ __typename: 'AttributeValue', id: string, name: string | null }> }>, media: Array<{ __typename: 'ProductMedia', url: string }> | null, stocks: Array<{ __typename: 'Stock', id: string, quantity: number, quantityAllocated: number, warehouse: { __typename: 'Warehouse', id: string, name: string } }> | null, preorder: { __typename: 'PreorderData', globalThreshold: number | null, globalSoldUnits: number, endDate: any | null } | null, channelListings: Array<{ __typename: 'ProductVariantChannelListing', channel: { __typename: 'Channel', id: string, name: string, currencyCode: string }, price: { __typename: 'Money', amount: number, currency: string } | null, costPrice: { __typename: 'Money', amount: number, currency: string } | null, preorderThreshold: { __typename: 'PreorderThreshold', quantity: number | null, soldUnits: number } | null }> | null }; -export type ProductFragment = { __typename: 'Product', name: string, slug: string, description: any | null, seoTitle: string | null, seoDescription: string | null, rating: number | null, chargeTaxes: boolean, isAvailable: boolean | null, id: string, defaultVariant: { __typename: 'ProductVariant', id: string } | null, category: { __typename: 'Category', id: string, name: string } | null, collections: Array<{ __typename: 'Collection', id: string, name: string }> | null, channelListings: Array<{ __typename: 'ProductChannelListing', isPublished: boolean, publicationDate: any | null, isAvailableForPurchase: boolean | null, availableForPurchase: any | null, visibleInListings: boolean, channel: { __typename: 'Channel', id: string, name: string, currencyCode: string } }> | null, media: Array<{ __typename: 'ProductMedia', id: string, alt: string, sortOrder: number | null, url: string, type: ProductMediaType, oembedData: any }> | null, variants: Array<{ __typename: 'ProductVariant', id: string, sku: string | null, name: string, margin: number | null, trackInventory: boolean, quantityLimitPerCustomer: number | null, media: Array<{ __typename: 'ProductMedia', url: string }> | null, stocks: Array<{ __typename: 'Stock', id: string, quantity: number, quantityAllocated: number, warehouse: { __typename: 'Warehouse', id: string, name: string } }> | null, preorder: { __typename: 'PreorderData', globalThreshold: number | null, globalSoldUnits: number, endDate: any | null } | null, channelListings: Array<{ __typename: 'ProductVariantChannelListing', channel: { __typename: 'Channel', id: string, name: string, currencyCode: string }, price: { __typename: 'Money', amount: number, currency: string } | null, costPrice: { __typename: 'Money', amount: number, currency: string } | null, preorderThreshold: { __typename: 'PreorderThreshold', quantity: number | null, soldUnits: number } | null }> | null }> | null, productType: { __typename: 'ProductType', id: string, name: string, hasVariants: boolean, taxType: { __typename: 'TaxType', description: string | null, taxCode: string | null } | null, 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 }, weight: { __typename: 'Weight', unit: WeightUnitsEnum, value: number } | null, taxType: { __typename: 'TaxType', description: string | null, taxCode: string | null } | null, 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 }> }>, metadata: Array<{ __typename: 'MetadataItem', key: string, value: string }>, privateMetadata: Array<{ __typename: 'MetadataItem', key: string, value: string }> }; +export type ProductFragment = { __typename: 'Product', name: string, slug: string, description: any | null, seoTitle: string | null, seoDescription: string | null, rating: number | null, chargeTaxes: boolean, isAvailable: boolean | null, id: string, defaultVariant: { __typename: 'ProductVariant', id: string } | null, category: { __typename: 'Category', id: string, name: string } | null, collections: Array<{ __typename: 'Collection', id: string, name: string }> | null, channelListings: Array<{ __typename: 'ProductChannelListing', isPublished: boolean, publicationDate: any | null, isAvailableForPurchase: boolean | null, availableForPurchase: any | null, visibleInListings: boolean, channel: { __typename: 'Channel', id: string, name: string, currencyCode: string } }> | null, media: Array<{ __typename: 'ProductMedia', id: string, alt: string, sortOrder: number | null, url: string, type: ProductMediaType, oembedData: any }> | null, variants: Array<{ __typename: 'ProductVariant', id: string, sku: string | null, name: string, trackInventory: boolean, quantityLimitPerCustomer: number | null, attributes: Array<{ __typename: 'SelectedAttribute', attribute: { __typename: 'Attribute', id: string, name: string | null }, values: Array<{ __typename: 'AttributeValue', id: string, name: string | null }> }>, media: Array<{ __typename: 'ProductMedia', url: string }> | null, stocks: Array<{ __typename: 'Stock', id: string, quantity: number, quantityAllocated: number, warehouse: { __typename: 'Warehouse', id: string, name: string } }> | null, preorder: { __typename: 'PreorderData', globalThreshold: number | null, globalSoldUnits: number, endDate: any | null } | null, channelListings: Array<{ __typename: 'ProductVariantChannelListing', channel: { __typename: 'Channel', id: string, name: string, currencyCode: string }, price: { __typename: 'Money', amount: number, currency: string } | null, costPrice: { __typename: 'Money', amount: number, currency: string } | null, preorderThreshold: { __typename: 'PreorderThreshold', quantity: number | null, soldUnits: number } | null }> | null }> | null, productType: { __typename: 'ProductType', id: string, name: string, hasVariants: boolean, taxType: { __typename: 'TaxType', description: string | null, taxCode: string | null } | null, 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 }, weight: { __typename: 'Weight', unit: WeightUnitsEnum, value: number } | null, taxType: { __typename: 'TaxType', description: string | null, taxCode: string | null } | null, 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 }> }>, metadata: Array<{ __typename: 'MetadataItem', key: string, value: string }>, privateMetadata: Array<{ __typename: 'MetadataItem', key: string, value: string }> }; export type VariantAttributeFragment = { __typename: 'Attribute', id: string, name: string | null, slug: 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 }; export type SelectedVariantAttributeFragment = { __typename: 'SelectedAttribute', attribute: { __typename: 'Attribute', id: string, name: string | null, slug: 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 }> }; -export type ProductVariantFragment = { __typename: 'ProductVariant', id: string, name: string, sku: string | null, trackInventory: boolean, quantityLimitPerCustomer: number | null, selectionAttributes: Array<{ __typename: 'SelectedAttribute', attribute: { __typename: 'Attribute', id: string, name: string | null, slug: 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 }> }>, nonSelectionAttributes: Array<{ __typename: 'SelectedAttribute', attribute: { __typename: 'Attribute', id: string, name: string | null, slug: 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 }> }>, media: Array<{ __typename: 'ProductMedia', id: string, url: string, type: ProductMediaType, oembedData: any }> | null, product: { __typename: 'Product', id: string, name: string, defaultVariant: { __typename: 'ProductVariant', id: string } | null, media: Array<{ __typename: 'ProductMedia', id: string, alt: string, sortOrder: number | null, url: string, type: ProductMediaType, oembedData: any }> | null, thumbnail: { __typename: 'Image', url: string } | null, channelListings: Array<{ __typename: 'ProductChannelListing', publicationDate: any | null, isPublished: boolean, channel: { __typename: 'Channel', id: string, name: string, currencyCode: string } }> | null, variants: Array<{ __typename: 'ProductVariant', id: string, name: string, sku: string | null, media: Array<{ __typename: 'ProductMedia', id: string, url: string, type: ProductMediaType, oembedData: any }> | null }> | null }, channelListings: Array<{ __typename: 'ProductVariantChannelListing', channel: { __typename: 'Channel', id: string, name: string, currencyCode: string }, price: { __typename: 'Money', amount: number, currency: string } | null, costPrice: { __typename: 'Money', amount: number, currency: string } | null, preorderThreshold: { __typename: 'PreorderThreshold', quantity: number | null, soldUnits: number } | null }> | null, stocks: Array<{ __typename: 'Stock', id: string, quantity: number, quantityAllocated: number, warehouse: { __typename: 'Warehouse', id: string, name: string } }> | null, preorder: { __typename: 'PreorderData', globalThreshold: number | null, globalSoldUnits: number, endDate: any | null } | null, weight: { __typename: 'Weight', unit: WeightUnitsEnum, value: number } | null, metadata: Array<{ __typename: 'MetadataItem', key: string, value: string }>, privateMetadata: Array<{ __typename: 'MetadataItem', key: string, value: string }> }; +export type ProductVariantFragment = { __typename: 'ProductVariant', id: string, name: string, sku: string | null, trackInventory: boolean, quantityLimitPerCustomer: number | null, selectionAttributes: Array<{ __typename: 'SelectedAttribute', attribute: { __typename: 'Attribute', id: string, name: string | null, slug: 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 }> }>, nonSelectionAttributes: Array<{ __typename: 'SelectedAttribute', attribute: { __typename: 'Attribute', id: string, name: string | null, slug: 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 }> }>, media: Array<{ __typename: 'ProductMedia', id: string, url: string, type: ProductMediaType, oembedData: any }> | null, product: { __typename: 'Product', id: string, name: string, defaultVariant: { __typename: 'ProductVariant', id: string } | null, media: Array<{ __typename: 'ProductMedia', id: string, alt: string, sortOrder: number | null, url: string, type: ProductMediaType, oembedData: any }> | null, thumbnail: { __typename: 'Image', url: string } | null, channelListings: Array<{ __typename: 'ProductChannelListing', id: string, publicationDate: any | null, isPublished: boolean, channel: { __typename: 'Channel', id: string, name: string, currencyCode: string } }> | null, variants: Array<{ __typename: 'ProductVariant', id: string, name: string, sku: string | null, media: Array<{ __typename: 'ProductMedia', id: string, url: string, type: ProductMediaType, oembedData: any }> | null }> | null }, channelListings: Array<{ __typename: 'ProductVariantChannelListing', channel: { __typename: 'Channel', id: string, name: string, currencyCode: string }, price: { __typename: 'Money', amount: number, currency: string } | null, costPrice: { __typename: 'Money', amount: number, currency: string } | null, preorderThreshold: { __typename: 'PreorderThreshold', quantity: number | null, soldUnits: number } | null }> | null, stocks: Array<{ __typename: 'Stock', id: string, quantity: number, quantityAllocated: number, warehouse: { __typename: 'Warehouse', id: string, name: string } }> | null, preorder: { __typename: 'PreorderData', globalThreshold: number | null, globalSoldUnits: number, endDate: any | null } | null, weight: { __typename: 'Weight', unit: WeightUnitsEnum, value: number } | null, metadata: Array<{ __typename: 'MetadataItem', key: string, value: string }>, privateMetadata: Array<{ __typename: 'MetadataItem', key: string, value: string }> }; export type ExportFileFragment = { __typename: 'ExportFile', id: string, status: JobStatusEnum, url: string | null }; @@ -7701,31 +7725,10 @@ export type ProductVariantSetDefaultMutation = { __typename: 'Mutation', product export type ProductUpdateMutationVariables = Exact<{ id: Scalars['ID']; input: ProductInput; - firstValues?: InputMaybe; - afterValues?: InputMaybe; - lastValues?: InputMaybe; - beforeValues?: InputMaybe; }>; -export type ProductUpdateMutation = { __typename: 'Mutation', productUpdate: { __typename: 'ProductUpdate', errors: Array<{ __typename: 'ProductError', attributes: Array | null, code: ProductErrorCode, field: string | null, message: string | null }>, product: { __typename: 'Product', name: string, slug: string, description: any | null, seoTitle: string | null, seoDescription: string | null, rating: number | null, chargeTaxes: boolean, isAvailable: boolean | null, id: string, defaultVariant: { __typename: 'ProductVariant', id: string } | null, category: { __typename: 'Category', id: string, name: string } | null, collections: Array<{ __typename: 'Collection', id: string, name: string }> | null, channelListings: Array<{ __typename: 'ProductChannelListing', isPublished: boolean, publicationDate: any | null, isAvailableForPurchase: boolean | null, availableForPurchase: any | null, visibleInListings: boolean, channel: { __typename: 'Channel', id: string, name: string, currencyCode: string } }> | null, media: Array<{ __typename: 'ProductMedia', id: string, alt: string, sortOrder: number | null, url: string, type: ProductMediaType, oembedData: any }> | null, variants: Array<{ __typename: 'ProductVariant', id: string, sku: string | null, name: string, margin: number | null, trackInventory: boolean, quantityLimitPerCustomer: number | null, media: Array<{ __typename: 'ProductMedia', url: string }> | null, stocks: Array<{ __typename: 'Stock', id: string, quantity: number, quantityAllocated: number, warehouse: { __typename: 'Warehouse', id: string, name: string } }> | null, preorder: { __typename: 'PreorderData', globalThreshold: number | null, globalSoldUnits: number, endDate: any | null } | null, channelListings: Array<{ __typename: 'ProductVariantChannelListing', channel: { __typename: 'Channel', id: string, name: string, currencyCode: string }, price: { __typename: 'Money', amount: number, currency: string } | null, costPrice: { __typename: 'Money', amount: number, currency: string } | null, preorderThreshold: { __typename: 'PreorderThreshold', quantity: number | null, soldUnits: number } | null }> | null }> | null, productType: { __typename: 'ProductType', id: string, name: string, hasVariants: boolean, taxType: { __typename: 'TaxType', description: string | null, taxCode: string | null } | null, 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 }, weight: { __typename: 'Weight', unit: WeightUnitsEnum, value: number } | null, taxType: { __typename: 'TaxType', description: string | null, taxCode: string | null } | null, 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 }> }>, metadata: Array<{ __typename: 'MetadataItem', key: string, value: string }>, privateMetadata: Array<{ __typename: 'MetadataItem', key: string, value: string }> } | null } | null }; - -export type SimpleProductUpdateMutationVariables = Exact<{ - id: Scalars['ID']; - input: ProductInput; - productVariantId: Scalars['ID']; - productVariantInput: ProductVariantInput; - addStocks: Array | StockInput; - deleteStocks: Array | Scalars['ID']; - updateStocks: Array | StockInput; - firstValues?: InputMaybe; - afterValues?: InputMaybe; - lastValues?: InputMaybe; - beforeValues?: InputMaybe; -}>; - - -export type SimpleProductUpdateMutation = { __typename: 'Mutation', productUpdate: { __typename: 'ProductUpdate', errors: Array<{ __typename: 'ProductError', attributes: Array | null, code: ProductErrorCode, field: string | null, message: string | null }>, product: { __typename: 'Product', name: string, slug: string, description: any | null, seoTitle: string | null, seoDescription: string | null, rating: number | null, chargeTaxes: boolean, isAvailable: boolean | null, id: string, defaultVariant: { __typename: 'ProductVariant', id: string } | null, category: { __typename: 'Category', id: string, name: string } | null, collections: Array<{ __typename: 'Collection', id: string, name: string }> | null, channelListings: Array<{ __typename: 'ProductChannelListing', isPublished: boolean, publicationDate: any | null, isAvailableForPurchase: boolean | null, availableForPurchase: any | null, visibleInListings: boolean, channel: { __typename: 'Channel', id: string, name: string, currencyCode: string } }> | null, media: Array<{ __typename: 'ProductMedia', id: string, alt: string, sortOrder: number | null, url: string, type: ProductMediaType, oembedData: any }> | null, variants: Array<{ __typename: 'ProductVariant', id: string, sku: string | null, name: string, margin: number | null, trackInventory: boolean, quantityLimitPerCustomer: number | null, media: Array<{ __typename: 'ProductMedia', url: string }> | null, stocks: Array<{ __typename: 'Stock', id: string, quantity: number, quantityAllocated: number, warehouse: { __typename: 'Warehouse', id: string, name: string } }> | null, preorder: { __typename: 'PreorderData', globalThreshold: number | null, globalSoldUnits: number, endDate: any | null } | null, channelListings: Array<{ __typename: 'ProductVariantChannelListing', channel: { __typename: 'Channel', id: string, name: string, currencyCode: string }, price: { __typename: 'Money', amount: number, currency: string } | null, costPrice: { __typename: 'Money', amount: number, currency: string } | null, preorderThreshold: { __typename: 'PreorderThreshold', quantity: number | null, soldUnits: number } | null }> | null }> | null, productType: { __typename: 'ProductType', id: string, name: string, hasVariants: boolean, taxType: { __typename: 'TaxType', description: string | null, taxCode: string | null } | null, 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 }, weight: { __typename: 'Weight', unit: WeightUnitsEnum, value: number } | null, taxType: { __typename: 'TaxType', description: string | null, taxCode: string | null } | null, 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 }> }>, metadata: Array<{ __typename: 'MetadataItem', key: string, value: string }>, privateMetadata: Array<{ __typename: 'MetadataItem', key: string, value: string }> } | null } | null, productVariantUpdate: { __typename: 'ProductVariantUpdate', errors: Array<{ __typename: 'ProductError', attributes: Array | null, code: ProductErrorCode, field: string | null, message: string | null }>, productVariant: { __typename: 'ProductVariant', id: string, name: string, sku: string | null, trackInventory: boolean, quantityLimitPerCustomer: number | null, selectionAttributes: Array<{ __typename: 'SelectedAttribute', attribute: { __typename: 'Attribute', id: string, name: string | null, slug: 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 }> }>, nonSelectionAttributes: Array<{ __typename: 'SelectedAttribute', attribute: { __typename: 'Attribute', id: string, name: string | null, slug: 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 }> }>, media: Array<{ __typename: 'ProductMedia', id: string, url: string, type: ProductMediaType, oembedData: any }> | null, product: { __typename: 'Product', id: string, name: string, defaultVariant: { __typename: 'ProductVariant', id: string } | null, media: Array<{ __typename: 'ProductMedia', id: string, alt: string, sortOrder: number | null, url: string, type: ProductMediaType, oembedData: any }> | null, thumbnail: { __typename: 'Image', url: string } | null, channelListings: Array<{ __typename: 'ProductChannelListing', publicationDate: any | null, isPublished: boolean, channel: { __typename: 'Channel', id: string, name: string, currencyCode: string } }> | null, variants: Array<{ __typename: 'ProductVariant', id: string, name: string, sku: string | null, media: Array<{ __typename: 'ProductMedia', id: string, url: string, type: ProductMediaType, oembedData: any }> | null }> | null }, channelListings: Array<{ __typename: 'ProductVariantChannelListing', channel: { __typename: 'Channel', id: string, name: string, currencyCode: string }, price: { __typename: 'Money', amount: number, currency: string } | null, costPrice: { __typename: 'Money', amount: number, currency: string } | null, preorderThreshold: { __typename: 'PreorderThreshold', quantity: number | null, soldUnits: number } | null }> | null, stocks: Array<{ __typename: 'Stock', id: string, quantity: number, quantityAllocated: number, warehouse: { __typename: 'Warehouse', id: string, name: string } }> | null, preorder: { __typename: 'PreorderData', globalThreshold: number | null, globalSoldUnits: number, endDate: any | null } | null, weight: { __typename: 'Weight', unit: WeightUnitsEnum, value: number } | null, metadata: Array<{ __typename: 'MetadataItem', key: string, value: string }>, privateMetadata: Array<{ __typename: 'MetadataItem', key: string, value: string }> } | null } | null, productVariantStocksCreate: { __typename: 'ProductVariantStocksCreate', errors: Array<{ __typename: 'BulkStockError', code: ProductErrorCode, field: string | null, index: number | null, message: string | null }>, productVariant: { __typename: 'ProductVariant', id: string, name: string, sku: string | null, trackInventory: boolean, quantityLimitPerCustomer: number | null, selectionAttributes: Array<{ __typename: 'SelectedAttribute', attribute: { __typename: 'Attribute', id: string, name: string | null, slug: 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 }> }>, nonSelectionAttributes: Array<{ __typename: 'SelectedAttribute', attribute: { __typename: 'Attribute', id: string, name: string | null, slug: 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 }> }>, media: Array<{ __typename: 'ProductMedia', id: string, url: string, type: ProductMediaType, oembedData: any }> | null, product: { __typename: 'Product', id: string, name: string, defaultVariant: { __typename: 'ProductVariant', id: string } | null, media: Array<{ __typename: 'ProductMedia', id: string, alt: string, sortOrder: number | null, url: string, type: ProductMediaType, oembedData: any }> | null, thumbnail: { __typename: 'Image', url: string } | null, channelListings: Array<{ __typename: 'ProductChannelListing', publicationDate: any | null, isPublished: boolean, channel: { __typename: 'Channel', id: string, name: string, currencyCode: string } }> | null, variants: Array<{ __typename: 'ProductVariant', id: string, name: string, sku: string | null, media: Array<{ __typename: 'ProductMedia', id: string, url: string, type: ProductMediaType, oembedData: any }> | null }> | null }, channelListings: Array<{ __typename: 'ProductVariantChannelListing', channel: { __typename: 'Channel', id: string, name: string, currencyCode: string }, price: { __typename: 'Money', amount: number, currency: string } | null, costPrice: { __typename: 'Money', amount: number, currency: string } | null, preorderThreshold: { __typename: 'PreorderThreshold', quantity: number | null, soldUnits: number } | null }> | null, stocks: Array<{ __typename: 'Stock', id: string, quantity: number, quantityAllocated: number, warehouse: { __typename: 'Warehouse', id: string, name: string } }> | null, preorder: { __typename: 'PreorderData', globalThreshold: number | null, globalSoldUnits: number, endDate: any | null } | null, weight: { __typename: 'Weight', unit: WeightUnitsEnum, value: number } | null, metadata: Array<{ __typename: 'MetadataItem', key: string, value: string }>, privateMetadata: Array<{ __typename: 'MetadataItem', key: string, value: string }> } | null } | null, productVariantStocksDelete: { __typename: 'ProductVariantStocksDelete', errors: Array<{ __typename: 'StockError', code: StockErrorCode, field: string | null, message: string | null }>, productVariant: { __typename: 'ProductVariant', id: string, name: string, sku: string | null, trackInventory: boolean, quantityLimitPerCustomer: number | null, selectionAttributes: Array<{ __typename: 'SelectedAttribute', attribute: { __typename: 'Attribute', id: string, name: string | null, slug: 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 }> }>, nonSelectionAttributes: Array<{ __typename: 'SelectedAttribute', attribute: { __typename: 'Attribute', id: string, name: string | null, slug: 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 }> }>, media: Array<{ __typename: 'ProductMedia', id: string, url: string, type: ProductMediaType, oembedData: any }> | null, product: { __typename: 'Product', id: string, name: string, defaultVariant: { __typename: 'ProductVariant', id: string } | null, media: Array<{ __typename: 'ProductMedia', id: string, alt: string, sortOrder: number | null, url: string, type: ProductMediaType, oembedData: any }> | null, thumbnail: { __typename: 'Image', url: string } | null, channelListings: Array<{ __typename: 'ProductChannelListing', publicationDate: any | null, isPublished: boolean, channel: { __typename: 'Channel', id: string, name: string, currencyCode: string } }> | null, variants: Array<{ __typename: 'ProductVariant', id: string, name: string, sku: string | null, media: Array<{ __typename: 'ProductMedia', id: string, url: string, type: ProductMediaType, oembedData: any }> | null }> | null }, channelListings: Array<{ __typename: 'ProductVariantChannelListing', channel: { __typename: 'Channel', id: string, name: string, currencyCode: string }, price: { __typename: 'Money', amount: number, currency: string } | null, costPrice: { __typename: 'Money', amount: number, currency: string } | null, preorderThreshold: { __typename: 'PreorderThreshold', quantity: number | null, soldUnits: number } | null }> | null, stocks: Array<{ __typename: 'Stock', id: string, quantity: number, quantityAllocated: number, warehouse: { __typename: 'Warehouse', id: string, name: string } }> | null, preorder: { __typename: 'PreorderData', globalThreshold: number | null, globalSoldUnits: number, endDate: any | null } | null, weight: { __typename: 'Weight', unit: WeightUnitsEnum, value: number } | null, metadata: Array<{ __typename: 'MetadataItem', key: string, value: string }>, privateMetadata: Array<{ __typename: 'MetadataItem', key: string, value: string }> } | null } | null, productVariantStocksUpdate: { __typename: 'ProductVariantStocksUpdate', errors: Array<{ __typename: 'BulkStockError', code: ProductErrorCode, field: string | null, index: number | null, message: string | null }>, productVariant: { __typename: 'ProductVariant', id: string, name: string, sku: string | null, trackInventory: boolean, quantityLimitPerCustomer: number | null, selectionAttributes: Array<{ __typename: 'SelectedAttribute', attribute: { __typename: 'Attribute', id: string, name: string | null, slug: 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 }> }>, nonSelectionAttributes: Array<{ __typename: 'SelectedAttribute', attribute: { __typename: 'Attribute', id: string, name: string | null, slug: 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 }> }>, media: Array<{ __typename: 'ProductMedia', id: string, url: string, type: ProductMediaType, oembedData: any }> | null, product: { __typename: 'Product', id: string, name: string, defaultVariant: { __typename: 'ProductVariant', id: string } | null, media: Array<{ __typename: 'ProductMedia', id: string, alt: string, sortOrder: number | null, url: string, type: ProductMediaType, oembedData: any }> | null, thumbnail: { __typename: 'Image', url: string } | null, channelListings: Array<{ __typename: 'ProductChannelListing', publicationDate: any | null, isPublished: boolean, channel: { __typename: 'Channel', id: string, name: string, currencyCode: string } }> | null, variants: Array<{ __typename: 'ProductVariant', id: string, name: string, sku: string | null, media: Array<{ __typename: 'ProductMedia', id: string, url: string, type: ProductMediaType, oembedData: any }> | null }> | null }, channelListings: Array<{ __typename: 'ProductVariantChannelListing', channel: { __typename: 'Channel', id: string, name: string, currencyCode: string }, price: { __typename: 'Money', amount: number, currency: string } | null, costPrice: { __typename: 'Money', amount: number, currency: string } | null, preorderThreshold: { __typename: 'PreorderThreshold', quantity: number | null, soldUnits: number } | null }> | null, stocks: Array<{ __typename: 'Stock', id: string, quantity: number, quantityAllocated: number, warehouse: { __typename: 'Warehouse', id: string, name: string } }> | null, preorder: { __typename: 'PreorderData', globalThreshold: number | null, globalSoldUnits: number, endDate: any | null } | null, weight: { __typename: 'Weight', unit: WeightUnitsEnum, value: number } | null, metadata: Array<{ __typename: 'MetadataItem', key: string, value: string }>, privateMetadata: Array<{ __typename: 'MetadataItem', key: string, value: string }> } | null } | null }; +export type ProductUpdateMutation = { __typename: 'Mutation', productUpdate: { __typename: 'ProductUpdate', errors: Array<{ __typename: 'ProductError', attributes: Array | null, code: ProductErrorCode, field: string | null, message: string | null }> } | null }; export type ProductCreateMutationVariables = Exact<{ input: ProductCreateInput; @@ -7741,6 +7744,31 @@ export type VariantDeleteMutationVariables = Exact<{ export type VariantDeleteMutation = { __typename: 'Mutation', productVariantDelete: { __typename: 'ProductVariantDelete', errors: Array<{ __typename: 'ProductError', code: ProductErrorCode, field: string | null, message: string | null }>, productVariant: { __typename: 'ProductVariant', id: string } | null } | null }; +export type VariantDatagridUpdateMutationVariables = Exact<{ + id: Scalars['ID']; + input: ProductVariantInput; +}>; + + +export type VariantDatagridUpdateMutation = { __typename: 'Mutation', productVariantUpdate: { __typename: 'ProductVariantUpdate', errors: Array<{ __typename: 'ProductError', attributes: Array | null, code: ProductErrorCode, field: string | null, message: string | null }> } | null }; + +export type VariantDatagridStockUpdateMutationVariables = Exact<{ + stocks: Array | StockInput; + removeStocks: Array | Scalars['ID']; + id: Scalars['ID']; +}>; + + +export type VariantDatagridStockUpdateMutation = { __typename: 'Mutation', productVariantStocksDelete: { __typename: 'ProductVariantStocksDelete', errors: Array<{ __typename: 'StockError', code: StockErrorCode, field: string | null, message: string | null }> } | null, productVariantStocksUpdate: { __typename: 'ProductVariantStocksUpdate', errors: Array<{ __typename: 'BulkStockError', code: ProductErrorCode, field: string | null, index: number | null, message: string | null }> } | null }; + +export type VariantDatagridChannelListingUpdateMutationVariables = Exact<{ + id: Scalars['ID']; + input: Array | ProductVariantChannelListingAddInput; +}>; + + +export type VariantDatagridChannelListingUpdateMutation = { __typename: 'Mutation', productVariantChannelListingUpdate: { __typename: 'ProductVariantChannelListingUpdate', errors: Array<{ __typename: 'ProductChannelListingError', code: ProductErrorCode, field: string | null, message: string | null, channels: Array | null }> } | null }; + export type VariantUpdateMutationVariables = Exact<{ addStocks: Array | StockInput; removeStocks: Array | Scalars['ID']; @@ -7759,7 +7787,7 @@ export type VariantUpdateMutationVariables = Exact<{ }>; -export type VariantUpdateMutation = { __typename: 'Mutation', productVariantStocksDelete: { __typename: 'ProductVariantStocksDelete', errors: Array<{ __typename: 'StockError', code: StockErrorCode, field: string | null, message: string | null }>, productVariant: { __typename: 'ProductVariant', id: string, stocks: Array<{ __typename: 'Stock', id: string, quantity: number, quantityAllocated: number, warehouse: { __typename: 'Warehouse', id: string, name: string } }> | null } | null } | null, productVariantStocksCreate: { __typename: 'ProductVariantStocksCreate', errors: Array<{ __typename: 'BulkStockError', code: ProductErrorCode, field: string | null, index: number | null, message: string | null }>, productVariant: { __typename: 'ProductVariant', id: string, stocks: Array<{ __typename: 'Stock', id: string, quantity: number, quantityAllocated: number, warehouse: { __typename: 'Warehouse', id: string, name: string } }> | null } | null } | null, productVariantStocksUpdate: { __typename: 'ProductVariantStocksUpdate', errors: Array<{ __typename: 'BulkStockError', code: ProductErrorCode, field: string | null, index: number | null, message: string | null }>, productVariant: { __typename: 'ProductVariant', id: string, name: string, sku: string | null, trackInventory: boolean, quantityLimitPerCustomer: number | null, selectionAttributes: Array<{ __typename: 'SelectedAttribute', attribute: { __typename: 'Attribute', id: string, name: string | null, slug: 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 }> }>, nonSelectionAttributes: Array<{ __typename: 'SelectedAttribute', attribute: { __typename: 'Attribute', id: string, name: string | null, slug: 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 }> }>, media: Array<{ __typename: 'ProductMedia', id: string, url: string, type: ProductMediaType, oembedData: any }> | null, product: { __typename: 'Product', id: string, name: string, defaultVariant: { __typename: 'ProductVariant', id: string } | null, media: Array<{ __typename: 'ProductMedia', id: string, alt: string, sortOrder: number | null, url: string, type: ProductMediaType, oembedData: any }> | null, thumbnail: { __typename: 'Image', url: string } | null, channelListings: Array<{ __typename: 'ProductChannelListing', publicationDate: any | null, isPublished: boolean, channel: { __typename: 'Channel', id: string, name: string, currencyCode: string } }> | null, variants: Array<{ __typename: 'ProductVariant', id: string, name: string, sku: string | null, media: Array<{ __typename: 'ProductMedia', id: string, url: string, type: ProductMediaType, oembedData: any }> | null }> | null }, channelListings: Array<{ __typename: 'ProductVariantChannelListing', channel: { __typename: 'Channel', id: string, name: string, currencyCode: string }, price: { __typename: 'Money', amount: number, currency: string } | null, costPrice: { __typename: 'Money', amount: number, currency: string } | null, preorderThreshold: { __typename: 'PreorderThreshold', quantity: number | null, soldUnits: number } | null }> | null, stocks: Array<{ __typename: 'Stock', id: string, quantity: number, quantityAllocated: number, warehouse: { __typename: 'Warehouse', id: string, name: string } }> | null, preorder: { __typename: 'PreorderData', globalThreshold: number | null, globalSoldUnits: number, endDate: any | null } | null, weight: { __typename: 'Weight', unit: WeightUnitsEnum, value: number } | null, metadata: Array<{ __typename: 'MetadataItem', key: string, value: string }>, privateMetadata: Array<{ __typename: 'MetadataItem', key: string, value: string }> } | null } | null, productVariantUpdate: { __typename: 'ProductVariantUpdate', errors: Array<{ __typename: 'ProductError', attributes: Array | null, code: ProductErrorCode, field: string | null, message: string | null }>, productVariant: { __typename: 'ProductVariant', id: string, name: string, sku: string | null, trackInventory: boolean, quantityLimitPerCustomer: number | null, selectionAttributes: Array<{ __typename: 'SelectedAttribute', attribute: { __typename: 'Attribute', id: string, name: string | null, slug: 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 }> }>, nonSelectionAttributes: Array<{ __typename: 'SelectedAttribute', attribute: { __typename: 'Attribute', id: string, name: string | null, slug: 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 }> }>, media: Array<{ __typename: 'ProductMedia', id: string, url: string, type: ProductMediaType, oembedData: any }> | null, product: { __typename: 'Product', id: string, name: string, defaultVariant: { __typename: 'ProductVariant', id: string } | null, media: Array<{ __typename: 'ProductMedia', id: string, alt: string, sortOrder: number | null, url: string, type: ProductMediaType, oembedData: any }> | null, thumbnail: { __typename: 'Image', url: string } | null, channelListings: Array<{ __typename: 'ProductChannelListing', publicationDate: any | null, isPublished: boolean, channel: { __typename: 'Channel', id: string, name: string, currencyCode: string } }> | null, variants: Array<{ __typename: 'ProductVariant', id: string, name: string, sku: string | null, media: Array<{ __typename: 'ProductMedia', id: string, url: string, type: ProductMediaType, oembedData: any }> | null }> | null }, channelListings: Array<{ __typename: 'ProductVariantChannelListing', channel: { __typename: 'Channel', id: string, name: string, currencyCode: string }, price: { __typename: 'Money', amount: number, currency: string } | null, costPrice: { __typename: 'Money', amount: number, currency: string } | null, preorderThreshold: { __typename: 'PreorderThreshold', quantity: number | null, soldUnits: number } | null }> | null, stocks: Array<{ __typename: 'Stock', id: string, quantity: number, quantityAllocated: number, warehouse: { __typename: 'Warehouse', id: string, name: string } }> | null, preorder: { __typename: 'PreorderData', globalThreshold: number | null, globalSoldUnits: number, endDate: any | null } | null, weight: { __typename: 'Weight', unit: WeightUnitsEnum, value: number } | null, metadata: Array<{ __typename: 'MetadataItem', key: string, value: string }>, privateMetadata: Array<{ __typename: 'MetadataItem', key: string, value: string }> } | null } | null }; +export type VariantUpdateMutation = { __typename: 'Mutation', productVariantStocksDelete: { __typename: 'ProductVariantStocksDelete', errors: Array<{ __typename: 'StockError', code: StockErrorCode, field: string | null, message: string | null }>, productVariant: { __typename: 'ProductVariant', id: string, stocks: Array<{ __typename: 'Stock', id: string, quantity: number, quantityAllocated: number, warehouse: { __typename: 'Warehouse', id: string, name: string } }> | null } | null } | null, productVariantStocksCreate: { __typename: 'ProductVariantStocksCreate', errors: Array<{ __typename: 'BulkStockError', code: ProductErrorCode, field: string | null, index: number | null, message: string | null }>, productVariant: { __typename: 'ProductVariant', id: string, stocks: Array<{ __typename: 'Stock', id: string, quantity: number, quantityAllocated: number, warehouse: { __typename: 'Warehouse', id: string, name: string } }> | null } | null } | null, productVariantStocksUpdate: { __typename: 'ProductVariantStocksUpdate', errors: Array<{ __typename: 'BulkStockError', code: ProductErrorCode, field: string | null, index: number | null, message: string | null }>, productVariant: { __typename: 'ProductVariant', id: string, name: string, sku: string | null, trackInventory: boolean, quantityLimitPerCustomer: number | null, selectionAttributes: Array<{ __typename: 'SelectedAttribute', attribute: { __typename: 'Attribute', id: string, name: string | null, slug: 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 }> }>, nonSelectionAttributes: Array<{ __typename: 'SelectedAttribute', attribute: { __typename: 'Attribute', id: string, name: string | null, slug: 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 }> }>, media: Array<{ __typename: 'ProductMedia', id: string, url: string, type: ProductMediaType, oembedData: any }> | null, product: { __typename: 'Product', id: string, name: string, defaultVariant: { __typename: 'ProductVariant', id: string } | null, media: Array<{ __typename: 'ProductMedia', id: string, alt: string, sortOrder: number | null, url: string, type: ProductMediaType, oembedData: any }> | null, thumbnail: { __typename: 'Image', url: string } | null, channelListings: Array<{ __typename: 'ProductChannelListing', id: string, publicationDate: any | null, isPublished: boolean, channel: { __typename: 'Channel', id: string, name: string, currencyCode: string } }> | null, variants: Array<{ __typename: 'ProductVariant', id: string, name: string, sku: string | null, media: Array<{ __typename: 'ProductMedia', id: string, url: string, type: ProductMediaType, oembedData: any }> | null }> | null }, channelListings: Array<{ __typename: 'ProductVariantChannelListing', channel: { __typename: 'Channel', id: string, name: string, currencyCode: string }, price: { __typename: 'Money', amount: number, currency: string } | null, costPrice: { __typename: 'Money', amount: number, currency: string } | null, preorderThreshold: { __typename: 'PreorderThreshold', quantity: number | null, soldUnits: number } | null }> | null, stocks: Array<{ __typename: 'Stock', id: string, quantity: number, quantityAllocated: number, warehouse: { __typename: 'Warehouse', id: string, name: string } }> | null, preorder: { __typename: 'PreorderData', globalThreshold: number | null, globalSoldUnits: number, endDate: any | null } | null, weight: { __typename: 'Weight', unit: WeightUnitsEnum, value: number } | null, metadata: Array<{ __typename: 'MetadataItem', key: string, value: string }>, privateMetadata: Array<{ __typename: 'MetadataItem', key: string, value: string }> } | null } | null, productVariantUpdate: { __typename: 'ProductVariantUpdate', errors: Array<{ __typename: 'ProductError', attributes: Array | null, code: ProductErrorCode, field: string | null, message: string | null }>, productVariant: { __typename: 'ProductVariant', id: string, name: string, sku: string | null, trackInventory: boolean, quantityLimitPerCustomer: number | null, selectionAttributes: Array<{ __typename: 'SelectedAttribute', attribute: { __typename: 'Attribute', id: string, name: string | null, slug: 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 }> }>, nonSelectionAttributes: Array<{ __typename: 'SelectedAttribute', attribute: { __typename: 'Attribute', id: string, name: string | null, slug: 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 }> }>, media: Array<{ __typename: 'ProductMedia', id: string, url: string, type: ProductMediaType, oembedData: any }> | null, product: { __typename: 'Product', id: string, name: string, defaultVariant: { __typename: 'ProductVariant', id: string } | null, media: Array<{ __typename: 'ProductMedia', id: string, alt: string, sortOrder: number | null, url: string, type: ProductMediaType, oembedData: any }> | null, thumbnail: { __typename: 'Image', url: string } | null, channelListings: Array<{ __typename: 'ProductChannelListing', id: string, publicationDate: any | null, isPublished: boolean, channel: { __typename: 'Channel', id: string, name: string, currencyCode: string } }> | null, variants: Array<{ __typename: 'ProductVariant', id: string, name: string, sku: string | null, media: Array<{ __typename: 'ProductMedia', id: string, url: string, type: ProductMediaType, oembedData: any }> | null }> | null }, channelListings: Array<{ __typename: 'ProductVariantChannelListing', channel: { __typename: 'Channel', id: string, name: string, currencyCode: string }, price: { __typename: 'Money', amount: number, currency: string } | null, costPrice: { __typename: 'Money', amount: number, currency: string } | null, preorderThreshold: { __typename: 'PreorderThreshold', quantity: number | null, soldUnits: number } | null }> | null, stocks: Array<{ __typename: 'Stock', id: string, quantity: number, quantityAllocated: number, warehouse: { __typename: 'Warehouse', id: string, name: string } }> | null, preorder: { __typename: 'PreorderData', globalThreshold: number | null, globalSoldUnits: number, endDate: any | null } | null, weight: { __typename: 'Weight', unit: WeightUnitsEnum, value: number } | null, metadata: Array<{ __typename: 'MetadataItem', key: string, value: string }>, privateMetadata: Array<{ __typename: 'MetadataItem', key: string, value: string }> } | null } | null }; export type VariantCreateMutationVariables = Exact<{ input: ProductVariantCreateInput; @@ -7770,7 +7798,7 @@ export type VariantCreateMutationVariables = Exact<{ }>; -export type VariantCreateMutation = { __typename: 'Mutation', productVariantCreate: { __typename: 'ProductVariantCreate', errors: Array<{ __typename: 'ProductError', attributes: Array | null, code: ProductErrorCode, field: string | null, message: string | null }>, productVariant: { __typename: 'ProductVariant', id: string, name: string, sku: string | null, trackInventory: boolean, quantityLimitPerCustomer: number | null, selectionAttributes: Array<{ __typename: 'SelectedAttribute', attribute: { __typename: 'Attribute', id: string, name: string | null, slug: 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 }> }>, nonSelectionAttributes: Array<{ __typename: 'SelectedAttribute', attribute: { __typename: 'Attribute', id: string, name: string | null, slug: 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 }> }>, media: Array<{ __typename: 'ProductMedia', id: string, url: string, type: ProductMediaType, oembedData: any }> | null, product: { __typename: 'Product', id: string, name: string, defaultVariant: { __typename: 'ProductVariant', id: string } | null, media: Array<{ __typename: 'ProductMedia', id: string, alt: string, sortOrder: number | null, url: string, type: ProductMediaType, oembedData: any }> | null, thumbnail: { __typename: 'Image', url: string } | null, channelListings: Array<{ __typename: 'ProductChannelListing', publicationDate: any | null, isPublished: boolean, channel: { __typename: 'Channel', id: string, name: string, currencyCode: string } }> | null, variants: Array<{ __typename: 'ProductVariant', id: string, name: string, sku: string | null, media: Array<{ __typename: 'ProductMedia', id: string, url: string, type: ProductMediaType, oembedData: any }> | null }> | null }, channelListings: Array<{ __typename: 'ProductVariantChannelListing', channel: { __typename: 'Channel', id: string, name: string, currencyCode: string }, price: { __typename: 'Money', amount: number, currency: string } | null, costPrice: { __typename: 'Money', amount: number, currency: string } | null, preorderThreshold: { __typename: 'PreorderThreshold', quantity: number | null, soldUnits: number } | null }> | null, stocks: Array<{ __typename: 'Stock', id: string, quantity: number, quantityAllocated: number, warehouse: { __typename: 'Warehouse', id: string, name: string } }> | null, preorder: { __typename: 'PreorderData', globalThreshold: number | null, globalSoldUnits: number, endDate: any | null } | null, weight: { __typename: 'Weight', unit: WeightUnitsEnum, value: number } | null, metadata: Array<{ __typename: 'MetadataItem', key: string, value: string }>, privateMetadata: Array<{ __typename: 'MetadataItem', key: string, value: string }> } | null } | null }; +export type VariantCreateMutation = { __typename: 'Mutation', productVariantCreate: { __typename: 'ProductVariantCreate', errors: Array<{ __typename: 'ProductError', attributes: Array | null, code: ProductErrorCode, field: string | null, message: string | null }>, productVariant: { __typename: 'ProductVariant', id: string, name: string, sku: string | null, trackInventory: boolean, quantityLimitPerCustomer: number | null, selectionAttributes: Array<{ __typename: 'SelectedAttribute', attribute: { __typename: 'Attribute', id: string, name: string | null, slug: 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 }> }>, nonSelectionAttributes: Array<{ __typename: 'SelectedAttribute', attribute: { __typename: 'Attribute', id: string, name: string | null, slug: 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 }> }>, media: Array<{ __typename: 'ProductMedia', id: string, url: string, type: ProductMediaType, oembedData: any }> | null, product: { __typename: 'Product', id: string, name: string, defaultVariant: { __typename: 'ProductVariant', id: string } | null, media: Array<{ __typename: 'ProductMedia', id: string, alt: string, sortOrder: number | null, url: string, type: ProductMediaType, oembedData: any }> | null, thumbnail: { __typename: 'Image', url: string } | null, channelListings: Array<{ __typename: 'ProductChannelListing', id: string, publicationDate: any | null, isPublished: boolean, channel: { __typename: 'Channel', id: string, name: string, currencyCode: string } }> | null, variants: Array<{ __typename: 'ProductVariant', id: string, name: string, sku: string | null, media: Array<{ __typename: 'ProductMedia', id: string, url: string, type: ProductMediaType, oembedData: any }> | null }> | null }, channelListings: Array<{ __typename: 'ProductVariantChannelListing', channel: { __typename: 'Channel', id: string, name: string, currencyCode: string }, price: { __typename: 'Money', amount: number, currency: string } | null, costPrice: { __typename: 'Money', amount: number, currency: string } | null, preorderThreshold: { __typename: 'PreorderThreshold', quantity: number | null, soldUnits: number } | null }> | null, stocks: Array<{ __typename: 'Stock', id: string, quantity: number, quantityAllocated: number, warehouse: { __typename: 'Warehouse', id: string, name: string } }> | null, preorder: { __typename: 'PreorderData', globalThreshold: number | null, globalSoldUnits: number, endDate: any | null } | null, weight: { __typename: 'Weight', unit: WeightUnitsEnum, value: number } | null, metadata: Array<{ __typename: 'MetadataItem', key: string, value: string }>, privateMetadata: Array<{ __typename: 'MetadataItem', key: string, value: string }> } | null } | null }; export type ProductMediaDeleteMutationVariables = Exact<{ id: Scalars['ID']; @@ -7810,6 +7838,14 @@ 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; +}>; + + +export type ProductVariantBulkCreateMutation = { __typename: 'Mutation', productVariantBulkCreate: { __typename: 'ProductVariantBulkCreate', errors: Array<{ __typename: 'BulkProductError', field: string | null, code: ProductErrorCode, index: number | null, channels: Array | null, message: string | null }>, productVariants: Array<{ __typename: 'ProductVariant', id: string }> } | null }; + export type ProductVariantBulkDeleteMutationVariables = Exact<{ ids: Array | Scalars['ID']; }>; @@ -7830,7 +7866,7 @@ export type ProductChannelListingUpdateMutationVariables = Exact<{ }>; -export type ProductChannelListingUpdateMutation = { __typename: 'Mutation', productChannelListingUpdate: { __typename: 'ProductChannelListingUpdate', product: { __typename: 'Product', id: string, channelListings: Array<{ __typename: 'ProductChannelListing', isPublished: boolean, publicationDate: any | null, isAvailableForPurchase: boolean | null, availableForPurchase: any | null, visibleInListings: boolean, channel: { __typename: 'Channel', id: string, name: string, currencyCode: string } }> | null, variants: Array<{ __typename: 'ProductVariant', id: string, channelListings: Array<{ __typename: 'ProductVariantChannelListing', channel: { __typename: 'Channel', id: string, name: string, currencyCode: string }, price: { __typename: 'Money', amount: number, currency: string } | null, costPrice: { __typename: 'Money', amount: number, currency: string } | null, preorderThreshold: { __typename: 'PreorderThreshold', quantity: number | null, soldUnits: number } | null }> | null }> | null } | null, errors: Array<{ __typename: 'ProductChannelListingError', code: ProductErrorCode, field: string | null, message: string | null, channels: Array | null }> } | null }; +export type ProductChannelListingUpdateMutation = { __typename: 'Mutation', productChannelListingUpdate: { __typename: 'ProductChannelListingUpdate', errors: Array<{ __typename: 'ProductChannelListingError', code: ProductErrorCode, field: string | null, message: string | null, channels: Array | null }> } | null }; export type ProductVariantReorderMutationVariables = Exact<{ move: ReorderInput; @@ -7914,7 +7950,7 @@ export type ProductDetailsQueryVariables = Exact<{ }>; -export type ProductDetailsQuery = { __typename: 'Query', product: { __typename: 'Product', name: string, slug: string, description: any | null, seoTitle: string | null, seoDescription: string | null, rating: number | null, chargeTaxes: boolean, isAvailable: boolean | null, id: string, defaultVariant: { __typename: 'ProductVariant', id: string } | null, category: { __typename: 'Category', id: string, name: string } | null, collections: Array<{ __typename: 'Collection', id: string, name: string }> | null, channelListings: Array<{ __typename: 'ProductChannelListing', isPublished: boolean, publicationDate: any | null, isAvailableForPurchase: boolean | null, availableForPurchase: any | null, visibleInListings: boolean, channel: { __typename: 'Channel', id: string, name: string, currencyCode: string } }> | null, media: Array<{ __typename: 'ProductMedia', id: string, alt: string, sortOrder: number | null, url: string, type: ProductMediaType, oembedData: any }> | null, variants: Array<{ __typename: 'ProductVariant', id: string, sku: string | null, name: string, margin: number | null, trackInventory: boolean, quantityLimitPerCustomer: number | null, media: Array<{ __typename: 'ProductMedia', url: string }> | null, stocks: Array<{ __typename: 'Stock', id: string, quantity: number, quantityAllocated: number, warehouse: { __typename: 'Warehouse', id: string, name: string } }> | null, preorder: { __typename: 'PreorderData', globalThreshold: number | null, globalSoldUnits: number, endDate: any | null } | null, channelListings: Array<{ __typename: 'ProductVariantChannelListing', channel: { __typename: 'Channel', id: string, name: string, currencyCode: string }, price: { __typename: 'Money', amount: number, currency: string } | null, costPrice: { __typename: 'Money', amount: number, currency: string } | null, preorderThreshold: { __typename: 'PreorderThreshold', quantity: number | null, soldUnits: number } | null }> | null }> | null, productType: { __typename: 'ProductType', id: string, name: string, hasVariants: boolean, taxType: { __typename: 'TaxType', description: string | null, taxCode: string | null } | null, 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 }, weight: { __typename: 'Weight', unit: WeightUnitsEnum, value: number } | null, taxType: { __typename: 'TaxType', description: string | null, taxCode: string | null } | null, 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 }> }>, metadata: Array<{ __typename: 'MetadataItem', key: string, value: string }>, privateMetadata: Array<{ __typename: 'MetadataItem', key: string, value: string }> } | null, taxTypes: Array<{ __typename: 'TaxType', description: string | null, taxCode: string | null }> | null }; +export type ProductDetailsQuery = { __typename: 'Query', product: { __typename: 'Product', name: string, slug: string, description: any | null, seoTitle: string | null, seoDescription: string | null, rating: number | null, chargeTaxes: boolean, isAvailable: boolean | null, id: string, defaultVariant: { __typename: 'ProductVariant', id: string } | null, category: { __typename: 'Category', id: string, name: string } | null, collections: Array<{ __typename: 'Collection', id: string, name: string }> | null, channelListings: Array<{ __typename: 'ProductChannelListing', isPublished: boolean, publicationDate: any | null, isAvailableForPurchase: boolean | null, availableForPurchase: any | null, visibleInListings: boolean, channel: { __typename: 'Channel', id: string, name: string, currencyCode: string } }> | null, media: Array<{ __typename: 'ProductMedia', id: string, alt: string, sortOrder: number | null, url: string, type: ProductMediaType, oembedData: any }> | null, variants: Array<{ __typename: 'ProductVariant', id: string, sku: string | null, name: string, trackInventory: boolean, quantityLimitPerCustomer: number | null, attributes: Array<{ __typename: 'SelectedAttribute', attribute: { __typename: 'Attribute', id: string, name: string | null }, values: Array<{ __typename: 'AttributeValue', id: string, name: string | null }> }>, media: Array<{ __typename: 'ProductMedia', url: string }> | null, stocks: Array<{ __typename: 'Stock', id: string, quantity: number, quantityAllocated: number, warehouse: { __typename: 'Warehouse', id: string, name: string } }> | null, preorder: { __typename: 'PreorderData', globalThreshold: number | null, globalSoldUnits: number, endDate: any | null } | null, channelListings: Array<{ __typename: 'ProductVariantChannelListing', channel: { __typename: 'Channel', id: string, name: string, currencyCode: string }, price: { __typename: 'Money', amount: number, currency: string } | null, costPrice: { __typename: 'Money', amount: number, currency: string } | null, preorderThreshold: { __typename: 'PreorderThreshold', quantity: number | null, soldUnits: number } | null }> | null }> | null, productType: { __typename: 'ProductType', id: string, name: string, hasVariants: boolean, taxType: { __typename: 'TaxType', description: string | null, taxCode: string | null } | null, 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 }, weight: { __typename: 'Weight', unit: WeightUnitsEnum, value: number } | null, taxType: { __typename: 'TaxType', description: string | null, taxCode: string | null } | null, 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 }> }>, metadata: Array<{ __typename: 'MetadataItem', key: string, value: string }>, privateMetadata: Array<{ __typename: 'MetadataItem', key: string, value: string }> } | null, taxTypes: Array<{ __typename: 'TaxType', description: string | null, taxCode: string | null }> | null }; export type ProductTypeQueryVariables = Exact<{ id: Scalars['ID']; @@ -7936,7 +7972,7 @@ export type ProductVariantDetailsQueryVariables = Exact<{ }>; -export type ProductVariantDetailsQuery = { __typename: 'Query', productVariant: { __typename: 'ProductVariant', id: string, name: string, sku: string | null, trackInventory: boolean, quantityLimitPerCustomer: number | null, selectionAttributes: Array<{ __typename: 'SelectedAttribute', attribute: { __typename: 'Attribute', id: string, name: string | null, slug: 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 }> }>, nonSelectionAttributes: Array<{ __typename: 'SelectedAttribute', attribute: { __typename: 'Attribute', id: string, name: string | null, slug: 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 }> }>, media: Array<{ __typename: 'ProductMedia', id: string, url: string, type: ProductMediaType, oembedData: any }> | null, product: { __typename: 'Product', id: string, name: string, defaultVariant: { __typename: 'ProductVariant', id: string } | null, media: Array<{ __typename: 'ProductMedia', id: string, alt: string, sortOrder: number | null, url: string, type: ProductMediaType, oembedData: any }> | null, thumbnail: { __typename: 'Image', url: string } | null, channelListings: Array<{ __typename: 'ProductChannelListing', publicationDate: any | null, isPublished: boolean, channel: { __typename: 'Channel', id: string, name: string, currencyCode: string } }> | null, variants: Array<{ __typename: 'ProductVariant', id: string, name: string, sku: string | null, media: Array<{ __typename: 'ProductMedia', id: string, url: string, type: ProductMediaType, oembedData: any }> | null }> | null }, channelListings: Array<{ __typename: 'ProductVariantChannelListing', channel: { __typename: 'Channel', id: string, name: string, currencyCode: string }, price: { __typename: 'Money', amount: number, currency: string } | null, costPrice: { __typename: 'Money', amount: number, currency: string } | null, preorderThreshold: { __typename: 'PreorderThreshold', quantity: number | null, soldUnits: number } | null }> | null, stocks: Array<{ __typename: 'Stock', id: string, quantity: number, quantityAllocated: number, warehouse: { __typename: 'Warehouse', id: string, name: string } }> | null, preorder: { __typename: 'PreorderData', globalThreshold: number | null, globalSoldUnits: number, endDate: any | null } | null, weight: { __typename: 'Weight', unit: WeightUnitsEnum, value: number } | null, metadata: Array<{ __typename: 'MetadataItem', key: string, value: string }>, privateMetadata: Array<{ __typename: 'MetadataItem', key: string, value: string }> } | null }; +export type ProductVariantDetailsQuery = { __typename: 'Query', productVariant: { __typename: 'ProductVariant', id: string, name: string, sku: string | null, trackInventory: boolean, quantityLimitPerCustomer: number | null, selectionAttributes: Array<{ __typename: 'SelectedAttribute', attribute: { __typename: 'Attribute', id: string, name: string | null, slug: 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 }> }>, nonSelectionAttributes: Array<{ __typename: 'SelectedAttribute', attribute: { __typename: 'Attribute', id: string, name: string | null, slug: 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 }> }>, media: Array<{ __typename: 'ProductMedia', id: string, url: string, type: ProductMediaType, oembedData: any }> | null, product: { __typename: 'Product', id: string, name: string, defaultVariant: { __typename: 'ProductVariant', id: string } | null, media: Array<{ __typename: 'ProductMedia', id: string, alt: string, sortOrder: number | null, url: string, type: ProductMediaType, oembedData: any }> | null, thumbnail: { __typename: 'Image', url: string } | null, channelListings: Array<{ __typename: 'ProductChannelListing', id: string, publicationDate: any | null, isPublished: boolean, channel: { __typename: 'Channel', id: string, name: string, currencyCode: string } }> | null, variants: Array<{ __typename: 'ProductVariant', id: string, name: string, sku: string | null, media: Array<{ __typename: 'ProductMedia', id: string, url: string, type: ProductMediaType, oembedData: any }> | null }> | null }, channelListings: Array<{ __typename: 'ProductVariantChannelListing', channel: { __typename: 'Channel', id: string, name: string, currencyCode: string }, price: { __typename: 'Money', amount: number, currency: string } | null, costPrice: { __typename: 'Money', amount: number, currency: string } | null, preorderThreshold: { __typename: 'PreorderThreshold', quantity: number | null, soldUnits: number } | null }> | null, stocks: Array<{ __typename: 'Stock', id: string, quantity: number, quantityAllocated: number, warehouse: { __typename: 'Warehouse', id: string, name: string } }> | null, preorder: { __typename: 'PreorderData', globalThreshold: number | null, globalSoldUnits: number, endDate: any | null } | null, weight: { __typename: 'Weight', unit: WeightUnitsEnum, value: number } | null, metadata: Array<{ __typename: 'MetadataItem', key: string, value: string }>, privateMetadata: Array<{ __typename: 'MetadataItem', key: string, value: string }> } | null }; export type ProductVariantCreateDataQueryVariables = Exact<{ id: Scalars['ID']; diff --git a/src/hooks/useHandleFormSubmit.ts b/src/hooks/useHandleFormSubmit.ts index 48c95f411..6db20ab78 100644 --- a/src/hooks/useHandleFormSubmit.ts +++ b/src/hooks/useHandleFormSubmit.ts @@ -4,9 +4,9 @@ import { MessageContext } from "@saleor/components/messages"; import { SubmitPromise } from "@saleor/hooks/useForm"; import { useContext } from "react"; -interface UseHandleFormSubmitProps { +interface UseHandleFormSubmitProps { formId?: FormId; - onSubmit: (data: TData) => SubmitPromise | void; + onSubmit: (data: TData) => SubmitPromise | void; } function useHandleFormSubmit({ diff --git a/src/hooks/usePreventHistoryBack.ts b/src/hooks/usePreventHistoryBack.ts new file mode 100644 index 000000000..fa562fa92 --- /dev/null +++ b/src/hooks/usePreventHistoryBack.ts @@ -0,0 +1,35 @@ +import React, { useRef } from "react"; + +/** + * Hook that prevents hostory-back when use touchpad on Mac. + * Since overscroll-behavior sometimes may not work (eg. Safari 15 which is in use) + * We need to handle this in JS + * + * https://caniuse.com/css-overscroll-behavior + */ + +export const usePreventHistoryBack = (scroller: HTMLDivElement) => { + const offsetY = useRef(0); + + const wheelHandler = evt => { + const notVertival = Math.abs(evt.deltaX) - Math.abs(evt.deltaY) >= 0; + + if (evt.target.scrollLeft <= 0 && evt.deltaX <= 0 && notVertival) { + evt.preventDefault(); + } + + offsetY.current = window.scrollY; + }; + + React.useEffect(() => { + if (!scroller) { + return; + } + + scroller.addEventListener("wheel", wheelHandler, { passive: false }); + + return () => { + scroller.removeEventListener("wheel", wheelHandler); + }; + }, [scroller]); +}; diff --git a/src/index.html b/src/index.html index 3982f62db..50e8316bf 100644 --- a/src/index.html +++ b/src/index.html @@ -1,13 +1,17 @@ - - - + + + Saleor e-commerce
+
diff --git a/src/index.tsx b/src/index.tsx index 6f1c03b95..24fc5e020 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -126,6 +126,7 @@ const Routes: React.FC = () => { return ( <> + {DEMO_MODE && } {homePageLoaded ? ( diff --git a/src/misc.ts b/src/misc.ts index 5cd4102f8..bb1165d23 100644 --- a/src/misc.ts +++ b/src/misc.ts @@ -256,6 +256,15 @@ export const extractMutationErrors = async < return e as TErrors; }; +export const hasMutationErrors = (result: FetchResult): boolean => { + if (!result?.data) { + return false; + } + return Object.values(result.data).some( + ({ errors }: SaleorMutationResult) => errors.length > 0, + ); +}; + export const getMutationErrors = < T extends FetchResult, TData extends T["data"], diff --git a/src/orders/components/OrderRefundPage/form.tsx b/src/orders/components/OrderRefundPage/form.tsx index 1b008581d..96907cb8a 100644 --- a/src/orders/components/OrderRefundPage/form.tsx +++ b/src/orders/components/OrderRefundPage/form.tsx @@ -24,7 +24,7 @@ export enum OrderRefundAmountCalculationMode { } export interface OrderRefundData { - amount: number | string; + amount: string; type: OrderRefundType; refundShipmentCosts: boolean; amountCalculationMode: OrderRefundAmountCalculationMode; diff --git a/src/products/components/ProductStocks/ProductStocks.tsx b/src/products/components/ProductStocks/ProductStocks.tsx index a72622e22..7c5ad53f8 100644 --- a/src/products/components/ProductStocks/ProductStocks.tsx +++ b/src/products/components/ProductStocks/ProductStocks.tsx @@ -37,7 +37,6 @@ import React from "react"; import { FormattedMessage, useIntl } from "react-intl"; import { ProductCreateData } from "../ProductCreatePage"; -import { ProductUpdateSubmitData } from "../ProductUpdatePage/form"; import { ProductVariantCreateData } from "../ProductVariantCreatePage/form"; import { ProductVariantUpdateData } from "../ProductVariantPage/form"; import { messages } from "./messages"; @@ -68,7 +67,6 @@ export interface ProductStocksProps { formErrors: | FormErrors | FormErrors - | FormErrors | FormErrors; hasVariants: boolean; stocks: ProductStockInput[]; diff --git a/src/products/components/ProductUpdatePage/ProductChannelsListingsDialog.tsx b/src/products/components/ProductUpdatePage/ProductChannelsListingsDialog.tsx new file mode 100644 index 000000000..e0dd4529e --- /dev/null +++ b/src/products/components/ProductUpdatePage/ProductChannelsListingsDialog.tsx @@ -0,0 +1,72 @@ +import ChannelsAvailabilityDialog from "@saleor/components/ChannelsAvailabilityDialog"; +import { ChannelFragment } from "@saleor/graphql"; +import useStateFromProps from "@saleor/hooks/useStateFromProps"; +import { DialogProps } from "@saleor/types"; +import { arrayDiff } from "@saleor/utils/arrays"; +import { toggle } from "@saleor/utils/lists"; +import React from "react"; +import { useIntl } from "react-intl"; + +import { ProductUpdateData } from "./types"; + +export type ProductChannelsListingDialogSubmit = ( + update: Record<"added" | "removed", string[]>, +) => void; + +export interface ProductChannelsListingsDialogProps extends DialogProps { + channels: ChannelFragment[]; + data: Pick; + onConfirm: ProductChannelsListingDialogSubmit; +} + +const ProductChannelsListingsDialog: React.FC = ({ + channels, + data, + open, + onClose, + onConfirm, +}) => { + const intl = useIntl(); + + const [selected, setSelected] = useStateFromProps( + data.channels.updateChannels.map(listing => listing.channelId), + ); + + const handleConfirm = () => { + onConfirm( + arrayDiff( + data.channels.updateChannels.map(({ channelId }) => channelId), + selected, + ), + ); + onClose(); + }; + + const handleToggleAll = () => + selected.length !== channels.length + ? setSelected(channels.map(({ id }) => id)) + : setSelected([]); + + return ( + selected.includes(id)} + channels={channels} + onChange={({ id }) => + setSelected(toggle(id, selected, (a, b) => a === b)) + } + onClose={onClose} + open={open} + title={intl.formatMessage({ + id: "Eau5AV", + defaultMessage: "Manage Products Channel Availability", + })} + confirmButtonState="default" + selected={selected.length} + onConfirm={handleConfirm} + /> + ); +}; + +ProductChannelsListingsDialog.displayName = "ProductChannelsListingsDialog"; +export default ProductChannelsListingsDialog; diff --git a/src/products/components/ProductUpdatePage/ProductUpdatePage.test.tsx b/src/products/components/ProductUpdatePage/ProductUpdatePage.test.tsx index 8956cd83b..cf1fcdd31 100644 --- a/src/products/components/ProductUpdatePage/ProductUpdatePage.test.tsx +++ b/src/products/components/ProductUpdatePage/ProductUpdatePage.test.tsx @@ -1,8 +1,7 @@ import placeholderImage from "@assets/images/placeholder255x255.png"; import { channelsList } from "@saleor/channels/fixtures"; -import { createChannelsData } from "@saleor/channels/utils"; import { collections } from "@saleor/collections/fixtures"; -import { fetchMoreProps, limits, listActionsProps } from "@saleor/fixtures"; +import { fetchMoreProps, limits } from "@saleor/fixtures"; import { product as productFixture } from "@saleor/products/fixtures"; import { taxTypes } from "@saleor/storybook/stories/taxes/fixtures"; import { warehouseList } from "@saleor/warehouses/fixtures"; @@ -13,7 +12,6 @@ import React from "react"; import ProductUpdatePage, { ProductUpdatePageProps } from "./ProductUpdatePage"; const product = productFixture(placeholderImage); -const channels = createChannelsData(channelsList); import * as _useNavigator from "@saleor/hooks/useNavigator"; import Adapter from "enzyme-adapter-react-16"; @@ -26,6 +24,7 @@ const onSubmit = jest.fn(); const useNavigator = jest.spyOn(_useNavigator, "default"); jest.mock("@saleor/components/RichTextEditor/RichTextEditor"); jest.mock("@saleor/utils/richText/useRichText"); +jest.mock("@glideapps/glide-data-grid"); (global as any).document.createRange = () => ({ // eslint-disable-next-line @@ -39,18 +38,13 @@ jest.mock("@saleor/utils/richText/useRichText"); }); const props: ProductUpdatePageProps = { - ...listActionsProps, + channels: channelsList, + variantListErrors: [], productId: "123", - allChannelsCount: 5, categories: [product.category], - channelsData: [], - channelsWithVariantsData: {}, isSimpleProduct: false, - setChannelsData: () => undefined, channelsErrors: [], collections, - currentChannels: channels, - defaultWeightUnit: "kg", disabled: false, errors: [], fetchCategories: () => undefined, @@ -63,25 +57,21 @@ const props: ProductUpdatePageProps = { header: product.name, media: product.media, limits, + refetch: () => undefined, + onAttributeValuesSearch: () => Promise.resolve([]), onAssignReferencesClick: () => undefined, - onChannelsChange: () => undefined, onCloseDialog: () => undefined, onDelete: () => undefined, onImageDelete: () => undefined, onImageUpload: () => undefined, onMediaUrlUpload: () => undefined, - onSetDefaultVariant: () => undefined, onSubmit, - onVariantReorder: () => undefined, - onVariantEndPreorderDialogOpen: () => undefined, - onWarehouseConfigure: () => undefined, - openChannelsModal: () => undefined, + onVariantShow: () => undefined, placeholderImage, product, referencePages: [], referenceProducts: [], saveButtonBarState: "default", - selectedChannelId: "123", taxTypes, variants: product.variants, warehouses: warehouseList, @@ -96,6 +86,7 @@ const selectors = { describe("Product details page", () => { useNavigator.mockImplementation(); + // DataEditor.mockImplementation(); it("can select empty option on attribute", async () => { const component = mount( diff --git a/src/products/components/ProductUpdatePage/ProductUpdatePage.tsx b/src/products/components/ProductUpdatePage/ProductUpdatePage.tsx index 19e1a5892..73062b9e2 100644 --- a/src/products/components/ProductUpdatePage/ProductUpdatePage.tsx +++ b/src/products/components/ProductUpdatePage/ProductUpdatePage.tsx @@ -1,4 +1,3 @@ -import { OutputData } from "@editorjs/editorjs"; import { extensionMountPoints, mapToMenuItemsForProductDetails, @@ -21,10 +20,13 @@ import Metadata from "@saleor/components/Metadata/Metadata"; import PageHeader from "@saleor/components/PageHeader"; import Savebar from "@saleor/components/Savebar"; import SeoForm from "@saleor/components/SeoForm"; +import { Choice } from "@saleor/components/SingleSelectField"; import { + ChannelFragment, PermissionEnum, ProductChannelListingErrorFragment, ProductDetailsVariantFragment, + ProductErrorFragment, ProductErrorWithAttributesFragment, ProductFragment, RefreshLimitsQuery, @@ -37,51 +39,39 @@ import { WarehouseFragment, } from "@saleor/graphql"; import { SubmitPromise } from "@saleor/hooks/useForm"; -import { FormsetData } from "@saleor/hooks/useFormset"; import useNavigator from "@saleor/hooks/useNavigator"; import useStateFromProps from "@saleor/hooks/useStateFromProps"; import { sectionNames } from "@saleor/intl"; import { ConfirmButtonTransitionState } from "@saleor/macaw-ui"; import { maybe } from "@saleor/misc"; import ProductExternalMediaDialog from "@saleor/products/components/ProductExternalMediaDialog"; -import ProductVariantPrice from "@saleor/products/components/ProductVariantPrice"; import { productImageUrl, productListUrl } from "@saleor/products/urls"; -import { ChannelsWithVariantsData } from "@saleor/products/views/ProductUpdate/types"; -import { - ChannelProps, - FetchMoreProps, - ListActions, - RelayToFlat, - ReorderAction, -} from "@saleor/types"; +import { ProductVariantListError } from "@saleor/products/views/ProductUpdate/handlers/errors"; +import { UseProductUpdateHandlerError } from "@saleor/products/views/ProductUpdate/handlers/useProductUpdateHandler"; +import { FetchMoreProps, RelayToFlat } from "@saleor/types"; import React from "react"; import { useIntl } from "react-intl"; -import ChannelsWithVariantsAvailabilityCard from "../../../channels/ChannelsWithVariantsAvailabilityCard/ChannelsWithVariantsAvailabilityCard"; -import { getChoices, ProductUpdatePageFormData } from "../../utils/data"; +import { getChoices } from "../../utils/data"; import ProductDetailsForm from "../ProductDetailsForm"; import ProductMedia from "../ProductMedia"; import ProductOrganization from "../ProductOrganization"; -import ProductShipping from "../ProductShipping/ProductShipping"; -import ProductStocks, { ProductStockInput } from "../ProductStocks"; import ProductTaxes from "../ProductTaxes"; import ProductVariants from "../ProductVariants"; -import ProductUpdateForm, { +import ProductUpdateForm from "./form"; +import ProductChannelsListingsDialog from "./ProductChannelsListingsDialog"; +import { ProductUpdateData, ProductUpdateHandlers, -} from "./form"; + ProductUpdateSubmitData, +} from "./types"; -export interface ProductUpdatePageProps extends ListActions, ChannelProps { +export interface ProductUpdatePageProps { + channels: ChannelFragment[]; productId: string; - channelsWithVariantsData: ChannelsWithVariantsData; - setChannelsData: (data: ChannelData[]) => void; - onChannelsChange: (data: ChannelData[]) => void; - channelsData: ChannelData[]; - currentChannels: ChannelData[]; - allChannelsCount: number; channelsErrors: ProductChannelListingErrorFragment[]; - defaultWeightUnit: string; - errors: ProductErrorWithAttributesFragment[]; + variantListErrors: ProductVariantListError[]; + errors: UseProductUpdateHandlerError[]; placeholderImage: string; collections: RelayToFlat; categories: RelayToFlat; @@ -112,39 +102,31 @@ export interface ProductUpdatePageProps extends ListActions, ChannelProps { fetchReferencePages?: (data: string) => void; fetchReferenceProducts?: (data: string) => void; fetchAttributeValues: (query: string, attributeId: string) => void; + refetch: () => Promise; + onAttributeValuesSearch: ( + id: string, + query: string, + ) => Promise>>; onAssignReferencesClick: (attribute: AttributeInput) => void; onCloseDialog: () => void; - onVariantReorder: ReorderAction; - onVariantEndPreorderDialogOpen: () => void; onImageDelete: (id: string) => () => void; - onSubmit: (data: ProductUpdatePageSubmitData) => SubmitPromise; - openChannelsModal: () => void; + onSubmit: (data: ProductUpdateSubmitData) => SubmitPromise; + onVariantShow: (id: string) => void; onAttributeSelectBlur: () => void; onDelete(); onImageReorder?(event: { oldIndex: number; newIndex: number }); onImageUpload(file: File); onMediaUrlUpload(mediaUrl: string); onSeoClick?(); - onSetDefaultVariant(variant: ProductDetailsVariantFragment); - onWarehouseConfigure(); -} - -export interface ProductUpdatePageSubmitData extends ProductUpdatePageFormData { - addStocks: ProductStockInput[]; - attributes: AttributeInput[]; - attributesWithNewFileValue: FormsetData; - collections: string[]; - description: OutputData; - removeStocks: string[]; - updateStocks: ProductStockInput[]; } export const ProductUpdatePage: React.FC = ({ productId, - defaultWeightUnit, disabled, categories: categoryChoiceList, + channels, channelsErrors, + variantListErrors, collections: collectionChoiceList, attributeValues, isSimpleProduct, @@ -161,33 +143,20 @@ export const ProductUpdatePage: React.FC = ({ saveButtonBarState, variants, warehouses, - setChannelsData, taxTypes, referencePages = [], referenceProducts = [], onDelete, - allChannelsCount, - currentChannels, onImageDelete, onImageReorder, onImageUpload, onMediaUrlUpload, - openChannelsModal, + onVariantShow, onSeoClick, onSubmit, - channelsData, - onSetDefaultVariant, - onVariantReorder, - onVariantEndPreorderDialogOpen, - onWarehouseConfigure, - isChecked, isMediaUrlModalVisible, - selected, - selectedChannelId, - toggle, - toggleAll, - toolbar, assignReferencesAttributeId, + onAttributeValuesSearch, onAssignReferencesClick, fetchReferencePages, fetchMoreReferencePages, @@ -195,13 +164,13 @@ export const ProductUpdatePage: React.FC = ({ fetchMoreReferenceProducts, fetchAttributeValues, fetchMoreAttributeValues, + refetch, onCloseDialog, - channelsWithVariantsData, - onChannelsChange, onAttributeSelectBlur, }) => { const intl = useIntl(); const navigate = useNavigator(); + const [channelPickerOpen, setChannelPickerOpen] = React.useState(false); const [selectedCategory, setSelectedCategory] = useStateFromProps( product?.category?.name || "", @@ -250,6 +219,22 @@ export const ProductUpdatePage: React.FC = ({ extensionMountPoints.PRODUCT_DETAILS, ); + const productErrors = React.useMemo( + () => + errors.filter( + error => error.__typename === "ProductError", + ) as ProductErrorWithAttributesFragment[], + [errors], + ); + + const productOrganizationErrors = React.useMemo( + () => + [...errors, ...channelsErrors].filter(err => + ["ProductChannelListingError", "ProductError"].includes(err.__typename), + ) as Array, + [errors, channelsErrors], + ); + const extensionMenuItems = mapToMenuItemsForProductDetails( PRODUCT_DETAILS_MORE_ACTIONS, productId, @@ -258,19 +243,14 @@ export const ProductUpdatePage: React.FC = ({ return ( = ({ fetchMoreReferenceProducts={fetchMoreReferenceProducts} assignReferencesAttributeId={assignReferencesAttributeId} disabled={disabled} + refetch={refetch} > {({ change, data, - formErrors, handlers, submit, isSaveDisabled, @@ -308,12 +288,25 @@ export const ProductUpdatePage: React.FC = ({ }), }, errors: channelsErrors, - allChannelsCount, + allChannelsCount: channels?.length, disabled, onChange: handlers.changeChannels, - openModal: openChannelsModal, + openModal: () => setChannelPickerOpen(true), }; + const listings = data.channels.updateChannels.map( + listing => { + const channel = channels?.find(ac => ac.id === listing.channelId); + return { + id: listing.channelId, + ...channel, + ...listing, + availableForPurchase: listing.availableForPurchaseDate, + currency: channel.currencyCode, + }; + }, + ); + return ( <> @@ -333,7 +326,7 @@ export const ProductUpdatePage: React.FC = ({ @@ -353,7 +346,7 @@ export const ProductUpdatePage: React.FC = ({ = ({ /> )} - {isSimpleProduct && ( - <> - - - - )} - {hasVariants ? ( - - ) : ( - <> - - - onVariantEndPreorderDialogOpen() - : null - } - data={data} - disabled={disabled} - hasVariants={false} - errors={errors} - formErrors={formErrors} - stocks={data.stocks} - warehouses={warehouses} - onChange={handlers.changeStock} - onFormDataChange={change} - onChangePreorderEndDate={handlers.changePreorderEndDate} - onWarehouseStockAdd={handlers.addStock} - onWarehouseStockDelete={handlers.deleteStock} - onWarehouseConfigure={onWarehouseConfigure} - /> - - )} + = ({ collectionsInputDisplayValue={selectedCollections} data={data} disabled={disabled} - errors={[...errors, ...channelsErrors]} + errors={productOrganizationErrors} fetchCategories={fetchCategories} fetchCollections={fetchCollections} fetchMoreCategories={fetchMoreCategories} @@ -472,39 +413,10 @@ export const ProductUpdatePage: React.FC = ({ onCollectionChange={handlers.selectCollection} /> - {isSimpleProduct ? ( - - ) : product?.variants.length === 0 ? ( - - ) : ( - - )} + = ({ open={mediaUrlModalStatus} onSubmit={onMediaUrlUpload} /> + setChannelPickerOpen(false)} + open={channelPickerOpen} + onConfirm={handlers.updateChannelList} + /> ); diff --git a/src/products/components/ProductUpdatePage/form.tsx b/src/products/components/ProductUpdatePage/form.tsx index 8bba16271..de8c69985 100644 --- a/src/products/components/ProductUpdatePage/form.tsx +++ b/src/products/components/ProductUpdatePage/form.tsx @@ -1,10 +1,8 @@ -import { OutputData } from "@editorjs/editorjs"; import { getAttributesDisplayData, getRichTextAttributesFromMap, getRichTextDataFromAttributes, mergeAttributes, - RichTextProps, } from "@saleor/attributes/utils/data"; import { createAttributeChangeHandler, @@ -16,53 +14,20 @@ import { createFetchReferencesHandler, } from "@saleor/attributes/utils/handlers"; import { - ChannelData, - ChannelPreorderArgs, - ChannelPriceArgs, -} from "@saleor/channels/utils"; -import { AttributeInput } from "@saleor/components/Attributes"; + DatagridChangeOpts, + DatagridChangeStateContext, + useDatagridChangeState, +} from "@saleor/components/Datagrid/useDatagridChange"; import { useExitFormDialog } from "@saleor/components/Form/useExitFormDialog"; -import { MetadataFormData } from "@saleor/components/Metadata"; -import { MultiAutocompleteChoiceType } from "@saleor/components/MultiAutocompleteSelectField"; -import { SingleAutocompleteChoiceType } from "@saleor/components/SingleAutocompleteSelectField"; -import { - ProductFragment, - SearchPagesQuery, - SearchProductsQuery, - SearchWarehousesQuery, -} from "@saleor/graphql"; -import useForm, { - CommonUseFormResultWithHandlers, - FormChange, - FormErrors, - SubmitPromise, -} from "@saleor/hooks/useForm"; -import useFormset, { - FormsetAtomicData, - FormsetChange, - FormsetData, -} from "@saleor/hooks/useFormset"; +import { ProductFragment } from "@saleor/graphql"; +import useForm from "@saleor/hooks/useForm"; +import useFormset from "@saleor/hooks/useFormset"; import useHandleFormSubmit from "@saleor/hooks/useHandleFormSubmit"; -import { errorMessages } from "@saleor/intl"; import { getAttributeInputFromProduct, getProductUpdatePageFormData, - getStockInputFromProduct, } from "@saleor/products/utils/data"; -import { - createChannelsChangeHandler, - createChannelsPreorderChangeHandler, - createChannelsPriceChangeHandler, - createPreorderEndDateChangeHandler, -} from "@saleor/products/utils/handlers"; -import { - validateCostPrice, - validatePrice, -} from "@saleor/products/utils/validation"; import { PRODUCT_UPDATE_FORM_ID } from "@saleor/products/views/ProductUpdate/consts"; -import { ChannelsWithVariantsData } from "@saleor/products/views/ProductUpdate/types"; -import { FetchMoreProps, RelayToFlat, ReorderEvent } from "@saleor/types"; -import { arrayDiff } from "@saleor/utils/arrays"; import createMultiAutocompleteSelectHandler from "@saleor/utils/handlers/multiAutocompleteSelectChangeHandler"; import createSingleAutocompleteSelectHandler from "@saleor/utils/handlers/singleAutocompleteSelectChangeHandler"; import getMetadata from "@saleor/utils/metadata/getMetadata"; @@ -70,190 +35,28 @@ import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTr import { RichTextContext } from "@saleor/utils/richText/context"; import { useMultipleRichText } from "@saleor/utils/richText/useMultipleRichText"; import useRichText from "@saleor/utils/richText/useRichText"; -import React, { useEffect, useMemo } from "react"; -import { useIntl } from "react-intl"; +import React, { useCallback, useEffect, useMemo, useRef } from "react"; -import { ProductStockFormsetData, ProductStockInput } from "../ProductStocks"; - -export interface ProductUpdateFormData extends MetadataFormData { - category: string | null; - changeTaxCode: boolean; - channelsData: ChannelData[]; - channelsWithVariants: ChannelsWithVariantsData; - channelListings: ChannelData[]; - chargeTaxes: boolean; - collections: string[]; - isAvailable: boolean; - name: string; - rating: number; - slug: string; - seoDescription: string; - seoTitle: string; - sku: string; - taxCode: string; - trackInventory: boolean; - isPreorder: boolean; - globalThreshold: string; - globalSoldUnits: number; - hasPreorderEndDate: boolean; - preorderEndDateTime?: string; - weight: string; -} -export interface FileAttributeInputData { - attributeId: string; - file: File; -} -export type FileAttributeInput = FormsetAtomicData< - FileAttributeInputData, - string[] ->; - -export interface FileAttributesSubmitData { - fileAttributes: FileAttributeInput[]; -} -export interface ProductUpdateData extends ProductUpdateFormData { - attributes: AttributeInput[]; - description: OutputData; - stocks: ProductStockInput[]; -} -export interface ProductUpdateSubmitData extends ProductUpdateFormData { - attributes: AttributeInput[]; - attributesWithNewFileValue: FormsetData; - collections: string[]; - description: OutputData; - addStocks: ProductStockInput[]; - updateStocks: ProductStockInput[]; - removeStocks: string[]; -} - -export interface ProductUpdateHandlers - extends Record< - | "changeMetadata" - | "selectCategory" - | "selectCollection" - | "selectTaxRate", - FormChange - >, - Record< - "changeStock" | "selectAttribute" | "selectAttributeMultiple", - FormsetChange - >, - Record<"changeChannelPrice", (id: string, data: ChannelPriceArgs) => void>, - Record< - "changeChannelPreorder", - (id: string, data: ChannelPreorderArgs) => void - >, - Record< - "changeChannels", - ( - id: string, - data: Omit, - ) => void - >, - Record<"selectAttributeReference", FormsetChange>, - Record<"selectAttributeFile", FormsetChange>, - Record<"reorderAttributeValue", FormsetChange>, - Record<"addStock" | "deleteStock", (id: string) => void> { - changePreorderEndDate: FormChange; - fetchReferences: (value: string) => void; - fetchMoreReferences: FetchMoreProps; -} - -export interface UseProductUpdateFormOutput - extends CommonUseFormResultWithHandlers< - ProductUpdateData, - ProductUpdateHandlers - >, - RichTextProps { - formErrors: FormErrors; -} - -export type UseProductUpdateFormRenderProps = Omit< +import { useProductChannelListingsForm } from "./formChannels"; +import { + ProductUpdateData, + ProductUpdateFormProps, + ProductUpdateSubmitData, + SubmitResult, + UseProductUpdateFormOpts, UseProductUpdateFormOutput, - "richText" ->; - -export interface UseProductUpdateFormOpts - extends Record< - "categories" | "collections" | "taxTypes", - SingleAutocompleteChoiceType[] - > { - setSelectedCategory: React.Dispatch>; - setSelectedCollections: React.Dispatch< - React.SetStateAction - >; - setSelectedTaxType: React.Dispatch>; - selectedCollections: MultiAutocompleteChoiceType[]; - warehouses: RelayToFlat; - channelsData: ChannelData[]; - hasVariants: boolean; - currentChannels: ChannelData[]; - setChannels: (data: ChannelData[]) => void; - setChannelsData: (data: ChannelData[]) => void; - referencePages: RelayToFlat; - referenceProducts: RelayToFlat; - fetchReferencePages?: (data: string) => void; - fetchMoreReferencePages?: FetchMoreProps; - fetchReferenceProducts?: (data: string) => void; - fetchMoreReferenceProducts?: FetchMoreProps; - assignReferencesAttributeId?: string; - channelsWithVariants: ChannelsWithVariantsData; - isSimpleProduct: boolean; -} - -export interface ProductUpdateFormProps extends UseProductUpdateFormOpts { - children: (props: UseProductUpdateFormRenderProps) => React.ReactNode; - product: ProductFragment; - onSubmit: (data: ProductUpdateSubmitData) => SubmitPromise; - disabled: boolean; -} - -const getStocksData = ( - product: ProductFragment, - stocks: FormsetData, -) => { - if (product?.productType?.hasVariants) { - return { addStocks: [], removeStocks: [], updateStocks: [] }; - } - - const dataStocks = stocks.map(stock => stock.id); - const variantStocks = - product?.variants[0]?.stocks.map(stock => stock.warehouse.id) || []; - const stockDiff = arrayDiff(variantStocks, dataStocks); - - return { - addStocks: stocks.filter(stock => - stockDiff.added.some(addedStock => addedStock === stock.id), - ), - removeStocks: stockDiff.removed, - updateStocks: stocks.filter( - stock => !stockDiff.added.some(addedStock => addedStock === stock.id), - ), - }; -}; +} from "./types"; function useProductUpdateForm( product: ProductFragment, - onSubmit: (data: ProductUpdateSubmitData) => SubmitPromise, + onSubmit: (data: ProductUpdateSubmitData) => SubmitResult, disabled: boolean, + refetch: () => Promise, opts: UseProductUpdateFormOpts, ): UseProductUpdateFormOutput { - const intl = useIntl(); const initial = useMemo( - () => - getProductUpdatePageFormData( - product, - product?.variants, - opts.currentChannels, - opts.channelsData, - opts.channelsWithVariants, - ), - [ - product, - opts.currentChannels, - opts.channelsData, - opts.channelsWithVariants, - ], + () => getProductUpdatePageFormData(product, product?.variants), + [product], ); const form = useForm(initial, undefined, { @@ -269,6 +72,17 @@ function useProductUpdateForm( setIsSubmitDisabled, } = form; + const datagrid = useDatagridChangeState(); + const variants = useRef({ + added: [], + removed: [], + updates: [], + }); + const handleVariantChange = React.useCallback((data: DatagridChangeOpts) => { + variants.current = data; + triggerChange(); + }, []); + const attributes = useFormset(getAttributeInputFromProduct(product)); const { getters: attributeRichTextGetters, @@ -278,7 +92,6 @@ function useProductUpdateForm( triggerChange, }); const attributesWithNewFileValue = useFormset([]); - const stocks = useFormset(getStockInputFromProduct(product)); const richText = useRichText({ initial: product?.description, loading: !product, @@ -295,6 +108,13 @@ function useProductUpdateForm( makeChangeHandler: makeMetadataChangeHandler, } = useMetadataChangeTrigger(); + const { + channels, + handleChannelChange, + handleChannelListUpdate, + touched: touchedChannels, + } = useProductChannelListingsForm(product, triggerChange); + const handleCollectionSelect = createMultiAutocompleteSelectHandler( event => toggleValue(event), opts.setSelectedCollections, @@ -343,25 +163,6 @@ function useProductUpdateForm( attributes.data, triggerChange, ); - const handleStockChange: FormsetChange = (id, value) => { - triggerChange(); - stocks.change(id, value); - }; - const handleStockAdd = (id: string) => { - triggerChange(); - stocks.add({ - data: { - quantityAllocated: 0, - }, - id, - label: opts.warehouses.find(warehouse => warehouse.id === id).name, - value: "0", - }); - }; - const handleStockDelete = (id: string) => { - triggerChange(); - stocks.remove(id); - }; const handleTaxTypeSelect = createSingleAutocompleteSelectHandler( handleChange, opts.setSelectedTaxType, @@ -369,47 +170,20 @@ function useProductUpdateForm( ); const changeMetadata = makeMetadataChangeHandler(handleChange); - const handleChannelsChange = createChannelsChangeHandler( - opts.isSimpleProduct ? opts.currentChannels : opts.channelsData, - opts.isSimpleProduct ? opts.setChannels : opts.setChannelsData, - triggerChange, - ); - - const handleChannelPreorderChange = createChannelsPreorderChangeHandler( - opts.isSimpleProduct ? opts.currentChannels : opts.channelsData, - opts.isSimpleProduct ? opts.setChannels : opts.setChannelsData, - triggerChange, - ); - - const handleChannelPriceChange = createChannelsPriceChangeHandler( - opts.isSimpleProduct ? opts.currentChannels : opts.channelsData, - opts.isSimpleProduct ? opts.setChannels : opts.setChannelsData, - triggerChange, - ); - - const handlePreorderEndDateChange = createPreorderEndDateChangeHandler( - form, - triggerChange, - intl.formatMessage(errorMessages.preorderEndDateInFutureErrorText), - ); - const data: ProductUpdateData = { ...formData, - channelListings: opts.currentChannels, - channelsData: opts.channelsData, attributes: getAttributesDisplayData( attributes.data, attributesWithNewFileValue.data, opts.referencePages, opts.referenceProducts, ), + channels, description: null, - stocks: stocks.data, }; const getSubmitData = async (): Promise => ({ ...data, - ...getStocksData(product, stocks.data), ...getMetadata(data, isMetadataModified, isPrivateMetadataModified), attributes: mergeAttributes( attributes.data, @@ -419,7 +193,14 @@ function useProductUpdateForm( ), ), attributesWithNewFileValue: attributesWithNewFileValue.data, + channels: { + ...channels, + updateChannels: channels.updateChannels.filter(listing => + touchedChannels.current.includes(listing.channelId), + ), + }, description: await richText.getValue(), + variants: variants.current, }); const handleSubmit = async (data: ProductUpdateSubmitData) => { @@ -437,7 +218,39 @@ function useProductUpdateForm( onSubmit: handleSubmit, }); - const submit = async () => handleFormSubmit(await getSubmitData()); + const submit = useCallback(async () => { + const result = await handleFormSubmit(await getSubmitData()); + await refetch(); + + datagrid.setAdded(prevAdded => + prevAdded.filter((_, index) => + result.some( + error => + error.__typename === "DatagridError" && + error.type === "create" && + error.index === index, + ), + ), + ); + datagrid.changes.current = datagrid.changes.current.filter(change => + datagrid.added.includes(change.row) + ? result.some( + error => + error.__typename === "DatagridError" && + error.type === "create" && + error.index === datagrid.added.findIndex(r => r === change.row), + ) + : result.some( + error => + error.__typename === "DatagridError" && + error.type !== "create" && + error.variantId === product.variants[change.row].id, + ), + ); + datagrid.setRemoved([]); + + return result; + }, [datagrid, handleFormSubmit, getSubmitData]); useEffect(() => setExitDialogSubmitRef(submit), [submit]); @@ -454,18 +267,6 @@ function useProductUpdateForm( return false; } - if (opts.hasVariants) { - return true; - } - - const hasInvalidChannelListingPrices = data.channelListings.some( - channel => - validatePrice(channel.price) || validateCostPrice(channel.costPrice), - ); - - if (hasInvalidChannelListingPrices) { - return false; - } return true; }; @@ -479,16 +280,12 @@ function useProductUpdateForm( return { change: handleChange, data, + datagrid, formErrors: form.errors, handlers: { - addStock: handleStockAdd, - changeChannelPrice: handleChannelPriceChange, - changeChannelPreorder: handleChannelPreorderChange, - changeChannels: handleChannelsChange, + changeChannels: handleChannelChange, changeMetadata, - changeStock: handleStockChange, - changePreorderEndDate: handlePreorderEndDateChange, - deleteStock: handleStockDelete, + changeVariants: handleVariantChange, fetchMoreReferences: handleFetchMoreReferences, fetchReferences: handleFetchReferences, reorderAttributeValue: handleAttributeValueReorder, @@ -499,6 +296,7 @@ function useProductUpdateForm( selectCategory: handleCategorySelect, selectCollection: handleCollectionSelect, selectTaxRate: handleTaxTypeSelect, + updateChannelList: handleChannelListUpdate, }, submit, isSaveDisabled, @@ -511,21 +309,25 @@ const ProductUpdateForm: React.FC = ({ children, product, onSubmit, + refetch, disabled, ...rest }) => { - const { richText, ...props } = useProductUpdateForm( + const { datagrid, richText, ...props } = useProductUpdateForm( product, onSubmit, disabled, + refetch, rest, ); return (
- - {children(props)} - + + + {children(props)} + +
); }; diff --git a/src/products/components/ProductUpdatePage/formChannels.ts b/src/products/components/ProductUpdatePage/formChannels.ts new file mode 100644 index 000000000..4de7e53ba --- /dev/null +++ b/src/products/components/ProductUpdatePage/formChannels.ts @@ -0,0 +1,86 @@ +import { ChannelOpts } from "@saleor/components/ChannelsAvailabilityCard/types"; +import { + ProductChannelListingAddInput, + ProductChannelListingUpdateInput, + ProductFragment, +} from "@saleor/graphql"; +import useStateFromProps from "@saleor/hooks/useStateFromProps"; +import uniq from "lodash/uniq"; +import uniqBy from "lodash/uniqBy"; +import { useCallback, useRef } from "react"; + +import { ProductChannelsListingDialogSubmit } from "./ProductChannelsListingsDialog"; + +const emptyListing: Omit = { + availableForPurchaseDate: null, + isAvailableForPurchase: false, + isPublished: false, + publicationDate: null, + visibleInListings: false, +}; + +export function useProductChannelListingsForm( + product: Pick, + triggerChange: () => void, +) { + const [channels, setChannels] = useStateFromProps< + ProductChannelListingUpdateInput + >({ + removeChannels: [], + updateChannels: + product?.channelListings.map(listing => ({ + channelId: listing.channel.id, + availableForPurchaseDate: listing.availableForPurchase, + ...listing, + })) ?? [], + }); + const touched = useRef([]); + + const touch = (id: string) => { + touched.current = uniq([...touched.current, id]); + }; + + const handleChannelChange = useCallback((id: string, data: ChannelOpts) => { + setChannels(prevData => ({ + ...prevData, + updateChannels: prevData.updateChannels.map(prevListing => + prevListing.channelId === id + ? { ...prevListing, ...data } + : prevListing, + ), + })); + triggerChange(); + touch(id); + }, []); + + const handleChannelListUpdate: ProductChannelsListingDialogSubmit = useCallback( + ({ added, removed }) => { + setChannels(prevData => ({ + ...prevData, + updateChannels: uniqBy( + [ + ...prevData.updateChannels, + ...added.map(id => ({ + channelId: id, + ...emptyListing, + })), + ], + "channelId", + ).filter(({ channelId }) => !removed.includes(channelId)), + removeChannels: uniq([...prevData.removeChannels, ...removed]).filter( + id => !added.includes(id), + ), + })); + triggerChange(); + added.forEach(id => touch(id)); + }, + [product], + ); + + return { + channels, + handleChannelChange, + handleChannelListUpdate, + touched, + }; +} diff --git a/src/products/components/ProductUpdatePage/types.ts b/src/products/components/ProductUpdatePage/types.ts new file mode 100644 index 000000000..cbe914ee1 --- /dev/null +++ b/src/products/components/ProductUpdatePage/types.ts @@ -0,0 +1,153 @@ +import { OutputData } from "@editorjs/editorjs"; +import { RichTextProps } from "@saleor/attributes/utils/data"; +import { AttributeInput } from "@saleor/components/Attributes"; +import { ChannelOpts } from "@saleor/components/ChannelsAvailabilityCard/types"; +import { + DatagridChangeOpts, + UseDatagridChangeState, +} from "@saleor/components/Datagrid/useDatagridChange"; +import { MetadataFormData } from "@saleor/components/Metadata"; +import { MultiAutocompleteChoiceType } from "@saleor/components/MultiAutocompleteSelectField"; +import { SingleAutocompleteChoiceType } from "@saleor/components/SingleAutocompleteSelectField"; +import { + MetadataErrorFragment, + ProductChannelListingUpdateInput, + ProductFragment, + SearchPagesQuery, + SearchProductsQuery, + SearchWarehousesQuery, +} from "@saleor/graphql"; +import { + CommonUseFormResultWithHandlers, + FormChange, + FormErrors, + SubmitPromise, +} from "@saleor/hooks/useForm"; +import { + FormsetAtomicData, + FormsetChange, + FormsetData, +} from "@saleor/hooks/useFormset"; +import { UseProductUpdateHandlerError } from "@saleor/products/views/ProductUpdate/handlers/useProductUpdateHandler"; +import { FetchMoreProps, RelayToFlat, ReorderEvent } from "@saleor/types"; + +import { ProductChannelsListingDialogSubmit } from "./ProductChannelsListingsDialog"; + +export interface ProductUpdateFormData extends MetadataFormData { + category: string | null; + changeTaxCode: boolean; + chargeTaxes: boolean; + collections: string[]; + isAvailable: boolean; + name: string; + rating: number; + slug: string; + seoDescription: string; + seoTitle: string; + sku: string; + taxCode: string; + trackInventory: boolean; + isPreorder: boolean; + globalThreshold: string; + globalSoldUnits: number; + hasPreorderEndDate: boolean; + preorderEndDateTime?: string; + weight: string; +} +export interface FileAttributeInputData { + attributeId: string; + file: File; +} +export type FileAttributeInput = FormsetAtomicData< + FileAttributeInputData, + string[] +>; + +export interface FileAttributesSubmitData { + fileAttributes: FileAttributeInput[]; +} +export interface ProductUpdateData extends ProductUpdateFormData { + attributes: AttributeInput[]; + channels: ProductChannelListingUpdateInput; + description: OutputData; +} +export interface ProductUpdateSubmitData extends ProductUpdateFormData { + attributes: AttributeInput[]; + attributesWithNewFileValue: FormsetData; + channels: ProductChannelListingUpdateInput; + collections: string[]; + description: OutputData; + variants: DatagridChangeOpts; +} + +export interface ProductUpdateHandlers + extends Record< + | "changeMetadata" + | "selectCategory" + | "selectCollection" + | "selectTaxRate", + FormChange + >, + Record< + "selectAttribute" | "selectAttributeMultiple", + FormsetChange + > { + changeChannels: (id: string, data: ChannelOpts) => void; + selectAttributeReference: FormsetChange; + selectAttributeFile: FormsetChange; + reorderAttributeValue: FormsetChange; + changeVariants: (data: DatagridChangeOpts) => void; + fetchReferences: (value: string) => void; + fetchMoreReferences: FetchMoreProps; + updateChannelList: ProductChannelsListingDialogSubmit; +} + +export interface UseProductUpdateFormOutput + extends CommonUseFormResultWithHandlers< + ProductUpdateData, + ProductUpdateHandlers + >, + RichTextProps { + datagrid: UseDatagridChangeState; + formErrors: FormErrors; +} + +export type UseProductUpdateFormRenderProps = Omit< + UseProductUpdateFormOutput, + "datagrid" | "richText" +>; + +export interface UseProductUpdateFormOpts + extends Record< + "categories" | "collections" | "taxTypes", + SingleAutocompleteChoiceType[] + > { + setSelectedCategory: React.Dispatch>; + setSelectedCollections: React.Dispatch< + React.SetStateAction + >; + setSelectedTaxType: React.Dispatch>; + selectedCollections: MultiAutocompleteChoiceType[]; + warehouses: RelayToFlat; + hasVariants: boolean; + referencePages: RelayToFlat; + referenceProducts: RelayToFlat; + fetchReferencePages?: (data: string) => void; + fetchMoreReferencePages?: FetchMoreProps; + fetchReferenceProducts?: (data: string) => void; + fetchMoreReferenceProducts?: FetchMoreProps; + assignReferencesAttributeId?: string; + isSimpleProduct: boolean; +} + +export type SubmitResult = SubmitPromise< + Array +>; + +export interface ProductUpdateFormProps extends UseProductUpdateFormOpts { + children: (props: UseProductUpdateFormRenderProps) => React.ReactNode; + product: ProductFragment; + onSubmit: (data: ProductUpdateSubmitData) => SubmitResult; + refetch: () => Promise; + disabled: boolean; +} diff --git a/src/products/components/ProductVariantChannels/ChannelsAvailabilityCard/CreateVariantTitle.tsx b/src/products/components/ProductVariantChannels/ChannelsAvailabilityCard/CreateVariantTitle.tsx index 429cb6e1a..0ad7efbd7 100644 --- a/src/products/components/ProductVariantChannels/ChannelsAvailabilityCard/CreateVariantTitle.tsx +++ b/src/products/components/ProductVariantChannels/ChannelsAvailabilityCard/CreateVariantTitle.tsx @@ -18,7 +18,12 @@ export const CreateVariantTitle: React.FC = ({ + } diff --git a/src/products/components/ProductVariants/ProductVariants.tsx b/src/products/components/ProductVariants/ProductVariants.tsx index 0935d1921..6e97b85ee 100644 --- a/src/products/components/ProductVariants/ProductVariants.tsx +++ b/src/products/components/ProductVariants/ProductVariants.tsx @@ -1,469 +1,135 @@ +import { Item } from "@glideapps/glide-data-grid"; +import { ChannelData } from "@saleor/channels/utils"; +import Datagrid, { + GetCellContentOpts, +} from "@saleor/components/Datagrid/Datagrid"; +import { DatagridChangeOpts } from "@saleor/components/Datagrid/useDatagridChange"; +import { Choice } from "@saleor/components/SingleSelectField"; import { - Card, - CardContent, - Hidden, - TableCell, - Typography, -} from "@material-ui/core"; -import { fade } from "@material-ui/core/styles/colorManipulator"; -import { Button } from "@saleor/components/Button"; -import CardTitle from "@saleor/components/CardTitle"; -import Checkbox from "@saleor/components/Checkbox"; -import LimitReachedAlert from "@saleor/components/LimitReachedAlert"; -import LinkChoice from "@saleor/components/LinkChoice"; -import Money from "@saleor/components/Money"; -import ResponsiveTable from "@saleor/components/ResponsiveTable"; -import { SingleAutocompleteChoiceType } from "@saleor/components/SingleAutocompleteSelectField"; -import Skeleton from "@saleor/components/Skeleton"; -import { - SortableTableBody, - SortableTableRow, -} from "@saleor/components/SortableTable"; -import { TableButtonWrapper } from "@saleor/components/TableButtonWrapper/TableButtonWrapper"; -import TableHead from "@saleor/components/TableHead"; -import { + AttributeInputTypeEnum, ProductDetailsVariantFragment, ProductFragment, RefreshLimitsQuery, + WarehouseFragment, } from "@saleor/graphql"; -import { makeStyles } from "@saleor/macaw-ui"; -import { - productVariantAddUrl, - productVariantEditUrl, -} from "@saleor/products/urls"; -import { isLimitReached } from "@saleor/utils/limits"; +import { buttonMessages } from "@saleor/intl"; +import { Button } from "@saleor/macaw-ui"; +import { ProductVariantListError } from "@saleor/products/views/ProductUpdate/handlers/errors"; +// import { isLimitReached } from "@saleor/utils/limits"; import React from "react"; -import { FormattedMessage, IntlShape, useIntl } from "react-intl"; +import { FormattedMessage, useIntl } from "react-intl"; -import { maybe, renderCollection } from "../../../misc"; -import { ChannelProps, ListActions, ReorderAction } from "../../../types"; -import ProductVariantSetDefault from "../ProductVariantSetDefault"; +import messages from "./messages"; +import { getColumnData, getData, getError } from "./utils"; -type Warehouse = ProductDetailsVariantFragment[][0]["stocks"][0]["warehouse"]; - -function getWarehouseChoices( - variants: ProductDetailsVariantFragment[], - intl: IntlShape, -): SingleAutocompleteChoiceType[] { - return [ - { - label: intl.formatMessage({ - id: "JtZ71e", - defaultMessage: "All Warehouses", - description: "filtering option", - }), - value: null, - }, - ...variants - .reduce( - (warehouses, variant) => [ - ...warehouses, - ...variant.stocks.reduce((variantStocks, stock) => { - if (!!warehouses.find(w => w.id === stock.warehouse.id)) { - return variantStocks; - } - - return [...variantStocks, stock.warehouse]; - }, []), - ], - [], - ) - .map(w => ({ - label: w.name, - value: w.id, - })), - ]; -} - -const useStyles = makeStyles( - theme => ({ - [theme.breakpoints.up("lg")]: { - colActions: { - width: 80, - }, - colInventory: { - width: 200, - }, - colName: {}, - colPrice: { - width: 135, - }, - colSku: { - width: 200, - }, - }, - alert: { - margin: theme.spacing(3, 3, 0), - }, - colGrab: { - width: 60, - }, - colInventory: { - textAlign: "right", - }, - colName: { - paddingLeft: 0, - }, - colPrice: { - textAlign: "right", - }, - colSku: {}, - colStatus: {}, - defaultVariant: { - color: fade(theme.palette.text.secondary, 0.6), - display: "block", - }, - link: { - cursor: "pointer", - }, - select: { - display: "inline-block", - }, - textLeft: { - textAlign: "left" as "left", - }, - textRight: { - textAlign: "right" as "right", - }, - warehouseLabel: { - display: "inline-block", - marginRight: theme.spacing(), - }, - warehouseSelectContainer: { - paddingTop: theme.spacing(2), - }, - }), - { name: "ProductVariants" }, -); - -function getAvailabilityLabel( - intl: IntlShape, - warehouse: string, - variant: ProductDetailsVariantFragment[][0], - numAvailable: number, -): string { - if (variant.preorder) { - if (variant.preorder.globalThreshold) { - return intl.formatMessage( - { - id: "80FeaT", - defaultMessage: "{globalThreshold} Global threshold", - description: "product variant preorder threshold", - }, - { - globalThreshold: variant.preorder.globalThreshold, - }, - ); - } - - return intl.formatMessage({ - id: "qbqMpk", - defaultMessage: "In preorder", - description: "product variant preorder threshold", - }); - } - - const variantStock = variant.stocks.find(s => s.warehouse.id === warehouse); - - if (!!warehouse) { - if (!!variantStock) { - if (variantStock.quantity > 0) { - return intl.formatMessage( - { - id: "uVssds", - defaultMessage: - "{stockQuantity,plural,one{{stockQuantity} available} other{{stockQuantity} available}}", - description: "product variant inventory", - }, - { - stockQuantity: variantStock.quantity, - }, - ); - } else { - return intl.formatMessage({ - id: "7mK2vs", - defaultMessage: "Unavailable", - description: "product variant inventory", - }); - } - } else { - return intl.formatMessage({ - id: "9PmyrU", - defaultMessage: "Not stocked", - description: "product variant inventory", - }); - } - } else { - if (numAvailable > 0) { - return intl.formatMessage( - { - id: "wWYYBR", - defaultMessage: - "{numLocations,plural,one{{numAvailable} available at {numLocations} location} other{{numAvailable} available at {numLocations} locations}}", - description: "product variant inventory", - }, - { - numAvailable, - numLocations: variant.stocks.length, - }, - ); - } else { - return intl.formatMessage({ - id: "6+sMz4", - defaultMessage: "Unavailable in all locations", - description: "product variant inventory", - }); - } - } -} - -interface ProductVariantsProps extends ListActions, ChannelProps { - productId: string; - disabled: boolean; +interface ProductVariantsProps { + channels: ChannelData[]; + errors: ProductVariantListError[]; limits: RefreshLimitsQuery["shop"]["limits"]; - product: ProductFragment; + variantAttributes: ProductFragment["productType"]["variantAttributes"]; variants: ProductDetailsVariantFragment[]; - onVariantReorder: ReorderAction; - onSetDefaultVariant(variant: ProductDetailsVariantFragment[][0]); + warehouses: WarehouseFragment[]; + onAttributeValuesSearch: ( + id: string, + query: string, + ) => Promise>>; + onChange: (data: DatagridChangeOpts) => void; + onRowClick: (id: string) => void; } -const numberOfColumns = 7; - -export const ProductVariants: React.FC = props => { - const { - productId, - disabled, - limits, - variants, - product, - onVariantReorder, - onSetDefaultVariant, - isChecked, - selected, - selectedChannelId, - toggle, - toggleAll, - toolbar, - } = props; - const classes = useStyles(props); - +export const ProductVariants: React.FC = ({ + channels, + errors, + variants, + warehouses, + variantAttributes, + onAttributeValuesSearch, + onChange, + onRowClick, +}) => { const intl = useIntl(); - const [warehouse, setWarehouse] = React.useState(null); - const hasVariants = maybe(() => variants.length > 0, true); - const limitReached = isLimitReached(limits, "productVariants"); + // const limitReached = isLimitReached(limits, "productVariants"); + + const columns = React.useMemo( + () => + variantAttributes && warehouses && channels + ? [ + "name", + "sku", + ...channels?.flatMap(channel => [ + `availableInChannel:${channel.id}`, + `channel:${channel.id}`, + ]), + ...warehouses?.map(warehouse => `stock:${warehouse.id}`), + ...variantAttributes + .filter(attribute => + [ + AttributeInputTypeEnum.DROPDOWN, + AttributeInputTypeEnum.PLAIN_TEXT, + ].includes(attribute.inputType), + ) + .map(attribute => `attribute:${attribute.id}`), + ].map(c => + getColumnData(c, channels, warehouses, variantAttributes, intl), + ) + : [], + [variantAttributes, warehouses, channels], + ); + + const getCellContent = React.useCallback( + ([column, row]: Item, opts: GetCellContentOpts) => + getData({ + availableColumns: columns, + column, + row, + channels, + variants, + searchAttributeValues: onAttributeValuesSearch, + ...opts, + }), + [columns, variants], + ); + + const getCellError = React.useCallback( + ([column, row]: Item, opts: GetCellContentOpts) => + getError(errors, { + availableColumns: columns, + column, + row, + channels, + variants, + searchAttributeValues: onAttributeValuesSearch, + ...opts, + }), + [columns, variants, errors], + ); return ( - - - - - } - /> - - {limitReached && ( - - - + [ + { + label: "Edit Variant", + onSelect: () => onRowClick(variants[index].id), + }, + ]} + rows={variants?.length ?? 0} + selectionActions={(indexes, { removeRows }) => ( + )} - - {variants.length > 0 ? ( - - - - - setWarehouse(event.target.value)} - /> - - ) : ( - - - - - - )} - {hasVariants && ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {renderCollection(variants, (variant, variantIndex) => { - const isSelected = variant ? isChecked(variant.id) : false; - const isDefault = - variant && product?.defaultVariant?.id === variant?.id; - const numAvailable = - variant && variant.stocks - ? variant.stocks.reduce( - (acc, s) => acc + s.quantity - s.quantityAllocated, - 0, - ) - : null; - const channel = variant.channelListings.find( - listing => listing.channel.id === selectedChannelId, - ); - - return ( - - - toggle(variant.id)} - /> - - - {variant ? variant.name || variant.sku : } - {isDefault && ( - - {intl.formatMessage({ - id: "vZMs8f", - defaultMessage: "Default", - description: "default product variant indicator", - })} - - )} - - - {variant ? variant.sku : } - - - - {variant ? ( - - ) : ( - - )} - - - - {numAvailable === null ? ( - - ) : ( - getAvailabilityLabel( - intl, - warehouse, - variant, - numAvailable, - ) - )} - - - - {variant?.id !== product?.defaultVariant?.id && ( - - onSetDefaultVariant(variant) - } - /> - )} - - - - ); - })} - - - )} - + title={intl.formatMessage(messages.title)} + onChange={onChange} + /> ); }; ProductVariants.displayName = "ProductVariants"; diff --git a/src/products/components/ProductVariants/datagrid/columnData.ts b/src/products/components/ProductVariants/datagrid/columnData.ts new file mode 100644 index 000000000..9e2efc52b --- /dev/null +++ b/src/products/components/ProductVariants/datagrid/columnData.ts @@ -0,0 +1,18 @@ +export function makeGetColumnData( + regexp: RegExp, +): (column: string) => string | null { + return column => { + if (!regexp.test(column)) { + return null; + } + + return column.match(regexp)[1]; + }; +} + +export const getColumnAttribute = makeGetColumnData(/^attribute:(.*)/); +export const getColumnChannel = makeGetColumnData(/^channel:(.*)/); +export const getColumnChannelAvailability = makeGetColumnData( + /^availableInChannel:(.*)/, +); +export const getColumnStock = makeGetColumnData(/^stock:(.*)/); diff --git a/src/products/components/ProductVariants/datagrid/getVariantChannelsInputs.ts b/src/products/components/ProductVariants/datagrid/getVariantChannelsInputs.ts new file mode 100644 index 000000000..bf14d4cb3 --- /dev/null +++ b/src/products/components/ProductVariants/datagrid/getVariantChannelsInputs.ts @@ -0,0 +1,62 @@ +import { + DatagridChange, + DatagridChangeOpts, +} from "@saleor/components/Datagrid/useDatagridChange"; + +import { getColumnChannel, getColumnChannelAvailability } from "./columnData"; + +const byCurrentRowByIndex = (index: number, data: DatagridChangeOpts) => ( + change: DatagridChange, +) => { + const totalRemoved = data.removed.filter(r => r <= index).length; + return change.row === index + totalRemoved; +}; + +const byChannelColumn = (change: DatagridChange) => + getColumnChannel(change.column); + +const availabilityToChannelColumn = (change: DatagridChange) => { + const availabilityChannelId = getColumnChannelAvailability(change.column); + + if (availabilityChannelId) { + return { + data: { + value: change.data ? 0 : null, + }, + column: `channel:${availabilityChannelId}`, + }; + } + return change; +}; + +const byColumn = (prev: DatagridChange[], change: DatagridChange) => { + const index = prev.findIndex(p => p.column === change.column); + if (index > -1) { + prev[index] = change; + return prev; + } + + return prev.concat(change); +}; + +const dataGridChangeToFlatChannel = (change: DatagridChange) => ({ + channelId: getColumnChannel(change.column), + price: change.data.value, +}); + +const byNotNullPrice = ( + change: ReturnType, +) => change.price !== null; + +export function getVariantChannelsInputs( + data: DatagridChangeOpts, + index: number, +) { + return data.updates + .filter(byCurrentRowByIndex(index, data)) + .map(availabilityToChannelColumn) + .filter(byChannelColumn) + .reduce(byColumn, []) + .map(dataGridChangeToFlatChannel) + .filter(byNotNullPrice); +} diff --git a/src/products/components/ProductVariants/messages.ts b/src/products/components/ProductVariants/messages.ts new file mode 100644 index 000000000..04099afc9 --- /dev/null +++ b/src/products/components/ProductVariants/messages.ts @@ -0,0 +1,46 @@ +import { defineMessages } from "react-intl"; + +const messages = defineMessages({ + name: { + defaultMessage: "Variant name", + id: "J4E+jp", + }, + sku: { + defaultMessage: "SKU", + id: "k4brJy", + }, + margin: { + defaultMessage: "Margin", + id: "Us9cA1", + description: "profit margin", + }, + price: { + defaultMessage: "Price", + id: "4hl9rS", + description: "variant price in channel", + }, + available: { + defaultMessage: "Available", + id: "BbP+k3", + description: "variant availability in channel", + }, + empty: { + defaultMessage: "Use button above to add new product variants", + id: "IH47ID", + }, + warehouses: { + defaultMessage: "Warehouses", + id: "U5Da30", + }, + attributes: { + defaultMessage: "Attributes", + id: "+xTpT1", + }, + title: { + defaultMessage: "Variants", + id: "1WbTJ5", + description: "product variants, title", + }, +}); + +export default messages; diff --git a/src/products/components/ProductVariants/utils.tsx b/src/products/components/ProductVariants/utils.tsx new file mode 100644 index 000000000..f738bc5b9 --- /dev/null +++ b/src/products/components/ProductVariants/utils.tsx @@ -0,0 +1,370 @@ +import { GridCell } from "@glideapps/glide-data-grid"; +import { ChannelData } from "@saleor/channels/utils"; +import { + booleanCell, + dropdownCell, + moneyCell, + numberCell, + textCell, +} from "@saleor/components/Datagrid/cells"; +import { emptyDropdownCellValue } from "@saleor/components/Datagrid/DropdownCell"; +import { numberCellEmptyValue } from "@saleor/components/Datagrid/NumberCell"; +import { AvailableColumn } from "@saleor/components/Datagrid/types"; +import { + DatagridChange, + DatagridChangeOpts, +} from "@saleor/components/Datagrid/useDatagridChange"; +import { Choice } from "@saleor/components/SingleSelectField"; +import { + ProductDetailsVariantFragment, + ProductFragment, + VariantDatagridChannelListingUpdateMutationVariables, + VariantDatagridStockUpdateMutationVariables, + VariantDatagridUpdateMutationVariables, + WarehouseFragment, +} from "@saleor/graphql"; +import { ProductVariantListError } from "@saleor/products/views/ProductUpdate/handlers/errors"; +import { mapNodeToChoice } from "@saleor/utils/maps"; +import { MutableRefObject } from "react"; +import { IntlShape } from "react-intl"; + +import { + getColumnAttribute, + getColumnChannel, + getColumnChannelAvailability, + getColumnStock, +} from "./datagrid/columnData"; +import { getVariantChannelsInputs } from "./datagrid/getVariantChannelsInputs"; +import messages from "./messages"; + +export function getVariantInput(data: DatagridChangeOpts, index: number) { + const attributes = data.updates + .filter( + change => + getColumnAttribute(change.column) && + change.row === index + data.removed.filter(r => r <= index).length, + ) + .map(change => { + const attributeId = getColumnAttribute(change.column); + + return { + id: attributeId, + values: [change.data.value.value], + }; + }); + + const sku = data.updates.find( + change => + change.column === "sku" && + change.row === index + data.removed.filter(r => r <= index).length, + )?.data; + + const name = data.updates.find( + change => + change.column === "name" && + change.row === index + data.removed.filter(r => r <= index).length, + )?.data; + + return { + attributes, + sku, + name, + }; +} + +export function getVariantInputs( + variants: ProductFragment["variants"], + data: DatagridChangeOpts, +): VariantDatagridUpdateMutationVariables[] { + return variants + .map( + (variant, variantIndex): VariantDatagridUpdateMutationVariables => ({ + id: variant.id, + input: getVariantInput(data, variantIndex), + }), + ) + .filter( + variables => + variables.input.sku || + variables.input.name || + variables.input.attributes.length > 0, + ); +} + +export function getStockInputs(data: DatagridChangeOpts, index: number) { + const stockChanges = data.updates.filter(change => + getColumnStock(change.column), + ); + + const variantChanges = stockChanges + .filter( + change => + change.row === index + data.removed.filter(r => r <= index).length, + ) + .map(change => ({ + warehouse: getColumnStock(change.column), + quantity: change.data.value, + })); + + return { + stocks: variantChanges.filter( + change => change.quantity !== numberCellEmptyValue, + ), + removeStocks: variantChanges + .filter(change => change.quantity === numberCellEmptyValue) + .map(({ warehouse }) => warehouse), + }; +} + +export function getStocks( + variants: ProductFragment["variants"], + data: DatagridChangeOpts, +): VariantDatagridStockUpdateMutationVariables[] { + return variants + .map((variant, variantIndex) => ({ + id: variant.id, + ...getStockInputs(data, variantIndex), + })) + .filter( + variables => + variables.removeStocks.length > 0 || variables.stocks.length > 0, + ); +} + +export function getVariantChannels( + variants: ProductFragment["variants"], + data: DatagridChangeOpts, +): VariantDatagridChannelListingUpdateMutationVariables[] { + return variants + .map((variant, variantIndex) => ({ + id: variant.id, + input: getVariantChannelsInputs(data, variantIndex), + })) + .filter(({ input }) => input.length > 0); +} + +function errorMatchesColumn( + error: ProductVariantListError, + columnId: string, +): boolean { + if (error.type === "channel") { + return ( + error.channelIds.includes(getColumnChannel(columnId)) || + error.channelIds.includes(getColumnChannelAvailability(columnId)) + ); + } + + if (error.type === "stock") { + return error.warehouseId.includes(getColumnStock(columnId)); + } + + if (error.type === "variantData") { + if (error.attributes?.length > 0) { + return error.attributes.includes(getColumnAttribute(columnId)); + } + return columnId === "sku"; + } +} + +export function getError( + errors: ProductVariantListError[], + { availableColumns, removed, column, row, variants }: GetDataOrError, +): boolean { + if (column === -1) { + return false; + } + + const columnId = availableColumns[column].id; + const variantId = variants[row + removed.filter(r => r <= row).length]?.id; + + if (!variantId) { + return errors.some( + err => err.type === "create" && err.index === row - variants.length, + ); + } + + return errors.some( + err => + err.type !== "create" && + err.variantId === variantId && + errorMatchesColumn(err, columnId), + ); +} + +interface GetDataOrError { + availableColumns: AvailableColumn[]; + column: number; + row: number; + variants: ProductDetailsVariantFragment[]; + changes: MutableRefObject; + channels: ChannelData[]; + added: number[]; + removed: number[]; + searchAttributeValues: ( + id: string, + text: string, + ) => Promise>>; + getChangeIndex: (column: string, row: number) => number; +} + +export function getData({ + availableColumns, + changes, + added, + removed, + column, + getChangeIndex, + row, + channels, + variants, + searchAttributeValues, +}: GetDataOrError): GridCell { + // For some reason it happens when user deselects channel + if (column === -1) { + return textCell(""); + } + + const columnId = availableColumns[column].id; + const change = changes.current[getChangeIndex(columnId, row)]?.data; + const dataRow = added.includes(row) + ? undefined + : variants[row + removed.filter(r => r <= row).length]; + + switch (columnId) { + case "name": + case "sku": + const value = change ?? (dataRow ? dataRow[columnId] : ""); + return textCell(value || ""); + } + + if (getColumnStock(columnId)) { + const value = + change?.value ?? + dataRow?.stocks.find( + stock => stock.warehouse.id === getColumnStock(columnId), + )?.quantity ?? + numberCellEmptyValue; + + return numberCell(value); + } + + if (getColumnChannel(columnId)) { + const channelId = getColumnChannel(columnId); + const listing = dataRow?.channelListings.find( + listing => listing.channel.id === channelId, + ); + const available = + changes.current[getChangeIndex(`availableInChannel:${channelId}`, row)] + ?.data ?? !!listing; + + if (!available) { + return { + ...numberCell(numberCellEmptyValue), + readonly: false, + allowOverlay: false, + }; + } + + const currency = channels.find(channel => channelId === channel.id) + ?.currency; + const value = change?.value ?? listing?.price?.amount ?? 0; + + return moneyCell(value, currency); + } + + if (getColumnChannelAvailability(columnId)) { + const channelId = getColumnChannelAvailability(columnId); + const listing = dataRow?.channelListings.find( + listing => listing.channel.id === channelId, + ); + const value = change ?? !!listing; + + return booleanCell(value); + } + + if (getColumnAttribute(columnId)) { + const value = + change?.value ?? + mapNodeToChoice( + dataRow?.attributes.find( + attribute => attribute.attribute.id === getColumnAttribute(columnId), + )?.values, + )[0] ?? + emptyDropdownCellValue; + + return dropdownCell(value, { + allowCustomValues: true, + emptyOption: true, + update: text => searchAttributeValues(getColumnAttribute(columnId), text), + }); + } +} + +export function getColumnData( + name: string, + channels: ChannelData[], + warehouses: WarehouseFragment[], + variantAttributes: ProductFragment["productType"]["variantAttributes"], + intl: IntlShape, +): AvailableColumn { + const common = { + id: name, + width: 200, + // Now we don't weirdly merge top-left header with the frozen column (name), + // leaving rest unnamed group columns (sku in this case) unmerged + group: " ", + }; + + if (["name", "sku"].includes(name)) { + return { + ...common, + title: intl.formatMessage(messages[name]), + }; + } + + if (getColumnStock(name)) { + return { + ...common, + width: 100, + title: warehouses.find(warehouse => warehouse.id === getColumnStock(name)) + ?.name, + group: intl.formatMessage(messages.warehouses), + }; + } + + if (getColumnChannel(name)) { + const channel = channels.find( + channel => channel.id === getColumnChannel(name), + ); + return { + ...common, + width: 150, + title: intl.formatMessage(messages.price), + group: channel.name, + }; + } + + if (getColumnChannelAvailability(name)) { + const channel = channels.find( + channel => channel.id === getColumnChannelAvailability(name), + ); + return { + ...common, + width: 80, + title: intl.formatMessage(messages.available), + group: channel.name, + }; + } + + if (getColumnAttribute(name)) { + return { + ...common, + title: variantAttributes.find( + attribute => attribute.id === getColumnAttribute(name), + )?.name, + group: intl.formatMessage(messages.attributes), + }; + } + + throw new Error(`Unknown column: ${name}`); +} diff --git a/src/products/fixtures.ts b/src/products/fixtures.ts index 41d72acf7..6d8466ded 100644 --- a/src/products/fixtures.ts +++ b/src/products/fixtures.ts @@ -1,3 +1,4 @@ +import { channelsList } from "@saleor/channels/fixtures"; import { AttributeInputTypeEnum, ProductFragment, @@ -9,7 +10,6 @@ import { } from "@saleor/graphql"; import { ProductType } from "@saleor/sdk/dist/apollo/types"; import { RelayToFlat } from "@saleor/types"; -import { warehouseList } from "@saleor/warehouses/fixtures"; import * as richTextEditorFixtures from "../components/RichTextEditor/fixtures.json"; @@ -222,12 +222,7 @@ export const product: ( { __typename: "ProductChannelListing", availableForPurchase: null, - channel: { - __typename: "Channel", - currencyCode: "USD", - id: "123", - name: "Channel1", - }, + channel: channelsList[0], isAvailableForPurchase: false, isPublished: true, pricing: { @@ -258,12 +253,7 @@ export const product: ( { __typename: "ProductChannelListing", availableForPurchase: null, - channel: { - __typename: "Channel", - currencyCode: "USD", - id: "124", - name: "Channel2", - }, + channel: channelsList[1], isAvailableForPurchase: false, isPublished: false, pricing: { @@ -610,142 +600,408 @@ export const product: ( url: "/example-url", variants: [ { - __typename: "ProductVariant", - channelListings: [], - id: "pv75934", - quantityLimitPerCustomer: 30, - margin: 2, - media: [ + id: "UHJvZHVjdFZhcmlhbnQ6MjAz", + sku: "43226647", + name: "1l", + margin: null, + attributes: [ { - __typename: "ProductMedia", - id: "pi92837", - type: ProductMediaType.IMAGE, - oembedData: "{}", - url: placeholderImage, - }, - { - __typename: "ProductMedia", - id: "pi92838", - type: ProductMediaType.IMAGE, - oembedData: "{}", - url: placeholderImage, + attribute: { + id: "QXR0cmlidXRlOjE1", + name: "Bottle Size", + __typename: "Attribute", + }, + values: [ + { + id: "QXR0cmlidXRlVmFsdWU6NDc=", + name: "1l", + __typename: "AttributeValue", + }, + ], + __typename: "SelectedAttribute", + }, + ], + media: [ + { + id: "1", + type: ProductMediaType.IMAGE, + url: placeholderImage, + __typename: "ProductMedia", }, ], - name: "Cordoba Oro", - sku: "87192-94370", stocks: [ { - __typename: "Stock", - id: "1", - quantity: 1, + id: "U3RvY2s6MTY0", + quantity: 272, quantityAllocated: 0, - warehouse: warehouseList[0], + warehouse: { + id: + "V2FyZWhvdXNlOjI2NDNiNmIwLWExMTQtNGRiMC1hM2U4LTFkZGY3ZGM3NDVkMg==", + name: "Europe for click and collect", + __typename: "Warehouse", + }, + __typename: "Stock", }, { + id: "U3RvY2s6MTY1", + quantity: 272, + quantityAllocated: 0, + warehouse: { + id: + "V2FyZWhvdXNlOmFmZDA4YjY4LWQwYmMtNGQ1My1iNjJkLTg1YWMxOWI3MjliYg==", + name: "Europe", + __typename: "Warehouse", + }, __typename: "Stock", - id: "2", - quantity: 4, + }, + { + id: "U3RvY2s6MTY2", + quantity: 274, quantityAllocated: 2, - warehouse: warehouseList[1], + warehouse: { + id: + "V2FyZWhvdXNlOjMxOTRjNjY5LTY1YjItNDBjYy04ZDI5LWI3M2Q0YTUwODBmMQ==", + name: "Asia", + __typename: "Warehouse", + }, + __typename: "Stock", + }, + { + id: "U3RvY2s6MTYz", + quantity: 272, + quantityAllocated: 0, + warehouse: { + id: + "V2FyZWhvdXNlOjI5YzBlYmYwLWVkNzktNDlmOS1hYmQ0LWQwNDBlOGNlZmI3Mg==", + name: "Oceania", + __typename: "Warehouse", + }, + __typename: "Stock", + }, + { + id: "U3RvY2s6MTY4", + quantity: 272, + quantityAllocated: 0, + warehouse: { + id: + "V2FyZWhvdXNlOjRiNjc1ZmVlLTE3OWYtNGMwNS04YmJlLWE0ZDJjOTc0OWQzMA==", + name: "Africa", + __typename: "Warehouse", + }, + __typename: "Stock", + }, + { + id: "U3RvY2s6MTY3", + quantity: 274, + quantityAllocated: 2, + warehouse: { + id: + "V2FyZWhvdXNlOmQwODA2MzM5LTVhNjAtNDAxNi1hNGUwLTRjNDYxNTZlY2IzMQ==", + name: "Americas", + __typename: "Warehouse", + }, + __typename: "Stock", }, ], trackInventory: true, - preorder: { - __typename: "PreorderData", - endDate: null, - globalSoldUnits: null, - globalThreshold: 0, - }, - }, - { - __typename: "ProductVariant", - quantityLimitPerCustomer: null, + preorder: null, channelListings: [ { - __typename: "ProductVariantChannelListing", channel: { - __typename: "Channel", + id: "Q2hhbm5lbDox", + name: "Channel-USD", currencyCode: "USD", - id: "123", - name: "Channel1", - }, - costPrice: { - __typename: "Money", - amount: 10, - currency: "USD", + __typename: "Channel", }, price: { - __typename: "Money", - amount: 1, + amount: 5.0, currency: "USD", + __typename: "Money", + }, + costPrice: { + amount: 1.5, + currency: "USD", + __typename: "Money", }, preorderThreshold: { - __typename: "PreorderThreshold", - quantity: 0, + quantity: null, soldUnits: 0, + __typename: "PreorderThreshold", }, + __typename: "ProductVariantChannelListing", }, { - __typename: "ProductVariantChannelListing", channel: { + id: "Q2hhbm5lbDoy", + name: "Channel-PLN", + currencyCode: "PLN", __typename: "Channel", - currencyCode: "USD", - id: "124", - name: "Channel2", - }, - costPrice: { - __typename: "Money", - amount: 10, - currency: "USD", }, price: { + amount: 20.0, + currency: "PLN", + __typename: "Money", + }, + costPrice: { + amount: 6.0, + currency: "PLN", __typename: "Money", - amount: 1, - currency: "USD", }, preorderThreshold: { - __typename: "PreorderThreshold", - quantity: 0, + quantity: null, soldUnits: 0, + __typename: "PreorderThreshold", }, + __typename: "ProductVariantChannelListing", + }, + ], + quantityLimitPerCustomer: null, + __typename: "ProductVariant", + }, + { + id: "UHJvZHVjdFZhcmlhbnQ6MjA0", + sku: "80884671", + name: "80884671", + margin: null, + attributes: [ + { + attribute: { + id: "QXR0cmlidXRlOjE1", + name: "Bottle Size", + __typename: "Attribute", + }, + values: [ + { + id: "QXR0cmlidXRlVmFsdWU6NDg=", + name: "2l", + __typename: "AttributeValue", + }, + ], + __typename: "SelectedAttribute", }, ], - id: "pv68615", - margin: 7, media: [ { - __typename: "ProductMedia", - id: "pi81234", + id: "1", + type: ProductMediaType.IMAGE, + url: placeholderImage, + __typename: "ProductMedia", + }, + ], + stocks: [], + trackInventory: true, + preorder: null, + channelListings: [ + { + channel: { + id: "Q2hhbm5lbDox", + name: "Channel-USD", + currencyCode: "USD", + __typename: "Channel", + }, + price: { + amount: 7.0, + currency: "USD", + __typename: "Money", + }, + costPrice: { + amount: 2.0, + currency: "USD", + __typename: "Money", + }, + preorderThreshold: { + quantity: null, + soldUnits: 0, + __typename: "PreorderThreshold", + }, + __typename: "ProductVariantChannelListing", + }, + { + channel: { + id: "Q2hhbm5lbDoy", + name: "Channel-PLN", + currencyCode: "PLN", + __typename: "Channel", + }, + price: { + amount: 28.0, + currency: "PLN", + __typename: "Money", + }, + costPrice: { + amount: 8.0, + currency: "PLN", + __typename: "Money", + }, + preorderThreshold: { + quantity: null, + soldUnits: 0, + __typename: "PreorderThreshold", + }, + __typename: "ProductVariantChannelListing", + }, + ], + quantityLimitPerCustomer: null, + __typename: "ProductVariant", + }, + { + id: "UHJvZHVjdFZhcmlhbnQ6MjAy", + sku: "93855755", + name: "500ml", + margin: null, + attributes: [ + { + attribute: { + id: "QXR0cmlidXRlOjE1", + name: "Bottle Size", + __typename: "Attribute", + }, + values: [ + { + id: "QXR0cmlidXRlVmFsdWU6NDY=", + name: "500ml", + __typename: "AttributeValue", + }, + ], + __typename: "SelectedAttribute", + }, + ], + media: [ + { + id: "1", type: ProductMediaType.IMAGE, - oembedData: "{}", url: placeholderImage, - }, - { __typename: "ProductMedia", - id: "pi1236912", - type: ProductMediaType.IMAGE, - oembedData: "{}", - url: placeholderImage, }, ], - name: "silver", - sku: "69055-15190", stocks: [ { + id: "U3RvY2s6MTU5", + quantity: 418, + quantityAllocated: 0, + warehouse: { + id: + "V2FyZWhvdXNlOmFmZDA4YjY4LWQwYmMtNGQ1My1iNjJkLTg1YWMxOWI3MjliYg==", + name: "Europe", + __typename: "Warehouse", + }, + __typename: "Stock", + }, + { + id: "U3RvY2s6MTYw", + quantity: 418, + quantityAllocated: 0, + warehouse: { + id: + "V2FyZWhvdXNlOjMxOTRjNjY5LTY1YjItNDBjYy04ZDI5LWI3M2Q0YTUwODBmMQ==", + name: "Asia", + __typename: "Warehouse", + }, + __typename: "Stock", + }, + { + id: "U3RvY2s6MTYx", + quantity: 418, + quantityAllocated: 0, + warehouse: { + id: + "V2FyZWhvdXNlOmQwODA2MzM5LTVhNjAtNDAxNi1hNGUwLTRjNDYxNTZlY2IzMQ==", + name: "Americas", + __typename: "Warehouse", + }, + __typename: "Stock", + }, + { + id: "U3RvY2s6MTU3", + quantity: 418, + quantityAllocated: 0, + warehouse: { + id: + "V2FyZWhvdXNlOjI5YzBlYmYwLWVkNzktNDlmOS1hYmQ0LWQwNDBlOGNlZmI3Mg==", + name: "Oceania", + __typename: "Warehouse", + }, + __typename: "Stock", + }, + { + id: "U3RvY2s6MTU4", + quantity: 418, + quantityAllocated: 0, + warehouse: { + id: + "V2FyZWhvdXNlOjI2NDNiNmIwLWExMTQtNGRiMC1hM2U4LTFkZGY3ZGM3NDVkMg==", + name: "Europe for click and collect", + __typename: "Warehouse", + }, + __typename: "Stock", + }, + { + id: "U3RvY2s6MTYy", + quantity: 418, + quantityAllocated: 0, + warehouse: { + id: + "V2FyZWhvdXNlOjRiNjc1ZmVlLTE3OWYtNGMwNS04YmJlLWE0ZDJjOTc0OWQzMA==", + name: "Africa", + __typename: "Warehouse", + }, __typename: "Stock", - id: "1", - quantity: 13, - quantityAllocated: 2, - warehouse: warehouseList[0], }, ], - trackInventory: false, - preorder: { - __typename: "PreorderData", - endDate: null, - globalSoldUnits: null, - globalThreshold: 0, - }, + trackInventory: true, + preorder: null, + channelListings: [ + { + channel: { + id: "Q2hhbm5lbDox", + name: "Channel-USD", + currencyCode: "USD", + __typename: "Channel", + }, + price: { + amount: 5.0, + currency: "USD", + __typename: "Money", + }, + costPrice: { + amount: 1.0, + currency: "USD", + __typename: "Money", + }, + preorderThreshold: { + quantity: null, + soldUnits: 0, + __typename: "PreorderThreshold", + }, + __typename: "ProductVariantChannelListing", + }, + { + channel: { + id: "Q2hhbm5lbDoy", + name: "Channel-PLN", + currencyCode: "PLN", + __typename: "Channel", + }, + price: { + amount: 20.0, + currency: "PLN", + __typename: "Money", + }, + costPrice: { + amount: 4.0, + currency: "PLN", + __typename: "Money", + }, + preorderThreshold: { + quantity: null, + soldUnits: 0, + __typename: "PreorderThreshold", + }, + __typename: "ProductVariantChannelListing", + }, + ], + quantityLimitPerCustomer: null, + __typename: "ProductVariant", }, ], visibleInListings: true, @@ -3122,6 +3378,7 @@ export const variant = (placeholderImage: string): ProductVariantFragment => ({ channelListings: [ { __typename: "ProductChannelListing", + id: "11", isPublished: false, publicationDate: null, channel: { @@ -3133,6 +3390,7 @@ export const variant = (placeholderImage: string): ProductVariantFragment => ({ }, { __typename: "ProductChannelListing", + id: "12", isPublished: true, publicationDate: "2022-01-21", channel: { diff --git a/src/products/mutations.ts b/src/products/mutations.ts index a6cf916f2..232d8f950 100644 --- a/src/products/mutations.ts +++ b/src/products/mutations.ts @@ -82,87 +82,11 @@ export const productVariantSetDefault = gql` `; export const productUpdateMutation = gql` - mutation ProductUpdate( - $id: ID! - $input: ProductInput! - $firstValues: Int - $afterValues: String - $lastValues: Int - $beforeValues: String - ) { + mutation ProductUpdate($id: ID!, $input: ProductInput!) { productUpdate(id: $id, input: $input) { errors { ...ProductErrorWithAttributes } - product { - ...Product - } - } - } -`; - -export const simpleProductUpdateMutation = gql` - mutation SimpleProductUpdate( - $id: ID! - $input: ProductInput! - $productVariantId: ID! - $productVariantInput: ProductVariantInput! - $addStocks: [StockInput!]! - $deleteStocks: [ID!]! - $updateStocks: [StockInput!]! - $firstValues: Int - $afterValues: String - $lastValues: Int - $beforeValues: String - ) { - productUpdate(id: $id, input: $input) { - errors { - ...ProductErrorWithAttributes - } - product { - ...Product - } - } - productVariantUpdate(id: $productVariantId, input: $productVariantInput) { - errors { - ...ProductErrorWithAttributes - } - productVariant { - ...ProductVariant - } - } - productVariantStocksCreate( - stocks: $addStocks - variantId: $productVariantId - ) { - errors { - ...BulkStockError - } - productVariant { - ...ProductVariant - } - } - productVariantStocksDelete( - warehouseIds: $deleteStocks - variantId: $productVariantId - ) { - errors { - ...StockError - } - productVariant { - ...ProductVariant - } - } - productVariantStocksUpdate( - stocks: $updateStocks - variantId: $productVariantId - ) { - errors { - ...BulkStockError - } - productVariant { - ...ProductVariant - } } } `; @@ -193,6 +117,48 @@ export const variantDeleteMutation = gql` } `; +export const variantDatagridUpdateMutation = gql` + mutation VariantDatagridUpdate($id: ID!, $input: ProductVariantInput!) { + productVariantUpdate(id: $id, input: $input) { + errors { + ...ProductErrorWithAttributes + } + } + } +`; + +export const variantDatagridStockUpdateMutation = gql` + mutation VariantDatagridStockUpdate( + $stocks: [StockInput!]! + $removeStocks: [ID!]! + $id: ID! + ) { + productVariantStocksDelete(warehouseIds: $removeStocks, variantId: $id) { + errors { + ...ProductVariantStocksDeleteError + } + } + productVariantStocksUpdate(stocks: $stocks, variantId: $id) { + errors { + ...BulkStockError + } + } + } +`; + +export const variantDatagridChannelListingUpdateMutation = gql` + mutation VariantDatagridChannelListingUpdate( + $id: ID! + $input: [ProductVariantChannelListingAddInput!]! + ) { + productVariantChannelListingUpdate(id: $id, input: $input) { + errors { + ...ProductChannelListingError + } + } + } +`; + export const variantUpdateMutation = gql` mutation VariantUpdate( $addStocks: [StockInput!]! @@ -382,6 +348,22 @@ export const productBulkDeleteMutation = gql` } `; +export const ProductVariantBulkCreateMutation = gql` + mutation ProductVariantBulkCreate( + $id: ID! + $inputs: [ProductVariantBulkCreateInput!]! + ) { + productVariantBulkCreate(product: $id, variants: $inputs) { + errors { + ...BulkProductError + } + productVariants { + id + } + } + } +`; + export const ProductVariantBulkDeleteMutation = gql` mutation ProductVariantBulkDelete($ids: [ID!]!) { productVariantBulkDelete(ids: $ids) { @@ -411,18 +393,6 @@ export const ProductChannelListingUpdateMutation = gql` $input: ProductChannelListingUpdateInput! ) { productChannelListingUpdate(id: $id, input: $input) { - product { - id - channelListings { - ...ChannelListingProductWithoutPricing - } - variants { - id - channelListings { - ...ChannelListingProductVariant - } - } - } errors { ...ProductChannelListingError } diff --git a/src/products/urls.ts b/src/products/urls.ts index 82e013a57..7f422ebc5 100644 --- a/src/products/urls.ts +++ b/src/products/urls.ts @@ -79,7 +79,6 @@ export const productListUrl = (params?: ProductListUrlQueryParams): string => export const productPath = (id: string) => urlJoin(productSection + id); export type ProductUrlDialog = | "remove" - | "remove-variants" | "assign-attribute-value" | ChannelsAction; export type ProductUrlQueryParams = BulkAction & diff --git a/src/products/utils/data.ts b/src/products/utils/data.ts index 311e18d1a..745eee71e 100644 --- a/src/products/utils/data.ts +++ b/src/products/utils/data.ts @@ -3,12 +3,10 @@ import { getSelectedAttributeValues, mergeChoicesWithValues, } from "@saleor/attributes/utils/data"; -import { ChannelData } from "@saleor/channels/utils"; import { AttributeInput, VariantAttributeScope, } from "@saleor/components/Attributes"; -import { MetadataFormData } from "@saleor/components/Metadata/types"; import { SingleAutocompleteChoiceType } from "@saleor/components/SingleAutocompleteSelectField"; import { ProductDetailsVariantFragment, @@ -26,7 +24,7 @@ import { mapEdgesToItems, mapMetadataItemToInput } from "@saleor/utils/maps"; import moment from "moment"; import { ProductStockInput } from "../components/ProductStocks"; -import { ChannelsWithVariantsData } from "../views/ProductUpdate/types"; +import { ProductUpdateFormData } from "../components/ProductUpdatePage/types"; export interface Collection { id: string; @@ -171,19 +169,6 @@ export function getStockInputFromVariant( ); } -export function getStockInputFromProduct( - product: ProductFragment, -): ProductStockInput[] { - return product?.variants[0]?.stocks.map(stock => ({ - data: { - quantityAllocated: stock?.quantityAllocated, - }, - id: stock.warehouse.id, - label: stock.warehouse.name, - value: stock.quantity.toString(), - })); -} - export function getCollectionInput( productCollections: ProductFragment["collections"], ): Collection[] { @@ -208,42 +193,13 @@ export function getChoices(nodes: Node[]): SingleAutocompleteChoiceType[] { ); } -export interface ProductUpdatePageFormData extends MetadataFormData { - category: string | null; - changeTaxCode: boolean; - channelsWithVariants: ChannelsWithVariantsData; - channelListings: ChannelData[]; - channelsData: ChannelData[]; - chargeTaxes: boolean; - collections: string[]; - isAvailable: boolean; - name: string; - slug: string; - rating: number; - seoDescription: string; - seoTitle: string; - sku: string; - taxCode: string; - trackInventory: boolean; - weight: string; - isPreorder: boolean; - globalThreshold: string; - globalSoldUnits: number; - hasPreorderEndDate: boolean; - preorderEndDateTime?: string; -} - export function getProductUpdatePageFormData( product: ProductFragment, variants: ProductDetailsVariantFragment[], - currentChannels: ChannelData[], - channelsData: ChannelData[], - channelsWithVariants: ChannelsWithVariantsData, -): ProductUpdatePageFormData { +): ProductUpdateFormData { const variant = product?.variants[0]; + return { - channelsWithVariants, - channelsData, category: maybe(() => product.category.id, ""), changeTaxCode: !!product?.taxType.taxCode, chargeTaxes: maybe(() => product.chargeTaxes, false), @@ -251,7 +207,6 @@ export function getProductUpdatePageFormData( () => product.collections.map(collection => collection.id), [], ), - channelListings: currentChannels.map(listing => ({ ...listing })), isAvailable: !!product?.isAvailable, metadata: product?.metadata?.map(mapMetadataItemToInput), name: maybe(() => product.name, ""), diff --git a/src/products/utils/handlers.ts b/src/products/utils/handlers.ts index f7deab69b..0312410e7 100644 --- a/src/products/utils/handlers.ts +++ b/src/products/utils/handlers.ts @@ -1,10 +1,10 @@ import { ChannelData, - ChannelPreorderArgs, ChannelPriceAndPreorderData, ChannelPriceArgs, ChannelPriceData, } from "@saleor/channels/utils"; +import { ProductChannelListingAddInput } from "@saleor/graphql"; import { FormChange, UseFormResult } from "@saleor/hooks/useForm"; import moment from "moment"; @@ -26,26 +26,6 @@ export function createChannelsPriceChangeHandler( }; } -export function createChannelsPreorderChangeHandler( - channelListings: ChannelData[], - updateChannels: (data: ChannelData[]) => void, - triggerChange: () => void, -) { - return (id: string, preorderData: ChannelPreorderArgs) => { - const { preorderThreshold, unitsSold } = preorderData; - - const updatedChannels = channelListings.map(channel => - channel.id === id - ? { ...channel, preorderThreshold, unitsSold } - : channel, - ); - - updateChannels(updatedChannels); - - triggerChange(); - }; -} - export function createChannelsChangeHandler( channelsData: ChannelData[], updateChannels: (data: ChannelData[]) => void, @@ -122,9 +102,17 @@ export const getChannelsInput = (channels: ChannelPriceAndPreorderData[]) => }, })); -export const getAvailabilityVariables = (channels: ChannelData[]) => +export const getAvailabilityVariables = ( + channels: ChannelData[], +): ProductChannelListingAddInput[] => channels.map(channel => { - const { isAvailableForPurchase, availableForPurchase } = channel; + const { + isAvailableForPurchase, + availableForPurchase, + isPublished, + publicationDate, + visibleInListings, + } = channel; const isAvailable = availableForPurchase && !isAvailableForPurchase ? true @@ -137,9 +125,9 @@ export const getAvailabilityVariables = (channels: ChannelData[]) => : availableForPurchase, channelId: channel.id, isAvailableForPurchase: isAvailable, - isPublished: channel.isPublished, - publicationDate: channel.publicationDate, - visibleInListings: channel.visibleInListings, + isPublished, + publicationDate, + visibleInListings, }; }); diff --git a/src/products/views/ProductUpdate/ProductUpdate.tsx b/src/products/views/ProductUpdate/ProductUpdate.tsx index 006b34367..53559b095 100644 --- a/src/products/views/ProductUpdate/ProductUpdate.tsx +++ b/src/products/views/ProductUpdate/ProductUpdate.tsx @@ -1,15 +1,8 @@ import placeholderImg from "@assets/images/placeholder255x255.png"; import { DialogContentText } from "@material-ui/core"; -import ChannelsWithVariantsAvailabilityDialog from "@saleor/channels/components/ChannelsWithVariantsAvailabilityDialog"; -import { - ChannelData, - createChannelsDataWithPrice, - createSortedChannelsDataFromProduct, -} from "@saleor/channels/utils"; import ActionDialog from "@saleor/components/ActionDialog"; import useAppChannel from "@saleor/components/AppLayout/AppChannelContext"; import { AttributeInput } from "@saleor/components/Attributes"; -import ChannelsAvailabilityDialog from "@saleor/components/ChannelsAvailabilityDialog"; import NotFoundPage from "@saleor/components/NotFoundPage"; import { useShopLimitsQuery } from "@saleor/components/Shop/queries"; import { WindowTitle } from "@saleor/components/WindowTitle"; @@ -19,37 +12,18 @@ import { } from "@saleor/config"; import { ProductMediaCreateMutationVariables, - ProductUpdateMutation, - useAttributeValueDeleteMutation, - useFileUploadMutation, - useProductChannelListingUpdateMutation, useProductDeleteMutation, useProductDetailsQuery, useProductMediaCreateMutation, useProductMediaDeleteMutation, useProductMediaReorderMutation, - useProductUpdateMutation, - useProductVariantBulkDeleteMutation, - useProductVariantChannelListingUpdateMutation, - useProductVariantPreorderDeactivateMutation, - useProductVariantReorderMutation, - useSimpleProductUpdateMutation, - useUpdateMetadataMutation, - useUpdatePrivateMetadataMutation, - useVariantCreateMutation, useWarehouseListQuery, } from "@saleor/graphql"; import { getSearchFetchMoreProps } from "@saleor/hooks/makeTopLevelSearch/utils"; -import useBulkActions from "@saleor/hooks/useBulkActions"; -import useChannels from "@saleor/hooks/useChannels"; import useNavigator from "@saleor/hooks/useNavigator"; import useNotifier from "@saleor/hooks/useNotifier"; -import useOnSetDefaultVariant from "@saleor/hooks/useOnSetDefaultVariant"; -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 ProductVariantEndPreorderDialog from "@saleor/products/components/ProductVariantEndPreorderDialog"; +import { useSearchAttributeValuesSuggestions } from "@saleor/searches/useAttributeValueSearch"; import useCategorySearch from "@saleor/searches/useCategorySearch"; import useCollectionSearch from "@saleor/searches/useCollectionSearch"; import usePageSearch from "@saleor/searches/usePageSearch"; @@ -57,9 +31,7 @@ import useProductSearch from "@saleor/searches/useProductSearch"; import { getProductErrorMessage } from "@saleor/utils/errors"; import useAttributeValueSearchHandler from "@saleor/utils/handlers/attributeValueSearchHandler"; import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers"; -import createMetadataUpdateHandler from "@saleor/utils/handlers/metadataUpdateHandler"; import { mapEdgesToItems } from "@saleor/utils/maps"; -import { warehouseAddPath } from "@saleor/warehouses/urls"; import React from "react"; import { defineMessages, FormattedMessage, useIntl } from "react-intl"; @@ -70,18 +42,13 @@ import { productUrl, ProductUrlDialog, ProductUrlQueryParams, + productVariantEditUrl, } from "../../urls"; -import { - CHANNELS_AVAILIABILITY_MODAL_SELECTOR, - PRODUCT_UPDATE_FORM_ID, -} from "./consts"; import { createImageReorderHandler, createImageUploadHandler, - createUpdateHandler, - createVariantReorderHandler, } from "./handlers"; -import useChannelVariantListings from "./useChannelVariantListings"; +import { useProductUpdateHandler } from "./handlers/useProductUpdateHandler"; const messages = defineMessages({ deleteProductDialogTitle: { @@ -115,9 +82,6 @@ interface ProductUpdateProps { export const ProductUpdate: React.FC = ({ id, params }) => { const navigate = useNavigator(); const notify = useNotifier(); - const { isSelected, listElements, reset, toggle, toggleAll } = useBulkActions( - params.ids, - ); const intl = useIntl(); const { loadMore: loadMoreCategories, @@ -153,13 +117,6 @@ export const ProductUpdate: React.FC = ({ id, params }) => { result: searchAttributeValuesOpts, reset: searchAttributeReset, } = useAttributeValueSearchHandler(DEFAULT_INITIAL_SEARCH_DATA); - const shop = useShop(); - const [updateMetadata] = useUpdateMetadataMutation({}); - const [updatePrivateMetadata] = useUpdatePrivateMetadataMutation({}); - const [ - productVariantCreate, - productVariantCreateOpts, - ] = useVariantCreateMutation({}); const { data, loading, refetch } = useProductDetailsQuery({ displayLoader: true, @@ -171,7 +128,7 @@ export const ProductUpdate: React.FC = ({ id, params }) => { const isSimpleProduct = !data?.product?.productType?.hasVariants; - const { availableChannels, channel } = useAppChannel(!isSimpleProduct); + const { availableChannels } = useAppChannel(false); const limitOpts = useShopLimitsQuery({ variables: { @@ -179,26 +136,6 @@ export const ProductUpdate: React.FC = ({ id, params }) => { }, }); - const [uploadFile, uploadFileOpts] = useFileUploadMutation({}); - - const handleUpdate = (data: ProductUpdateMutation) => { - if (data.productUpdate.errors.length === 0) { - notify({ - status: "success", - text: intl.formatMessage(commonMessages.savedChanges), - }); - } - }; - const [updateProduct, updateProductOpts] = useProductUpdateMutation({ - onCompleted: handleUpdate, - }); - const [ - updateSimpleProduct, - updateSimpleProductOpts, - ] = useSimpleProductUpdateMutation({ - onCompleted: handleUpdate, - }); - const [ reorderProductImages, reorderProductImagesOpts, @@ -245,107 +182,21 @@ export const ProductUpdate: React.FC = ({ id, params }) => { }), }); - const [ - bulkProductVariantDelete, - bulkProductVariantDeleteOpts, - ] = useProductVariantBulkDeleteMutation({ - onCompleted: data => { - if (data.productVariantBulkDelete.errors.length === 0) { - closeModal(); - reset(); - refetch(); - limitOpts.refetch(); - } - }, - }); - const [openModal, closeModal] = createDialogActionHandlers< ProductUrlDialog, ProductUrlQueryParams >(navigate, params => productUrl(id, params), params); - const [ - isEndPreorderModalOpened, - setIsEndPreorderModalOpened, - ] = React.useState(false); - const product = data?.product; - // useMemo saves, like, 46 rerenders here - const allChannels: ChannelData[] = React.useMemo( - () => - createChannelsDataWithPrice( - product, - availableChannels, - ).sort((channel, nextChannel) => - channel.name.localeCompare(nextChannel.name), - ), - [product, availableChannels], - ); - - const [channelsData, setChannelsData] = useStateFromProps(allChannels); - const { - channels: updatedChannels, - channelsWithVariantsData, - setChannelVariantListing, - } = useChannelVariantListings(allChannels); - - const productChannelsChoices: ChannelData[] = createSortedChannelsDataFromProduct( - product, - ); - - const { - channelListElements, - channelsToggle, - currentChannels, - handleChannelsConfirm, - handleChannelsModalClose, - handleChannelsModalOpen, - isChannelSelected, - isChannelsModalOpen, - setCurrentChannels, - toggleAllChannels, - } = useChannels( - productChannelsChoices, - params?.action, - { - closeModal, - openModal, - }, - { formId: PRODUCT_UPDATE_FORM_ID }, - ); - - const warehouses = useWarehouseListQuery({ + const getAttributeValuesSuggestions = useSearchAttributeValuesSuggestions(); + const warehousesQuery = useWarehouseListQuery({ displayLoader: true, variables: { first: 50, - filter: { - channels: currentChannels.map(channel => channel.id), - }, }, }); - const [ - updateChannels, - updateChannelsOpts, - ] = useProductChannelListingUpdateMutation({ - onCompleted: data => { - if (!!data.productChannelListingUpdate.errors.length) { - data.productChannelListingUpdate.errors.forEach(error => - notify({ - status: "error", - text: getProductErrorMessage(error, intl), - }), - ); - } - }, - }); - - const [ - updateVariantChannels, - updateVariantChannelsOpts, - ] = useProductVariantChannelListingUpdateMutation({}); - const [ createProductMedia, createProductMediaOpts, @@ -381,41 +232,16 @@ export const ProductUpdate: React.FC = ({ id, params }) => { }); }; - const [ - deleteAttributeValue, - deleteAttributeValueOpts, - ] = useAttributeValueDeleteMutation({}); - - const onSetDefaultVariant = useOnSetDefaultVariant( - product ? product.id : null, - null, - ); - - const [ - reorderProductVariants, - reorderProductVariantsOpts, - ] = useProductVariantReorderMutation({}); - const handleBack = () => navigate(productListUrl()); const handleImageDelete = (id: string) => () => deleteProductImage({ variables: { id } }); - const handleSubmit = createMetadataUpdateHandler( - product, - createUpdateHandler( - product, - allChannels, - variables => uploadFile({ variables }), - variables => updateProduct({ variables }), - variables => updateSimpleProduct({ variables }), - updateChannels, - updateVariantChannels, - productVariantCreate, - variables => deleteAttributeValue({ variables }), - ), - variables => updateMetadata({ variables }), - variables => updatePrivateMetadata({ variables }), + const [submit, submitOpts] = useProductUpdateHandler(product); + + const warehouses = React.useMemo( + () => mapEdgesToItems(warehousesQuery.data?.warehouses) || [], + [warehousesQuery.data], ); const handleImageUpload = createImageUploadHandler(id, variables => @@ -425,22 +251,6 @@ export const ProductUpdate: React.FC = ({ id, params }) => { reorderProductImages({ variables }), ); - const handleVariantReorder = createVariantReorderHandler(product, variables => - reorderProductVariants({ variables }), - ); - - const handleDeactivatePreorder = async () => { - await handleDeactivateVariantPreorder(product.variants[0].id); - setIsEndPreorderModalOpened(false); - }; - - const [ - deactivatePreorder, - deactivatePreoderOpts, - ] = useProductVariantPreorderDeactivateMutation({}); - const handleDeactivateVariantPreorder = (id: string) => - deactivatePreorder({ variables: { id } }); - const handleAssignAttributeReferenceClick = (attribute: AttributeInput) => navigate( productUrl(id, { @@ -451,26 +261,17 @@ export const ProductUpdate: React.FC = ({ id, params }) => { ); const disableFormSave = - uploadFileOpts.loading || + submitOpts.loading || createProductImageOpts.loading || deleteProductOpts.loading || reorderProductImagesOpts.loading || - updateProductOpts.loading || - reorderProductVariantsOpts.loading || - updateChannelsOpts.loading || - updateVariantChannelsOpts.loading || - productVariantCreateOpts.loading || - deactivatePreoderOpts.loading || - deleteAttributeValueOpts.loading || createProductMediaOpts.loading || loading; const formTransitionState = getMutationState( - updateProductOpts.called || updateSimpleProductOpts.called, - updateProductOpts.loading || updateSimpleProductOpts.loading, - updateProductOpts.data?.productUpdate.errors, - updateSimpleProductOpts.data?.productUpdate.errors, - updateSimpleProductOpts.data?.productVariantUpdate.errors, + submitOpts.called, + submitOpts.loading, + submitOpts.errors, createProductMediaOpts.data?.productMediaCreate.errors, ); @@ -482,18 +283,6 @@ export const ProductUpdate: React.FC = ({ id, params }) => { const attributeValues = mapEdgesToItems(searchAttributeValuesOpts?.data?.attribute.choices) || []; - const errors = [ - ...(updateProductOpts.data?.productUpdate.errors || []), - ...(updateSimpleProductOpts.data?.productUpdate.errors || []), - ...(productVariantCreateOpts.data?.productVariantCreate.errors || []), - ]; - - const channelsErrors = [ - ...(updateChannelsOpts?.data?.productChannelListingUpdate?.errors || []), - ...(updateVariantChannelsOpts?.data?.productVariantChannelListingUpdate - ?.errors || []), - ]; - const fetchMoreCollections = getSearchFetchMoreProps( searchCollectionsOpts, loadMoreCollections, @@ -528,94 +317,43 @@ export const ProductUpdate: React.FC = ({ id, params }) => { return ( <> - {!!allChannels?.length && - (isSimpleProduct || product?.variants.length === 0 ? ( - - ) : ( - { - closeModal(); - setChannelVariantListing(listings); - }} - /> - ))} openModal("remove")} onImageReorder={handleImageReorder} onMediaUrlUpload={handleMediaUrlUpload} - onSubmit={handleSubmit} - onWarehouseConfigure={() => navigate(warehouseAddPath)} - onVariantReorder={handleVariantReorder} - onVariantEndPreorderDialogOpen={() => setIsEndPreorderModalOpened(true)} + onSubmit={submit} + onVariantShow={variantId => + navigate(productVariantEditUrl(product.id, variantId), { + resetScroll: true, + }) + } onImageUpload={handleImageUpload} onImageDelete={handleImageDelete} - toolbar={ - - openModal("remove-variants", { - ids: listElements, - }) - } - > - - - } - isChecked={isSelected} - selected={listElements.length} - toggle={toggle} - toggleAll={toggleAll} fetchMoreCategories={fetchMoreCategories} fetchMoreCollections={fetchMoreCollections} - selectedChannelId={channel?.id} assignReferencesAttributeId={ params.action === "assign-attribute-value" && params.id } @@ -631,6 +369,7 @@ export const ProductUpdate: React.FC = ({ id, params }) => { fetchMoreAttributeValues={fetchMoreAttributeValues} onCloseDialog={() => navigate(productUrl(id), { resetScroll: false })} onAttributeSelectBlur={searchAttributeReset} + onAttributeValuesSearch={getAttributeValuesSuggestions} /> = ({ id, params }) => { /> - - bulkProductVariantDelete({ - variables: { - ids: params.ids, - }, - }) - } - variant="delete" - title={intl.formatMessage(messages.deleteVariantDialogTitle)} - > - - {params?.ids?.length}, - }} - /> - - - {isSimpleProduct && !!product?.variants?.[0]?.preorder && ( - setIsEndPreorderModalOpened(false)} - onConfirm={handleDeactivatePreorder} - open={isEndPreorderModalOpened} - variantGlobalSoldUnits={product.variants[0].preorder.globalSoldUnits} - /> - )} ); }; diff --git a/src/products/views/ProductUpdate/consts.ts b/src/products/views/ProductUpdate/consts.ts index 5d27b9c50..bbf19c3db 100644 --- a/src/products/views/ProductUpdate/consts.ts +++ b/src/products/views/ProductUpdate/consts.ts @@ -1,11 +1,3 @@ -import { ChannelWithVariantData } from "./types"; - export const CHANNELS_AVAILIABILITY_MODAL_SELECTOR = "open-channels-picker"; -export const initialChannelWithVariantData: ChannelWithVariantData = { - variantsIdsToRemove: [], - variantsIdsToAdd: [], - selectedVariantsIds: [], -}; - export const PRODUCT_UPDATE_FORM_ID = Symbol(); diff --git a/src/products/views/ProductUpdate/handlers/errors.ts b/src/products/views/ProductUpdate/handlers/errors.ts new file mode 100644 index 000000000..1cd7950c1 --- /dev/null +++ b/src/products/views/ProductUpdate/handlers/errors.ts @@ -0,0 +1,120 @@ +import { FetchResult } from "@apollo/client"; +import { + ProductChannelListingUpdateMutation, + ProductErrorCode, + ProductVariantBulkCreateMutation, + ProductVariantChannelListingUpdateMutation, + ProductVariantChannelListingUpdateMutationVariables, + StockInput, + VariantDatagridStockUpdateMutation, + VariantDatagridStockUpdateMutationVariables, + VariantDatagridUpdateMutation, + VariantDatagridUpdateMutationVariables, +} from "@saleor/graphql"; +import { hasMutationErrors } from "@saleor/misc"; + +export type ProductVariantListError = + | { + __typename: "DatagridError"; + attributes: string[] | null; + error: ProductErrorCode; + variantId: string; + type: "variantData"; + } + | { + __typename: "DatagridError"; + variantId: string; + warehouseId: string; + type: "stock"; + } + | { + __typename: "DatagridError"; + error: ProductErrorCode; + variantId: string; + channelIds: string[]; + type: "channel"; + } + | { + __typename: "DatagridError"; + error: ProductErrorCode; + index: number; + type: "create"; + }; + +export function getProductVariantListErrors( + productChannelsUpdateResult: FetchResult, + variantMutationResults: FetchResult[], +): ProductVariantListError[] { + return [productChannelsUpdateResult, ...variantMutationResults] + .filter(hasMutationErrors) + .flatMap(result => { + if (result.data.productVariantChannelListingUpdate) { + const data = result.data as ProductVariantChannelListingUpdateMutation; + return data.productVariantChannelListingUpdate.errors.map< + ProductVariantListError + >(error => ({ + __typename: "DatagridError", + type: "channel", + error: error.code, + variantId: (result.extensions + .variables as ProductVariantChannelListingUpdateMutationVariables) + .id, + channelIds: error.channels, + })); + } + + if (result.data.productVariantStocksUpdate) { + const data = result.data as VariantDatagridStockUpdateMutation; + const variables = result.extensions + .variables as VariantDatagridStockUpdateMutationVariables; + return [ + ...data.productVariantStocksUpdate.errors.map< + ProductVariantListError + >(error => ({ + __typename: "DatagridError", + type: "stock", + variantId: (variables as VariantDatagridStockUpdateMutationVariables) + .id, + warehouseId: (variables.stocks as StockInput[])[error.index] + .warehouse, + })), + ...data.productVariantStocksDelete.errors.map< + ProductVariantListError + >(() => ({ + __typename: "DatagridError", + type: "stock", + variantId: (variables as VariantDatagridStockUpdateMutationVariables) + .id, + warehouseId: null, + })), + ]; + } + + if (result.data.productVariantUpdate) { + const data = result.data as VariantDatagridUpdateMutation; + const variables = result.extensions + .variables as VariantDatagridUpdateMutationVariables; + return data.productVariantUpdate.errors.map( + error => ({ + __typename: "DatagridError", + type: "variantData", + variantId: (variables as VariantDatagridUpdateMutationVariables).id, + error: error.code, + attributes: error.attributes, + }), + ); + } + + if (result.data.productVariantBulkCreate) { + const data = result.data as ProductVariantBulkCreateMutation; + return data.productVariantBulkCreate.errors.map< + ProductVariantListError + >(error => ({ + __typename: "DatagridError", + type: "create", + index: error.index, + error: error.code, + })); + } + }); +} diff --git a/src/products/views/ProductUpdate/handlers/index.ts b/src/products/views/ProductUpdate/handlers/index.ts index 9ffefed66..3c8ce1eb1 100644 --- a/src/products/views/ProductUpdate/handlers/index.ts +++ b/src/products/views/ProductUpdate/handlers/index.ts @@ -1,218 +1,14 @@ -import { FetchResult } from "@apollo/client"; import { - getAttributesAfterFileAttributesUpdate, - mergeAttributeValueDeleteErrors, - mergeFileUploadErrors, -} from "@saleor/attributes/utils/data"; -import { - handleDeleteMultipleAttributeValues, - handleUploadMultipleFiles, - prepareAttributesInput, -} from "@saleor/attributes/utils/handlers"; -import { ChannelData } from "@saleor/channels/utils"; -import { VALUES_PAGINATE_BY } from "@saleor/config"; -import { - AttributeErrorFragment, - AttributeValueDeleteMutation, - AttributeValueDeleteMutationVariables, - BulkStockErrorFragment, - FileUploadMutation, - FileUploadMutationVariables, Node, - ProductChannelListingErrorFragment, - ProductChannelListingUpdateMutation, - ProductChannelListingUpdateMutationVariables, - ProductErrorFragment, ProductFragment, ProductMediaCreateMutationVariables, ProductMediaReorderMutationVariables, - ProductUpdateMutation, - ProductUpdateMutationVariables, - ProductVariantChannelListingUpdateMutation, - ProductVariantChannelListingUpdateMutationVariables, - ProductVariantReorderMutationVariables, - SimpleProductUpdateMutation, - SimpleProductUpdateMutationVariables, - StockErrorFragment, - UploadErrorFragment, - VariantCreateMutation, - VariantCreateMutationVariables, + ProductVariantReorderMutationFn, } from "@saleor/graphql"; -import { ProductUpdatePageSubmitData } from "@saleor/products/components/ProductUpdatePage"; -import { - getAttributeInputFromProduct, - mapFormsetStockToStockInput, -} from "@saleor/products/utils/data"; import { ReorderEvent } from "@saleor/types"; import { move } from "@saleor/utils/lists"; -import { getParsedDataForJsonStringField } from "@saleor/utils/richText/misc"; import { arrayMove } from "react-sortable-hoc"; -import { - getChannelsVariables, - getSimpleChannelsVariables, - getSimpleProductErrors, - getSimpleProductVariables, - getVariantChannelsInput, -} from "./utils"; - -type SubmitErrors = Array< - | ProductErrorFragment - | StockErrorFragment - | BulkStockErrorFragment - | AttributeErrorFragment - | UploadErrorFragment - | ProductChannelListingErrorFragment ->; - -export function createUpdateHandler( - product: ProductFragment, - allChannels: ChannelData[], - uploadFile: ( - variables: FileUploadMutationVariables, - ) => Promise>, - updateProduct: ( - variables: ProductUpdateMutationVariables, - ) => Promise>, - updateSimpleProduct: ( - variables: SimpleProductUpdateMutationVariables, - ) => Promise>, - updateChannels: (options: { - variables: ProductChannelListingUpdateMutationVariables; - }) => Promise>, - updateVariantChannels: (options: { - variables: ProductVariantChannelListingUpdateMutationVariables; - }) => Promise>, - productVariantCreate: (options: { - variables: VariantCreateMutationVariables; - }) => Promise>, - deleteAttributeValue: ( - variables: AttributeValueDeleteMutationVariables, - ) => Promise>, -) { - return async (data: ProductUpdatePageSubmitData) => { - let errors: SubmitErrors = []; - - const uploadFilesResult = await handleUploadMultipleFiles( - data.attributesWithNewFileValue, - uploadFile, - ); - - const deleteAttributeValuesResult = await handleDeleteMultipleAttributeValues( - data.attributesWithNewFileValue, - product?.attributes, - deleteAttributeValue, - ); - - errors = [ - ...errors, - ...mergeFileUploadErrors(uploadFilesResult), - ...mergeAttributeValueDeleteErrors(deleteAttributeValuesResult), - ]; - const updatedFileAttributes = getAttributesAfterFileAttributesUpdate( - data.attributesWithNewFileValue, - uploadFilesResult, - ); - - const productVariables: ProductUpdateMutationVariables = { - id: product.id, - input: { - attributes: prepareAttributesInput({ - attributes: data.attributes, - prevAttributes: getAttributeInputFromProduct(product), - updatedFileAttributes, - }), - category: data.category, - chargeTaxes: data.chargeTaxes, - collections: data.collections, - description: getParsedDataForJsonStringField(data.description), - name: data.name, - rating: data.rating, - seo: { - description: data.seoDescription, - title: data.seoTitle, - }, - slug: data.slug, - taxCode: data.changeTaxCode ? data.taxCode : null, - }, - firstValues: VALUES_PAGINATE_BY, - }; - - if (product.productType.hasVariants) { - const result = await updateProduct(productVariables); - errors = [...errors, ...result.data.productUpdate.errors]; - - if (product.variants.length === 0) { - await updateChannels(getSimpleChannelsVariables(data, product)); - } else { - await updateChannels(getChannelsVariables(product, allChannels, data)); - } - } else { - if (!product.variants.length) { - const productVariantResult = await productVariantCreate({ - variables: { - input: { - attributes: - product.productType.variantAttributes?.map(attribute => ({ - id: attribute.id, - values: attribute.choices.edges.map(value => value.node.slug), - })) || [], - product: product.id, - sku: data.sku, - stocks: data.updateStocks.map(mapFormsetStockToStockInput), - }, - }, - }); - errors = [ - ...errors, - ...productVariantResult.data.productVariantCreate.errors, - ]; - - const variantId = - productVariantResult.data.productVariantCreate?.productVariant?.id; - - if (variantId) { - updateVariantChannels({ - variables: { - id: variantId, - input: getVariantChannelsInput(data), - }, - }); - - await updateChannels( - getChannelsVariables(product, allChannels, data), - ); - - const result = await updateSimpleProduct( - getSimpleProductVariables(productVariables, data, variantId), - ); - errors = [...errors, ...getSimpleProductErrors(result.data)]; - } - } else { - const result = await updateSimpleProduct( - getSimpleProductVariables( - productVariables, - data, - product.variants[0].id, - ), - ); - errors = [...errors, ...getSimpleProductErrors(result.data)]; - - await updateChannels(getSimpleChannelsVariables(data, product)); - - updateVariantChannels({ - variables: { - id: product.variants[0].id, - input: getVariantChannelsInput(data), - }, - }); - } - } - - return errors; - }; -} - export function createImageUploadHandler( id: string, createProductImage: (variables: ProductMediaCreateMutationVariables) => void, @@ -247,30 +43,37 @@ function areVariantsEqual(a: Node, b: Node) { export function createVariantReorderHandler< T extends { id: string; variants: any[] } ->( - product: T, - reorderProductVariants: ( - variables: ProductVariantReorderMutationVariables, - ) => void, -) { +>(product: T, reorderProductVariants: ProductVariantReorderMutationFn) { return ({ newIndex, oldIndex }: ReorderEvent) => { const oldVariantOrder = [...product.variants]; - product.variants = [ - ...move( - product.variants[oldIndex], - product!.variants, - areVariantsEqual, - newIndex, - ), - ]; - reorderProductVariants({ - move: { - id: oldVariantOrder[oldIndex].id, - sortOrder: newIndex - oldIndex, + variables: { + move: { + id: oldVariantOrder[oldIndex].id, + sortOrder: newIndex - oldIndex, + }, + productId: product.id, }, - productId: product.id, + optimisticResponse: () => ({ + __typename: "Mutation", + productVariantReorder: { + __typename: "ProductVariantReorder", + errors: [], + product: { + __typename: "Product", + id: product.id, + variants: [ + ...move( + product.variants[oldIndex], + product!.variants, + areVariantsEqual, + newIndex, + ), + ], + }, + }, + }), }); }; } diff --git a/src/products/views/ProductUpdate/handlers/useProductUpdateHandler.ts b/src/products/views/ProductUpdate/handlers/useProductUpdateHandler.ts new file mode 100644 index 000000000..fd65fcc8e --- /dev/null +++ b/src/products/views/ProductUpdate/handlers/useProductUpdateHandler.ts @@ -0,0 +1,241 @@ +import { FetchResult } from "@apollo/client"; +import { + mergeAttributeValueDeleteErrors, + mergeFileUploadErrors, +} from "@saleor/attributes/utils/data"; +import { + handleDeleteMultipleAttributeValues, + handleUploadMultipleFiles, +} from "@saleor/attributes/utils/handlers"; +import { + AttributeErrorFragment, + BulkProductErrorFragment, + MetadataErrorFragment, + ProductChannelListingErrorFragment, + ProductErrorFragment, + ProductErrorWithAttributesFragment, + ProductFragment, + UploadErrorFragment, + useAttributeValueDeleteMutation, + useFileUploadMutation, + useProductChannelListingUpdateMutation, + useProductUpdateMutation, + useProductVariantBulkCreateMutation, + useProductVariantBulkDeleteMutation, + useUpdateMetadataMutation, + useUpdatePrivateMetadataMutation, + useVariantDatagridChannelListingUpdateMutation, + useVariantDatagridStockUpdateMutation, + useVariantDatagridUpdateMutation, +} from "@saleor/graphql"; +import useNotifier from "@saleor/hooks/useNotifier"; +import { commonMessages } from "@saleor/intl"; +import { ProductUpdateSubmitData } from "@saleor/products/components/ProductUpdatePage/types"; +import { getVariantChannelsInputs } from "@saleor/products/components/ProductVariants/datagrid/getVariantChannelsInputs"; +import { + getStockInputs, + getStocks, + getVariantChannels, + getVariantInput, + getVariantInputs, +} from "@saleor/products/components/ProductVariants/utils"; +import { getProductErrorMessage } from "@saleor/utils/errors"; +import createMetadataUpdateHandler from "@saleor/utils/handlers/metadataUpdateHandler"; +import { useState } from "react"; +import { useIntl } from "react-intl"; + +import { getProductVariantListErrors, ProductVariantListError } from "./errors"; +import { + getProductChannelsUpdateVariables, + getProductUpdateVariables, +} from "./utils"; + +export type UseProductUpdateHandlerError = + | ProductErrorWithAttributesFragment + | ProductErrorFragment + | BulkProductErrorFragment + | AttributeErrorFragment + | UploadErrorFragment + | ProductChannelListingErrorFragment + | ProductVariantListError; + +type UseProductUpdateHandler = ( + data: ProductUpdateSubmitData, +) => Promise>; +interface UseProductUpdateHandlerOpts { + called: boolean; + loading: boolean; + errors: ProductErrorWithAttributesFragment[]; + variantListErrors: ProductVariantListError[]; + channelsErrors: ProductChannelListingErrorFragment[]; +} + +export function useProductUpdateHandler( + product: ProductFragment, +): [UseProductUpdateHandler, UseProductUpdateHandlerOpts] { + const intl = useIntl(); + const notify = useNotifier(); + const [variantListErrors, setVariantListErrors] = useState< + ProductVariantListError[] + >([]); + const [called, setCalled] = useState(false); + const [loading, setLoading] = useState(false); + + const [updateMetadata] = useUpdateMetadataMutation({}); + const [updatePrivateMetadata] = useUpdatePrivateMetadataMutation({}); + const [updateStocks] = useVariantDatagridStockUpdateMutation({}); + const [updateVariant] = useVariantDatagridUpdateMutation(); + const [createVariants] = useProductVariantBulkCreateMutation(); + const [deleteVariants] = useProductVariantBulkDeleteMutation(); + + const [uploadFile] = useFileUploadMutation(); + + const [updateProduct, updateProductOpts] = useProductUpdateMutation(); + const [ + updateChannels, + updateChannelsOpts, + ] = useProductChannelListingUpdateMutation({ + onCompleted: data => { + if (!!data.productChannelListingUpdate.errors.length) { + data.productChannelListingUpdate.errors.forEach(error => + notify({ + status: "error", + text: getProductErrorMessage(error, intl), + }), + ); + } + }, + }); + + const [ + updateVariantChannels, + ] = useVariantDatagridChannelListingUpdateMutation(); + + const [deleteAttributeValue] = useAttributeValueDeleteMutation(); + + const sendMutations = async ( + data: ProductUpdateSubmitData, + ): Promise => { + let errors: UseProductUpdateHandlerError[] = []; + const uploadFilesResult = await handleUploadMultipleFiles( + data.attributesWithNewFileValue, + variables => uploadFile({ variables }), + ); + + const deleteAttributeValuesResult = await handleDeleteMultipleAttributeValues( + data.attributesWithNewFileValue, + product?.attributes, + variables => deleteAttributeValue({ variables }), + ); + + errors = [ + ...errors, + ...mergeFileUploadErrors(uploadFilesResult), + ...mergeAttributeValueDeleteErrors(deleteAttributeValuesResult), + ]; + + if (data.variants.removed.length > 0) { + errors.push( + ...( + await deleteVariants({ + variables: { + ids: data.variants.removed.map( + index => product.variants[index].id, + ), + }, + }) + ).data.productVariantBulkDelete.errors, + ); + } + + const result = await updateProduct({ + variables: getProductUpdateVariables(product, data, uploadFilesResult), + }); + errors = [...errors, ...result.data.productUpdate.errors]; + + const productChannelsUpdateResult = await updateChannels({ + variables: getProductChannelsUpdateVariables(product, data), + }); + + const mutations: Array> = [ + ...getStocks(product.variants, data.variants).map(variables => + updateStocks({ variables }), + ), + ...getVariantInputs(product.variants, data.variants).map(variables => + updateVariant({ variables }), + ), + ...getVariantChannels(product.variants, data.variants).map(variables => + updateVariantChannels({ + variables, + }), + ), + ]; + + if (data.variants.added.length > 0) { + mutations.push( + createVariants({ + variables: { + id: product.id, + inputs: data.variants.added.map(index => ({ + ...getVariantInput(data.variants, index), + channelListings: getVariantChannelsInputs(data.variants, index), + stocks: getStockInputs(data.variants, index).stocks, + })), + }, + }), + ); + } + + const variantMutationResults = await Promise.all(mutations); + + const variantErrors = getProductVariantListErrors( + productChannelsUpdateResult, + variantMutationResults, + ); + + errors = [...errors, ...variantErrors]; + + setVariantListErrors(variantErrors); + + return errors; + }; + + const submit = async (data: ProductUpdateSubmitData) => { + setCalled(true); + setLoading(true); + + const errors = await createMetadataUpdateHandler( + product, + sendMutations, + variables => updateMetadata({ variables }), + variables => updatePrivateMetadata({ variables }), + )(data); + + setLoading(false); + + if (errors.length === 0) { + notify({ + status: "success", + text: intl.formatMessage(commonMessages.savedChanges), + }); + } + + return errors; + }; + + const errors = updateProductOpts.data?.productUpdate.errors ?? []; + + const channelsErrors = + updateChannelsOpts?.data?.productChannelListingUpdate?.errors ?? []; + + return [ + submit, + { + called, + loading, + channelsErrors, + errors, + variantListErrors, + }, + ]; +} diff --git a/src/products/views/ProductUpdate/handlers/utils.ts b/src/products/views/ProductUpdate/handlers/utils.ts index 8b5a9337d..46dee15b0 100644 --- a/src/products/views/ProductUpdate/handlers/utils.ts +++ b/src/products/views/ProductUpdate/handlers/utils.ts @@ -1,201 +1,137 @@ +import { FetchResult } from "@apollo/client"; +import { getAttributesAfterFileAttributesUpdate } from "@saleor/attributes/utils/data"; +import { prepareAttributesInput } from "@saleor/attributes/utils/handlers"; +import { VALUES_PAGINATE_BY } from "@saleor/config"; import { - ChannelData, - createSortedChannelsDataFromProduct, -} from "@saleor/channels/utils"; -import { + FileUploadMutation, ProductChannelListingAddInput, - ProductDetailsVariantFragment, + ProductChannelListingUpdateMutationVariables, ProductFragment, - ProductUpdateMutationVariables, - SimpleProductUpdateMutation, } from "@saleor/graphql"; -import { weight } from "@saleor/misc"; -import { getById } from "@saleor/orders/components/OrderReturnPage/utils"; -import { ProductUpdatePageSubmitData } from "@saleor/products/components/ProductUpdatePage"; -import { ProductUpdateSubmitData } from "@saleor/products/components/ProductUpdatePage/form"; -import { mapFormsetStockToStockInput } from "@saleor/products/utils/data"; -import { getAvailabilityVariables } from "@saleor/products/utils/handlers"; -import { arrayDiff } from "@saleor/utils/arrays"; -import isEqual from "lodash/isEqual"; +import { ProductUpdateSubmitData } from "@saleor/products/components/ProductUpdatePage/types"; +import { getColumnChannelAvailability } from "@saleor/products/components/ProductVariants/datagrid/columnData"; +import { getAttributeInputFromProduct } from "@saleor/products/utils/data"; +import { getParsedDataForJsonStringField } from "@saleor/utils/richText/misc"; +import pick from "lodash/pick"; +import uniq from "lodash/uniq"; -import { ChannelsWithVariantsData, ChannelWithVariantData } from "../types"; -import { getParsedChannelsWithVariantsDataFromChannels } from "../utils"; - -export const getSimpleProductVariables = ( - productVariables: ProductUpdateMutationVariables, - data: ProductUpdatePageSubmitData, - productId: string, -) => ({ - ...productVariables, - addStocks: data.addStocks.map(mapFormsetStockToStockInput), - deleteStocks: data.removeStocks, - input: { - ...productVariables.input, - weight: weight(data.weight), - }, - productVariantId: productId, - productVariantInput: { - sku: data.sku, - trackInventory: data.trackInventory, - preorder: data.isPreorder - ? { - globalThreshold: data.globalThreshold - ? parseInt(data.globalThreshold, 10) - : null, - endDate: data.preorderEndDateTime, - } - : undefined, - }, - updateStocks: data.updateStocks.map(mapFormsetStockToStockInput), -}); - -export const getSimpleProductErrors = (data: SimpleProductUpdateMutation) => [ - ...data.productUpdate.errors, - ...data.productVariantStocksCreate.errors, - ...data.productVariantStocksDelete.errors, - ...data.productVariantStocksUpdate.errors, -]; - -export const getChannelListingBaseInputData = ({ - id: channelId, - isPublished, - publicationDate, - isAvailableForPurchase, - availableForPurchase, - visibleInListings, -}: ChannelData) => ({ - channelId, - isPublished, - publicationDate, - visibleInListings, - isAvailableForPurchase, - availableForPurchaseDate: availableForPurchase, -}); - -export const getChannelListingUpdateInputFromData = ( - { variantsIdsToAdd, variantsIdsToRemove }: ChannelWithVariantData, - { selectedVariantsIds: initialSelectedVariantsIds }: ChannelWithVariantData, - basicChannelData: ChannelData, -) => ({ - ...getChannelListingBaseInputData(basicChannelData), - addVariants: arrayDiff(initialSelectedVariantsIds, variantsIdsToAdd).added, - removeVariants: variantsIdsToRemove, -}); - -const getParsedChannelsData = ( - channelsWithVariants: ChannelsWithVariantsData, - initialChannelWithVariants: ChannelsWithVariantsData, - channelsData: ChannelData[], -): ProductChannelListingAddInput[] => - channelsData.map(({ id, ...rest }) => - getChannelListingUpdateInputFromData( - channelsWithVariants[id], - initialChannelWithVariants[id], - { id, ...rest }, - ), - ); - -const shouldRemoveChannel = (allVariants: ProductDetailsVariantFragment[]) => ({ - removeVariants, -}: ProductChannelListingAddInput) => - isRemovingAllVariants(allVariants, removeVariants); - -const isRemovingAllVariants = ( - allVariants: ProductDetailsVariantFragment[], - removeVariants: string[], -) => !!removeVariants.length && removeVariants.length === allVariants.length; - -const shouldUpdateChannel = ( - initialChannelWithVariantData, - allVariants: ProductDetailsVariantFragment[], - allChannels: ChannelData[], -) => ({ - removeVariants, - addVariants, - channelId, - ...rest -}: ProductChannelListingAddInput) => { - const initialDataInput = getChannelListingUpdateInputFromData( - initialChannelWithVariantData[channelId], - initialChannelWithVariantData[channelId], - allChannels.find(getById(channelId)), - ); - - const hasDataChanged = !isEqual( - { removeVariants, addVariants, channelId, ...rest }, - initialDataInput, - ); - - const isRemovingChannel = isRemovingAllVariants(allVariants, removeVariants); - - return hasDataChanged && !isRemovingChannel; -}; - -export const getChannelsVariables = ( - { id, variants }: ProductFragment, - allChannels: ChannelData[], - { channelsWithVariants, channelsData }: ProductUpdateSubmitData, -) => { - const initialChannelWithVariants = getParsedChannelsWithVariantsDataFromChannels( - channelsData, - ); - - const channelsToBeUpdated = getParsedChannelsData( - channelsWithVariants, - initialChannelWithVariants, - channelsData, - ).filter( - shouldUpdateChannel(initialChannelWithVariants, variants, allChannels), - ); - - const channelsIdsToBeRemoved = getParsedChannelsData( - channelsWithVariants, - initialChannelWithVariants, - channelsData, - ) - .filter(shouldRemoveChannel(variants)) - .map(({ channelId }) => channelId); - - return { - variables: { - id, - input: { - updateChannels: channelsToBeUpdated, - removeChannels: channelsIdsToBeRemoved, - }, - }, - }; -}; - -export const getSimpleChannelsVariables = ( - data: ProductUpdatePageSubmitData, +export function getProductUpdateVariables( product: ProductFragment, -) => { - const productChannels = createSortedChannelsDataFromProduct(product); - const existingChannelIDs = productChannels.map(channel => channel.id); - const modifiedChannelIDs = data.channelListings.map(channel => channel.id); - - const removedChannelIDs = existingChannelIDs.filter( - x => !modifiedChannelIDs.includes(x), + data: ProductUpdateSubmitData, + uploadFilesResult: Array>, +) { + const updatedFileAttributes = getAttributesAfterFileAttributesUpdate( + data.attributesWithNewFileValue, + uploadFilesResult, ); return { - variables: { - id: product.id, - input: { - updateChannels: getAvailabilityVariables(data.channelListings), - removeChannels: removedChannelIDs, + id: product.id, + input: { + attributes: prepareAttributesInput({ + attributes: data.attributes, + prevAttributes: getAttributeInputFromProduct(product), + updatedFileAttributes, + }), + category: data.category, + chargeTaxes: data.chargeTaxes, + collections: data.collections, + description: getParsedDataForJsonStringField(data.description), + name: data.name, + rating: data.rating, + seo: { + description: data.seoDescription, + title: data.seoTitle, }, + slug: data.slug, + taxCode: data.changeTaxCode ? data.taxCode : null, }, + firstValues: VALUES_PAGINATE_BY, }; +} + +const hasChannel = ( + channelId: string, + variant?: ProductFragment["variants"][number], +) => { + if (!variant) { + return false; + } + + return variant.channelListings.some(c => c.channel.id === channelId); }; -export const getVariantChannelsInput = ({ - channelListings, -}: ProductUpdatePageSubmitData) => - channelListings.map(listing => ({ - channelId: listing.id, - costPrice: listing.costPrice || null, - price: listing.price, - preorderThreshold: listing.preorderThreshold, - })); +export function getProductChannelsUpdateVariables( + product: ProductFragment, + data: ProductUpdateSubmitData, +): ProductChannelListingUpdateMutationVariables { + const channels = uniq([ + ...product.channelListings.map(listing => listing.channel.id), + ...data.channels.updateChannels.map(listing => listing.channelId), + ]); + + const dataUpdated = new Map(); + data.channels.updateChannels + .map(listing => + pick( + listing, + // Filtering it here so we send only fields defined in input schema + [ + "availableForPurchaseAt", + "availableForPurchaseDate", + "channelId", + "isAvailableForPurchase", + "isPublished", + "publicationDate", + "publishedAt", + "visibleInListings", + ] as Array, + ), + ) + .forEach(listing => dataUpdated.set(listing.channelId, listing)); + + const variantsUpdates = new Map(); + channels + .map(channelId => ({ + channelId, + addVariants: data.variants.updates + .filter( + change => + !data.variants.added.includes(change.row) && + !hasChannel(channelId, product.variants[change.row]) && + channelId === getColumnChannelAvailability(change.column) && + change.data, + ) + .map(change => product.variants[change.row].id), + removeVariants: data.variants.updates + .filter( + change => + product.variants[change.row] && + channelId === getColumnChannelAvailability(change.column) && + !change.data, + ) + .map(change => product.variants[change.row].id), + })) + .filter( + listing => + listing.addVariants.length > 0 || listing.removeVariants.length > 0, + ) + .forEach(listing => variantsUpdates.set(listing.channelId, listing)); + + const updateChannels = channels + .filter( + channelId => dataUpdated.has(channelId) || variantsUpdates.has(channelId), + ) + .map(channelId => ({ + ...dataUpdated.get(channelId), + ...variantsUpdates.get(channelId), + })); + + return { + id: product.id, + input: { + ...data.channels, + updateChannels, + }, + }; +} diff --git a/src/products/views/ProductUpdate/types.ts b/src/products/views/ProductUpdate/types.ts deleted file mode 100644 index 8fc875f28..000000000 --- a/src/products/views/ProductUpdate/types.ts +++ /dev/null @@ -1,19 +0,0 @@ -export interface ChannelWithVariantData { - selectedVariantsIds: string[]; - variantsIdsToRemove: string[]; - variantsIdsToAdd: string[]; -} - -export type ChannelVariantListing = Record; -export type ChannelsWithVariantsData = Record; - -export interface UseChannelsWithProductVariants { - addVariantToChannel: (channelId: string, variantId: string) => void; - removeVariantFromChannel: (channelId: string, variantId: string) => void; - channelsWithVariantsData: ChannelsWithVariantsData; - toggleAllChannels: () => void; - toggleAllChannelVariants: (channelId: string) => void; - channelVariantListing: ChannelVariantListing; - setChannelVariantListing: (listings: ChannelVariantListing) => void; - reset: () => void; -} diff --git a/src/products/views/ProductUpdate/useChannelVariantListings.ts b/src/products/views/ProductUpdate/useChannelVariantListings.ts deleted file mode 100644 index d6938db71..000000000 --- a/src/products/views/ProductUpdate/useChannelVariantListings.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { ChannelData } from "@saleor/channels/utils"; -import useStateFromProps from "@saleor/hooks/useStateFromProps"; -import { arrayDiff } from "@saleor/utils/arrays"; -import { useMemo } from "react"; - -import { ChannelsWithVariantsData } from "./types"; -import { createFromChannels, createUpdatedChannels } from "./utils"; - -function useChannelVariantListings(channels: ChannelData[]) { - const initialChannelVariantListing = useMemo( - () => createFromChannels(channels, ({ variantsIds }) => variantsIds), - [channels], - ); - - const [ - updatedChannelVariantListing, - setUpdatedChannelVariantListing, - ] = useStateFromProps(initialChannelVariantListing); - - const channelsWithVariantsData = useMemo( - () => - createFromChannels(channels, channel => { - const diff = arrayDiff( - initialChannelVariantListing[channel.id], - updatedChannelVariantListing[channel.id], - ); - - return { - selectedVariantsIds: updatedChannelVariantListing[channel.id], - variantsIdsToAdd: diff.added, - variantsIdsToRemove: diff.removed, - }; - }), - [updatedChannelVariantListing], - ); - - const reset = () => - setUpdatedChannelVariantListing(initialChannelVariantListing); - - const updatedChannels: ChannelData[] = useMemo( - () => createUpdatedChannels(channels, updatedChannelVariantListing), - [channels, updatedChannelVariantListing], - ); - - return { - channels: updatedChannels, - channelsWithVariantsData, - channelVariantListing: updatedChannelVariantListing, - setChannelVariantListing: setUpdatedChannelVariantListing, - reset, - }; -} - -export default useChannelVariantListings; diff --git a/src/products/views/ProductUpdate/useChannelsWithProductVariants.test.ts b/src/products/views/ProductUpdate/useChannelsWithProductVariants.test.ts deleted file mode 100644 index 5f2c090c4..000000000 --- a/src/products/views/ProductUpdate/useChannelsWithProductVariants.test.ts +++ /dev/null @@ -1,178 +0,0 @@ -import { ChannelData } from "@saleor/channels/utils"; -import { act, renderHook } from "@testing-library/react-hooks"; - -import useChannelsWithProductVariants from "./useChannelsWithProductVariants"; - -const channels: ChannelData[] = [ - { - id: "channel1", - name: "Channel 1", - variantsIds: ["variant1", "variant2"], - }, - { - id: "channel2", - name: "Channel 2", - variantsIds: [], - }, -]; - -const variants = ["variant1", "variant2", "variant3", "variant4", "variant5"]; - -const setupHook = () => - renderHook(() => useChannelsWithProductVariants(channels, variants)); - -describe("useChannelsWithProductVariants", () => { - it("properly initializes state", () => { - const { result } = setupHook(); - - expect( - result.current.channelsWithVariantsData.channel1.selectedVariantsIds, - ).toHaveLength(2); - expect( - result.current.channelsWithVariantsData.channel1.variantsIdsToAdd, - ).toHaveLength(0); - expect( - result.current.channelsWithVariantsData.channel1.variantsIdsToRemove, - ).toHaveLength(0); - - expect( - result.current.channelsWithVariantsData.channel2.selectedVariantsIds, - ).toHaveLength(0); - expect( - result.current.channelsWithVariantsData.channel2.variantsIdsToAdd, - ).toHaveLength(0); - expect( - result.current.channelsWithVariantsData.channel2.variantsIdsToRemove, - ).toHaveLength(0); - }); - - it("properly adds variants", () => { - const { result } = setupHook(); - - act(() => result.current.addVariantToChannel("channel1", "variant3")); - - expect( - result.current.channelsWithVariantsData.channel1.selectedVariantsIds, - ).toHaveLength(3); - expect( - result.current.channelsWithVariantsData.channel1.variantsIdsToAdd, - ).toHaveLength(1); - expect( - result.current.channelsWithVariantsData.channel1.variantsIdsToRemove, - ).toHaveLength(0); - - expect( - result.current.channelsWithVariantsData.channel2.selectedVariantsIds, - ).toHaveLength(0); - expect( - result.current.channelsWithVariantsData.channel2.variantsIdsToAdd, - ).toHaveLength(0); - expect( - result.current.channelsWithVariantsData.channel2.variantsIdsToRemove, - ).toHaveLength(0); - }); - - it("properly removes variants", () => { - const { result } = setupHook(); - - act(() => result.current.removeVariantFromChannel("channel1", "variant2")); - - expect( - result.current.channelsWithVariantsData.channel1.selectedVariantsIds, - ).toHaveLength(1); - expect( - result.current.channelsWithVariantsData.channel1.variantsIdsToAdd, - ).toHaveLength(0); - expect( - result.current.channelsWithVariantsData.channel1.variantsIdsToRemove, - ).toHaveLength(1); - - expect( - result.current.channelsWithVariantsData.channel2.selectedVariantsIds, - ).toHaveLength(0); - expect( - result.current.channelsWithVariantsData.channel2.variantsIdsToAdd, - ).toHaveLength(0); - expect( - result.current.channelsWithVariantsData.channel2.variantsIdsToRemove, - ).toHaveLength(0); - }); - - it("properly toggles all variants in channel", () => { - const { result } = setupHook(); - - // Deselect all because it's partially selected - act(() => result.current.toggleAllChannelVariants("channel1")); - - expect( - result.current.channelsWithVariantsData.channel1.selectedVariantsIds, - ).toHaveLength(0); - expect( - result.current.channelsWithVariantsData.channel1.variantsIdsToAdd, - ).toHaveLength(0); - expect( - result.current.channelsWithVariantsData.channel1.variantsIdsToRemove, - ).toHaveLength(2); - - // Select all - act(() => result.current.toggleAllChannelVariants("channel1")); - - expect( - result.current.channelsWithVariantsData.channel1.selectedVariantsIds, - ).toHaveLength(5); - expect( - result.current.channelsWithVariantsData.channel1.variantsIdsToAdd, - ).toHaveLength(3); - expect( - result.current.channelsWithVariantsData.channel1.variantsIdsToRemove, - ).toHaveLength(0); - }); - - it("properly toggles all", () => { - const { result } = setupHook(); - - // Select all - act(result.current.toggleAllChannels); - - expect( - result.current.channelsWithVariantsData.channel1.selectedVariantsIds, - ).toHaveLength(5); - expect( - result.current.channelsWithVariantsData.channel1.variantsIdsToAdd, - ).toHaveLength(3); - expect( - result.current.channelsWithVariantsData.channel1.variantsIdsToRemove, - ).toHaveLength(0); - expect( - result.current.channelsWithVariantsData.channel2.selectedVariantsIds, - ).toHaveLength(5); - expect( - result.current.channelsWithVariantsData.channel2.variantsIdsToAdd, - ).toHaveLength(5); - expect( - result.current.channelsWithVariantsData.channel2.variantsIdsToRemove, - ).toHaveLength(0); - - // Deselect all - act(result.current.toggleAllChannels); - - expect( - result.current.channelsWithVariantsData.channel1.selectedVariantsIds, - ).toHaveLength(0); - expect( - result.current.channelsWithVariantsData.channel1.variantsIdsToAdd, - ).toHaveLength(0); - expect( - result.current.channelsWithVariantsData.channel1.variantsIdsToRemove, - ).toHaveLength(2); - expect( - result.current.channelsWithVariantsData.channel2.selectedVariantsIds, - ).toHaveLength(0); - expect( - result.current.channelsWithVariantsData.channel2.variantsIdsToAdd, - ).toHaveLength(0); - expect( - result.current.channelsWithVariantsData.channel2.variantsIdsToRemove, - ).toHaveLength(0); - }); -}); diff --git a/src/products/views/ProductUpdate/useChannelsWithProductVariants.ts b/src/products/views/ProductUpdate/useChannelsWithProductVariants.ts deleted file mode 100644 index 9f948788b..000000000 --- a/src/products/views/ProductUpdate/useChannelsWithProductVariants.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { ChannelData } from "@saleor/channels/utils"; -import uniq from "lodash/uniq"; - -import { UseChannelsWithProductVariants } from "./types"; -import useChannelVariantListings from "./useChannelVariantListings"; -import { - addAllVariantsToAllChannels, - getChannelVariantToggleData, -} from "./utils"; - -const useChannelsWithProductVariants = ( - channels: ChannelData[], - variants: string[], -): UseChannelsWithProductVariants => { - const { - channelsWithVariantsData, - setChannelVariantListing, - channelVariantListing, - reset, - } = useChannelVariantListings(channels); - - const handleAddVariant = (channelId: string, variantId: string) => - setChannelVariantListing(listings => ({ - ...listings, - [channelId]: uniq([...listings[channelId], variantId]), - })); - - const handleRemoveVariant = (channelId: string, variantId: string) => - setChannelVariantListing(listings => ({ - ...listings, - [channelId]: listings[channelId].filter( - selectedVariantId => selectedVariantId !== variantId, - ), - })); - - const toggleAllChannelVariants = (channelId: string) => { - const isChannelSelected = channelVariantListing[channelId].length > 0; - - setChannelVariantListing({ - ...channelVariantListing, - [channelId]: getChannelVariantToggleData(variants, isChannelSelected), - }); - }; - - const toggleAllChannels = () => - setChannelVariantListing(listings => - addAllVariantsToAllChannels(listings, variants), - ); - - return { - channelsWithVariantsData, - channelVariantListing, - addVariantToChannel: handleAddVariant, - removeVariantFromChannel: handleRemoveVariant, - toggleAllChannelVariants, - toggleAllChannels, - setChannelVariantListing, - reset, - }; -}; - -export default useChannelsWithProductVariants; diff --git a/src/products/views/ProductUpdate/utils.ts b/src/products/views/ProductUpdate/utils.ts deleted file mode 100644 index 6d2ec5235..000000000 --- a/src/products/views/ProductUpdate/utils.ts +++ /dev/null @@ -1,122 +0,0 @@ -import { ChannelData } from "@saleor/channels/utils"; -import every from "lodash/every"; -import reduce from "lodash/reduce"; - -import { initialChannelWithVariantData } from "./consts"; -import { - ChannelsWithVariantsData, - ChannelVariantListing, - ChannelWithVariantData, -} from "./types"; - -export function createFromChannels( - channels: ChannelData[], - cb: (channel: ChannelData) => T, -): Record { - return channels?.reduce( - (result: Record, channel) => ({ - ...result, - [channel.id]: cb(channel), - }), - {}, - ); -} - -export function createUpdatedChannels( - channels: ChannelData[], - listing: ChannelVariantListing, -): ChannelData[] { - return reduce( - listing, - (acc, variantsIds, channelId) => [ - ...acc, - { - id: channelId, - name: channels.find(channel => channel.id === channelId).name, - variantsIds, - } as ChannelData, - ], - [], - ); -} - -export const getParsedChannelsWithVariantsDataFromChannels = ( - channels: ChannelData[], -): ChannelsWithVariantsData => - createFromChannels( - channels, - ({ variantsIds }) => - ({ - ...initialChannelWithVariantData, - selectedVariantsIds: variantsIds, - } as ChannelWithVariantData), - ); - -export const getChannelVariantToggleData = ( - variants: string[], - isSelected: boolean, -): string[] => (isSelected ? [] : variants); - -export const areAllVariantsAtAllChannelsSelected = ( - variants: string[] = [], - channelsWithVariants: ChannelVariantListing = {}, -) => - every(channelsWithVariants, channelWithVariantsData => - areAllChannelVariantsSelected(variants, channelWithVariantsData), - ); - -export const areAllChannelVariantsSelected = ( - variants: string[] = [], - selectedVariants: string[], -) => selectedVariants.length === variants.length; - -export const areAnyChannelVariantsSelected = ( - channelsWithVariantsData: ChannelWithVariantData, -) => channelsWithVariantsData?.selectedVariantsIds.length > 0; - -export const getTotalSelectedChannelsCount = ( - channelsWithVariantsData: ChannelsWithVariantsData, -) => - Object.values(channelsWithVariantsData).filter( - channel => channel.selectedVariantsIds.length > 0, - ).length; - -export const addAllVariantsToAllChannels = ( - listings: ChannelVariantListing, - variants: string[], -): ChannelVariantListing => { - const areAllChannelsSelected = areAllVariantsAtAllChannelsSelected( - variants, - listings, - ); - - const updatedListing = reduce( - listings, - (result: ChannelVariantListing, _, channelId) => ({ - ...result, - [channelId]: getChannelVariantToggleData( - variants, - areAllChannelsSelected, - ), - }), - {}, - ); - - return updatedListing; -}; - -export const channelVariantListingDiffToDict = ( - listing: ChannelsWithVariantsData, -): ChannelVariantListing => - reduce( - listing, - ( - listingDict: ChannelVariantListing, - { selectedVariantsIds }, - channelId, - ) => ({ - ...listingDict, - [channelId]: selectedVariantsIds, - }), - {}, - ); diff --git a/src/products/views/ProductVariant/ProductVariant.tsx b/src/products/views/ProductVariant/ProductVariant.tsx index 92b6cef5f..a0472f98c 100644 --- a/src/products/views/ProductVariant/ProductVariant.tsx +++ b/src/products/views/ProductVariant/ProductVariant.tsx @@ -167,7 +167,7 @@ export const ProductVariant: React.FC = ({ const handleVariantReorder = createVariantReorderHandler( variant?.product, - variables => reorderProductVariants({ variables }), + reorderProductVariants, ); const disableFormSave = diff --git a/src/products/views/ProductVariantCreate.tsx b/src/products/views/ProductVariantCreate.tsx index 2387e20aa..6e474348a 100644 --- a/src/products/views/ProductVariantCreate.tsx +++ b/src/products/views/ProductVariantCreate.tsx @@ -96,8 +96,9 @@ export const ProductVariant: React.FC = ({ reorderProductVariantsOpts, ] = useProductVariantReorderMutation({}); - const handleVariantReorder = createVariantReorderHandler(product, variables => - reorderProductVariants({ variables }), + const handleVariantReorder = createVariantReorderHandler( + product, + reorderProductVariants, ); const handleCreate = async (formData: ProductVariantCreateData) => { diff --git a/src/searches/useAttributeValueSearch.ts b/src/searches/useAttributeValueSearch.ts index 21cfd0e4b..55ed56e66 100644 --- a/src/searches/useAttributeValueSearch.ts +++ b/src/searches/useAttributeValueSearch.ts @@ -1,10 +1,11 @@ -import { gql } from "@apollo/client"; +import { gql, useApolloClient } from "@apollo/client"; import { SearchAttributeValuesDocument, SearchAttributeValuesQuery, SearchAttributeValuesQueryVariables, } from "@saleor/graphql"; import makeSearch from "@saleor/hooks/makeSearch"; +import { mapEdgesToItems } from "@saleor/utils/maps"; export const searchAttributeValues = gql` query SearchAttributeValues( @@ -29,6 +30,27 @@ export const searchAttributeValues = gql` } `; +export function useSearchAttributeValuesSuggestions() { + const client = useApolloClient(); + + return (id: string, query: string) => + client + .query({ + query: SearchAttributeValuesDocument, + variables: { + id, + first: 10, + query, + }, + }) + .then(({ data }) => + mapEdgesToItems(data.attribute.choices).map(({ name, slug }) => ({ + label: name, + value: slug, + })), + ); +} + export default makeSearch< SearchAttributeValuesQuery, SearchAttributeValuesQueryVariables diff --git a/src/storybook/Stories.test.ts b/src/storybook/Stories.test.ts index dc822ea2c..52682e2c3 100644 --- a/src/storybook/Stories.test.ts +++ b/src/storybook/Stories.test.ts @@ -4,6 +4,7 @@ import initStoryshots from "@storybook/addon-storyshots"; import { configure, render } from "enzyme"; import Adapter from "enzyme-adapter-react-16"; import toJSON from "enzyme-to-json"; +import { mockRandomForEach } from "jest-mock-random"; import React from "react"; // Fixes useLayoutEffect warnings @@ -19,6 +20,10 @@ jest.mock("@material-ui/styles/createGenerateClassName"); initStoryshots({ configPath: "src/storybook/", test({ story }) { + mockRandomForEach( + Array.from(Array(25).keys()).map(x => parseFloat(`0.${x + 1}`)), + ); + const result = render(story.render() as any); expect(toJSON(result)).toMatchSnapshot(); }, diff --git a/src/storybook/__snapshots__/Stories.test.ts.snap b/src/storybook/__snapshots__/Stories.test.ts.snap index 0e18872b3..0105ed6f8 100644 --- a/src/storybook/__snapshots__/Stories.test.ts.snap +++ b/src/storybook/__snapshots__/Stories.test.ts.snap @@ -101,7 +101,7 @@ exports[`Storyshots Attributes / Attributes default 1`] = ` aria-expanded="false" aria-haspopup="listbox" aria-labelledby="downshift-16-label" - class="SingleAutocompleteSelectField-container-id" + class="SingleAutocompleteSelectField-container-id click-outside-ignore" role="combobox" >
-
- Set publication date -

@@ -5418,7 +4641,7 @@ exports[`Storyshots Generics / Channels availability card with onChange 1`] = ` class="MuiFormControl-root-id RadioSwitchField-formControl-id ChannelsAvailabilityCard-radioField-id" >
@@ -5434,7 +4657,7 @@ exports[`Storyshots Generics / Channels availability card with onChange 1`] = ` > @@ -5478,7 +4701,7 @@ exports[`Storyshots Generics / Channels availability card with onChange 1`] = ` @@ -5536,7 +4759,7 @@ exports[`Storyshots Generics / Channels availability card with onChange 1`] = ` `; +exports[`Storyshots Generics / Datagrid default 1`] = ` +
+
+
+
+
+ + Datagrid + +
+
+
+ +
+
+
+
+
+
+
+
+
+ + + + +
+
+ +
+
+
+
+
+
+
+
+
+`; + +exports[`Storyshots Generics / Datagrid with errors 1`] = ` +
+
+
+
+
+ + Datagrid + +
+
+
+ +
+
+
+
+
+
+
+
+
+ + + + +
+
+ +
+
+
+
+
+
+
+
+
+`; + exports[`Storyshots Generics / Date default 1`] = `
- Not published -
+ />
- Not published -
+ />
- Not published -
+ />
- Not published -
+ />
- Not published -
+ />
- Not published -
+ />
- Not published -
+ />
- Not published -
+ />
- Not published -
+ />
- Not published -
+ />
- Not published -
+ />
- Not published -
+ />
- Not published -
+ />
- Not published -
+ />
- Not published -
+ />
- Not published -
+ />
- Not published -
+ />
- Not published -
+ />
- Not published -
+ />
- euro + EUR
- euro + EUR
- euro + EUR
- euro + EUR
- euro + EUR
- euro + EUR
- euro + EUR
- euro + EUR
- euro + EUR
- euro + EUR
- euro + EUR
- euro + EUR
- euro + EUR
- euro + EUR
- euro + EUR
- euro + EUR
- euro + EUR
- euro + EUR
- euro + EUR
- euro + EUR
- euro + EUR
- euro + EUR
- euro + EUR
- euro + EUR
- euro + EUR
- euro + EUR
- euro + EUR
- euro + EUR
- euro + EUR
- euro + EUR
- euro + EUR
- euro + EUR
- euro + EUR
- euro + EUR