diff --git a/cypress/apiRequests/Product.js b/cypress/apiRequests/Product.js index a2864d4fb..6e6a102d1 100644 --- a/cypress/apiRequests/Product.js +++ b/cypress/apiRequests/Product.js @@ -42,6 +42,8 @@ export function updateProduct(productId, input) { export function updateChannelInProduct({ productId, channelId, + variantsIdsToAdd = "[]", + variantsIdsToRemove = "[]", isPublished = true, isAvailableForPurchase = true, visibleInListings = true @@ -49,11 +51,13 @@ export function updateChannelInProduct({ const mutation = `mutation{ productChannelListingUpdate(id:"${productId}", input:{ - addChannels:{ + updateChannels:{ channelId:"${channelId}" isPublished:${isPublished} isAvailableForPurchase:${isAvailableForPurchase} visibleInListings:${visibleInListings} + addVariants:${variantsIdsToAdd} + removeVariants:${variantsIdsToRemove} } }){ product{ diff --git a/cypress/apiRequests/ShippingMethod.js b/cypress/apiRequests/ShippingMethod.js index 03140bddc..7afb51c10 100644 --- a/cypress/apiRequests/ShippingMethod.js +++ b/cypress/apiRequests/ShippingMethod.js @@ -13,11 +13,12 @@ export function createShippingRate(name, shippingZone) { return cy.sendRequestWithQuery(mutation); } -export function createShippingZone(name, country) { +export function createShippingZone(name, country, channelId) { const mutation = `mutation{ shippingZoneCreate(input:{ name: "${name}" countries: "${country}" + addChannels:["${channelId}"] }){ shippingZone{ id @@ -27,7 +28,19 @@ export function createShippingZone(name, country) { }`; return cy.sendRequestWithQuery(mutation); } - +export function addChannelToShippingZone(shippingZoneId, channelId) { + const mutation = `mutation addCh{ + shippingZoneUpdate(id:"${shippingZoneId}", input:{ + addChannels:["${channelId}"] + }){ + shippingErrors{ + field + message + } + } + }`; + return cy.sendRequestWithQuery(mutation); +} export function addChannelToShippingMethod(shippingRateId, channelId, price) { const mutation = `mutation{ shippingMethodChannelListingUpdate(id:"${shippingRateId}", input:{ diff --git a/cypress/elements/channels/available-channels-form.js b/cypress/elements/channels/available-channels-form.js index e7be964b5..c3316ad6a 100644 --- a/cypress/elements/channels/available-channels-form.js +++ b/cypress/elements/channels/available-channels-form.js @@ -1,6 +1,6 @@ export const AVAILABLE_CHANNELS_FORM = { menageChannelsButton: "[data-test-id='channels-availiability-manage-button']", - assignedChannels: "[data-test='channel-availability-item']", + assignedChannels: "[class*=expandIcon]", publishedRadioButtons: "[name*='isPublished']", availableForPurchaseRadioButtons: "[name*='isAvailableForPurchase']", radioButtonsValueTrue: "[value='true']", diff --git a/cypress/elements/shipping/shipping-zone-details.js b/cypress/elements/shipping/shipping-zone-details.js index 854c01601..249dac3ae 100644 --- a/cypress/elements/shipping/shipping-zone-details.js +++ b/cypress/elements/shipping/shipping-zone-details.js @@ -7,6 +7,7 @@ export const SHIPPING_ZONE_DETAILS = { addWeightRateButton: "[data-test-id='add-weight-rate']", nameInput: "[name='name']", shippingRatePriceTableCell: "[data-test-id='shipping-rate-price']", - warehouseOption: "[data-test='multiautocomplete-select-option']", - warehouseSelector: "[placeholder*='Warehouse']" + option: "[data-test='multiautocomplete-select-option']", + warehouseSelector: "[placeholder*='Warehouse']", + channelSelector: "[placeholder*='Channel']" }; diff --git a/cypress/integration/configuration/shippingMethod.js b/cypress/integration/configuration/shippingMethod.js index b32608654..f5443f01a 100644 --- a/cypress/integration/configuration/shippingMethod.js +++ b/cypress/integration/configuration/shippingMethod.js @@ -1,7 +1,10 @@ // import faker from "faker"; -import { addChannelToShippingMethod } from "../../apiRequests/ShippingMethod"; +import { + addChannelToShippingMethod, + addChannelToShippingZone +} from "../../apiRequests/ShippingMethod"; import { SHIPPING_ZONE_DETAILS } from "../../elements/shipping/shipping-zone-details"; import { selectChannelInHeader } from "../../steps/channelsSteps"; import { @@ -104,10 +107,14 @@ describe("Shipping methods", () => { }) => { shippingZone = shippingZoneResp; shippingMethod = shippingMethodResp; - addChannelToShippingMethod( - shippingMethod.id, - createdChannel.id, - createdChannelPrice + addChannelToShippingZone(shippingZone.id, createdChannel.id).then( + () => { + addChannelToShippingMethod( + shippingMethod.id, + createdChannel.id, + createdChannelPrice + ); + } ); } ) @@ -143,7 +150,12 @@ describe("Shipping methods", () => { it("should create price based shipping method", () => { const shippingName = `${startsWith}${faker.random.number()}`; - createShippingZone(shippingName, warehouse.name, plAddress.countryFullName); + createShippingZone( + shippingName, + warehouse.name, + plAddress.countryFullName, + defaultChannel.name + ); createShippingRate(shippingName, price, rateOptions.PRICE_OPTION); createCheckout({ @@ -164,7 +176,12 @@ describe("Shipping methods", () => { it("should create weight based shipping method", () => { const shippingName = `${startsWith}${faker.random.number()}`; - createShippingZone(shippingName, warehouse.name, plAddress.countryFullName); + createShippingZone( + shippingName, + warehouse.name, + plAddress.countryFullName, + defaultChannel.name + ); createShippingRate(shippingName, price, rateOptions.WEIGHT_OPTION); createCheckout({ channelSlug: defaultChannel.slug, diff --git a/cypress/integration/products/productsList/filteringProducts.js b/cypress/integration/products/productsList/filteringProducts.js index dc2083b4b..14a2681eb 100644 --- a/cypress/integration/products/productsList/filteringProducts.js +++ b/cypress/integration/products/productsList/filteringProducts.js @@ -93,7 +93,7 @@ describe("Products", () => { }); }); - it("should filter products out of stock", () => { + xit("should filter products out of stock", () => { const productOutOfStock = `${startsWith}${faker.random.number()}`; createProductInChannel({ name: productOutOfStock, diff --git a/cypress/screenshots/channels.js/Channels -- should navigate to channels page -- before all hook (failed).png b/cypress/screenshots/channels.js/Channels -- should navigate to channels page -- before all hook (failed).png new file mode 100644 index 000000000..9a915910b Binary files /dev/null and b/cypress/screenshots/channels.js/Channels -- should navigate to channels page -- before all hook (failed).png differ diff --git a/cypress/steps/collectionsSteps.js b/cypress/steps/collectionsSteps.js index 4e2314369..0799ddaa7 100644 --- a/cypress/steps/collectionsSteps.js +++ b/cypress/steps/collectionsSteps.js @@ -1,5 +1,6 @@ import { COLLECTION_SELECTORS } from "../elements/catalog/collection-selectors"; import { ASSIGN_PRODUCTS_SELECTORS } from "../elements/catalog/products/assign-products-selectors"; +import { PRODUCT_DETAILS } from "../elements/catalog/products/product-details"; import { AVAILABLE_CHANNELS_FORM } from "../elements/channels/available-channels-form"; import { SELECT_CHANNELS_TO_ASSIGN } from "../elements/channels/select-channels-to-assign"; import { BUTTON_SELECTORS } from "../elements/shared/button-selectors"; @@ -28,6 +29,7 @@ export function createCollection(collectionName, isPublished, channel) { .click(); cy.addAliasToGraphRequest("CreateCollection"); cy.get(COLLECTION_SELECTORS.saveButton).click(); + cy.get(PRODUCT_DETAILS.confirmationMsg).should("be.visible"); return cy .wait("@CreateCollection") .its("response.body.data.collectionCreate.collection"); diff --git a/cypress/steps/shippingMethodSteps.js b/cypress/steps/shippingMethodSteps.js index 915340384..eb77a7045 100644 --- a/cypress/steps/shippingMethodSteps.js +++ b/cypress/steps/shippingMethodSteps.js @@ -3,7 +3,12 @@ import { SHIPPING_RATE_DETAILS } from "../elements/shipping/shipping-rate-detail import { SHIPPING_ZONE_DETAILS } from "../elements/shipping/shipping-zone-details"; import { SHIPPING_ZONES_LIST } from "../elements/shipping/shipping-zones-list"; -export function createShippingZone(shippingName, warehouseName, country) { +export function createShippingZone( + shippingName, + warehouseName, + country, + channelName +) { cy.get(SHIPPING_ZONES_LIST.addShippingZone) .click() .get(SHIPPING_ZONE_DETAILS.nameInput) @@ -21,8 +26,13 @@ export function createShippingZone(shippingName, warehouseName, country) { .click() .get(SHIPPING_ZONE_DETAILS.warehouseSelector) .click() - .get(SHIPPING_ZONE_DETAILS.warehouseOption) + .get(SHIPPING_ZONE_DETAILS.option) .contains(warehouseName) + .click() + .get(SHIPPING_ZONE_DETAILS.channelSelector) + .click() + .get(SHIPPING_ZONE_DETAILS.option) + .contains(channelName) .click(); cy.addAliasToGraphRequest("UpdateShippingZone"); cy.get(BUTTON_SELECTORS.confirm).click(); diff --git a/cypress/utils/shippingUtils.js b/cypress/utils/shippingUtils.js index 23f6710ed..7608208b0 100644 --- a/cypress/utils/shippingUtils.js +++ b/cypress/utils/shippingUtils.js @@ -6,7 +6,7 @@ export function createShipping({ channelId, name, address, price = 1 }) { let shippingZone; let warehouse; - return createShippingZone(name, address.country) + return createShippingZone(name, address.country, channelId) .then(shippingZoneResp => { shippingZone = shippingZoneResp; createWarehouse({ name, shippingZoneId: shippingZone.id, address }); @@ -26,9 +26,9 @@ export function createShipping({ channelId, name, address, price = 1 }) { .then(() => ({ shippingMethod, shippingZone, warehouse })); } -export function createShippingZone(name, country) { +export function createShippingZone(name, country, channelId) { return shippingMethodRequest - .createShippingZone(name, country) + .createShippingZone(name, country, channelId) .its("body.data.shippingZoneCreate.shippingZone"); } export function createWarehouse({ name, shippingZoneId, address }) { diff --git a/cypress/videos/channels.js.mp4 b/cypress/videos/channels.js.mp4 new file mode 100644 index 000000000..e0b361503 Binary files /dev/null and b/cypress/videos/channels.js.mp4 differ diff --git a/cypress/videos/collections.js.mp4 b/cypress/videos/collections.js.mp4 new file mode 100644 index 000000000..10cbc2fb1 Binary files /dev/null and b/cypress/videos/collections.js.mp4 differ diff --git a/locale/defaultMessages.json b/locale/defaultMessages.json index eb5275f7d..1af33ea26 100644 --- a/locale/defaultMessages.json +++ b/locale/defaultMessages.json @@ -15,6 +15,22 @@ "context": "by preposition", "string": "by" }, + "channel hidden": { + "context": "channel publication status", + "string": "Hidden" + }, + "channel publication date visible since": { + "context": "channel publication date", + "string": "Visible since {date}" + }, + "channel publication date will become available": { + "context": "channel publication date", + "string": "Will become available on {date}" + }, + "channel visible": { + "context": "channel publication status", + "string": "Visible" + }, "configurationMenuAttributes": { "string": "Determine attributes used to create product types" }, @@ -275,6 +291,10 @@ "context": "new discount label", "string": "New discount value" }, + "not published channel": { + "context": "product label", + "string": "Not published" + }, "orderCustomerBillingAddressNotSet": { "context": "no address is set in draft order", "string": "Not set" @@ -302,6 +322,10 @@ "pageTypeInputLabel": { "string": "Select content type" }, + "product available for purchase": { + "context": "product availability", + "string": "Available for purchase" + }, "productExportFieldCategory": { "context": "product field", "string": "Category" @@ -396,6 +420,10 @@ "context": "no warehouses info", "string": "There are no warehouses set up for your store. To add stock quantity to the product please configure a warehouse" }, + "published channel": { + "context": "product label", + "string": "Published" + }, "saleDetailsPageCategoriesQuantity": { "context": "number of categories", "string": "Categories ({quantity})" @@ -420,10 +448,18 @@ "context": "unassign product from sale, button", "string": "Unassign" }, + "set availability date": { + "context": "product availability date label", + "string": "Set availability date" + }, "shipment refund title": { "context": "shipment refund title", "string": "Shipment was refunded" }, + "shippingZoneChannels_dot_autocomplete_dot_label": { + "context": "ChannelsSection select field label", + "string": "Channel" + }, "shippingZoneDetailsDialogsDeleteShippingMethod": { "context": "delete shipping method", "string": "Are you sure you want to delete {name}?" @@ -433,7 +469,7 @@ "string": "Are you sure you want to delete {name}?" }, "shippingZoneWarehouses_dot_autocomplete_dot_label": { - "context": "autocomplete select label", + "context": "WarehousesSection select field label", "string": "Warehouse" }, "src_dot_accept": { @@ -1283,6 +1319,22 @@ "context": "channels section name", "string": "Channels" }, + "src_dot_channels_dot_ChannelsWithVariantsAvailabilityCard_dot_allVariantsLabel": { + "context": "all variants label", + "string": "All variants" + }, + "src_dot_channels_dot_ChannelsWithVariantsAvailabilityCard_dot_variantCountLabel": { + "context": "variants count label", + "string": "{variantsCount} variants" + }, + "src_dot_channels_dot_components_dot_CannotDefineChannelsAvailabilityCard_dot_subtitle": { + "context": "CannotDefineChannelsAvailabilityCard subtitle", + "string": "You will be able to define availability of product after creating variants." + }, + "src_dot_channels_dot_components_dot_CannotDefineChannelsAvailabilityCard_dot_title": { + "context": "CannotDefineChannelsAvailabilityCard title", + "string": "Availability" + }, "src_dot_channels_dot_components_dot_ChannelDeleteDialog_dot_deleteChannel": { "context": "dialog header", "string": "Delete Channel" @@ -1363,6 +1415,26 @@ "context": "activate", "string": "Activate" }, + "src_dot_channels_dot_components_dot_ChannelsWithVariantsAvailabilityDialog_dot_title": { + "context": "channels variants availability dialog title", + "string": "Manage Channels" + }, + "src_dot_channels_dot_components_dot_ChannelsWithVariantsAvailabilityDialog_dot_variantsSelectedLabel": { + "context": "variants selected label", + "string": "{variantsAmount} variants selected" + }, + "src_dot_channels_dot_components_dot_ShippingZonesCard_dot_addZoneTitle": { + "context": "add shipping zone title", + "string": "Add Shipping Zones" + }, + "src_dot_channels_dot_components_dot_ShippingZonesCard_dot_subtitle": { + "context": "card subtitle", + "string": "Select Shipping Zones that will be supplied via this channel. You can assign Shipping Zones to multiple channels." + }, + "src_dot_channels_dot_components_dot_ShippingZonesCard_dot_title": { + "context": "title", + "string": "{zonesCount} Shipping Zones" + }, "src_dot_channels_dot_pages_dot_ChannelsListPage_dot_2754800034": { "context": "alert", "string": "Channel limit reached" @@ -1703,46 +1775,43 @@ "src_dot_components_dot_AutocompleteSelectMenu_dot_2332404293": { "string": "No results" }, - "src_dot_components_dot_AvailabilityCard_dot_1306298759": { - "context": "product availability", - "string": "Available for purchase" - }, - "src_dot_components_dot_AvailabilityCard_dot_2162667201": { - "context": "channel publication date", - "string": "Visible since {date}" - }, - "src_dot_components_dot_AvailabilityCard_dot_2232321263": { - "context": "product publication date label", - "string": "will become published on {date}" - }, - "src_dot_components_dot_AvailabilityCard_dot_2938074852": { - "context": "product available for purchase date", - "string": "will become available on {date}" - }, - "src_dot_components_dot_AvailabilityCard_dot_3285520461": { - "context": "channel publication date", - "string": "Will become available on {date}" - }, - "src_dot_components_dot_AvailabilityCard_dot_570524410": { - "context": "product availability date label", - "string": "Set availability date" - }, - "src_dot_components_dot_AvailabilityCard_dot_643174786": { - "context": "channel publication status", - "string": "Visible" - }, - "src_dot_components_dot_AvailabilityCard_dot_77815154": { - "context": "channel publication status", - "string": "Hidden" - }, - "src_dot_components_dot_AvailabilityCard_dot_825317195": { - "context": "product unavailability", - "string": "Unavailable for purchase" - }, "src_dot_components_dot_BulkAttributeUnassignDialog_dot_3177750460": { "context": "unassign multiple attributes from item", "string": "{counter,plural,one{Are you sure you want to unassign this attribute from {itemTypeName}?} other{Are you sure you want to unassign {attributeQuantity} attributes from {itemTypeName}?}}" }, + "src_dot_components_dot_ChannelsAvailabilityCard_dot_3326160357": { + "context": "section header", + "string": "Availability" + }, + "src_dot_components_dot_ChannelsAvailabilityCard_dot_370220662": { + "context": "channels availability text", + "string": "Available at {selectedChannelsCount} out of {allChannelsCount, plural, one {# channel} other {# channels}}" + }, + "src_dot_components_dot_ChannelsAvailabilityCard_dot_4037103586": { + "context": "section header button", + "string": "Manage" + }, + "src_dot_components_dot_ChannelsAvailabilityCard_dot_Channel_dot_1311467573": { + "string": "Show in product listings" + }, + "src_dot_components_dot_ChannelsAvailabilityCard_dot_Channel_dot_1815688500": { + "context": "date", + "string": "since {date}" + }, + "src_dot_components_dot_ChannelsAvailabilityCard_dot_Channel_dot_2060790769": { + "context": "publish on date", + "string": "Publish on" + }, + "src_dot_components_dot_ChannelsAvailabilityCard_dot_Channel_dot_2264302389": { + "string": "Disabling this checkbox will remove product from search and category pages. It will be available on collection pages." + }, + "src_dot_components_dot_ChannelsAvailabilityCard_dot_Channel_dot_2699516026": { + "context": "available on date", + "string": "Set available on" + }, + "src_dot_components_dot_ChannelsAvailabilityCard_dot_Channel_dot_292404896": { + "string": "Set publication date" + }, "src_dot_components_dot_ChannelsAvailabilityContent_dot_1528830621": { "string": "Select channels you want for {contentType} to be available on" }, @@ -1758,6 +1827,25 @@ "src_dot_components_dot_ChannelsAvailabilityContent_dot_4243012684": { "string": "Search through channels" }, + "src_dot_components_dot_ChannelsAvailabilityDialogWrapper_dot_4243012684": { + "string": "Search through channels" + }, + "src_dot_components_dot_ChannelsAvailabilityDialogWrapper_dot_channelsAlphabeticallyTitle": { + "context": "channels alphabetically title", + "string": "Channels from A to Z" + }, + "src_dot_components_dot_ChannelsAvailabilityDialogWrapper_dot_notFoundTitle": { + "context": "no channels found title", + "string": "No Channels Found" + }, + "src_dot_components_dot_ChannelsAvailabilityDialogWrapper_dot_selectAllChannelsLabel": { + "context": "select all channels label", + "string": "Select All Channels" + }, + "src_dot_components_dot_ChannelsAvailabilityDialogWrapper_dot_selectTitle": { + "context": "select title", + "string": "Select channels you want for {contentType} to be available on" + }, "src_dot_components_dot_ChannelsAvailabilityDropdown_dot_1043589445": { "context": "product channel publication status", "string": "hidden" @@ -1778,39 +1866,6 @@ "context": "product status", "string": "Available in {count} out of {allCount, plural, one {# channel} other {# channels}}" }, - "src_dot_components_dot_ChannelsAvailability_dot_1311467573": { - "string": "Show in product listings" - }, - "src_dot_components_dot_ChannelsAvailability_dot_1815688500": { - "context": "date", - "string": "since {date}" - }, - "src_dot_components_dot_ChannelsAvailability_dot_2060790769": { - "context": "publish on date", - "string": "Publish on" - }, - "src_dot_components_dot_ChannelsAvailability_dot_2264302389": { - "string": "Disabling this checkbox will remove product from search and category pages. It will be available on collection pages." - }, - "src_dot_components_dot_ChannelsAvailability_dot_2699516026": { - "context": "available on date", - "string": "Set available on" - }, - "src_dot_components_dot_ChannelsAvailability_dot_292404896": { - "string": "Set publication date" - }, - "src_dot_components_dot_ChannelsAvailability_dot_3326160357": { - "context": "section header", - "string": "Availability" - }, - "src_dot_components_dot_ChannelsAvailability_dot_370220662": { - "context": "channels availability text", - "string": "Available at {selectedChannelsCount} out of {allChannelsCount, plural, one {# channel} other {# channels}}" - }, - "src_dot_components_dot_ChannelsAvailability_dot_4037103586": { - "context": "section header button", - "string": "Manage" - }, "src_dot_components_dot_ColumnPicker_dot_1483881697": { "context": "button", "string": "Reset" @@ -4955,9 +5010,6 @@ "context": "there are more elements of list that are hidden", "string": "and {number} more" }, - "src_dot_products_dot_components_dot_ProductExportDialog_dot_1583816707": { - "string": "Add all channels" - }, "src_dot_products_dot_components_dot_ProductExportDialog_dot_1890035856": { "context": "informations about product organization, header", "string": "Product Organization" @@ -5564,28 +5616,28 @@ "src_dot_products_dot_views_dot_ProductList_dot_44832327": { "string": "We are currently exporting your requested CSV. As soon as it is available it will be sent to your email address" }, - "src_dot_products_dot_views_dot_ProductUpdate_dot_1177237881": { - "context": "dialog content", - "string": "{counter,plural,one{Are you sure you want to delete this variant?} other{Are you sure you want to delete {displayQuantity} variants?}}" - }, - "src_dot_products_dot_views_dot_ProductUpdate_dot_1454532689": { - "context": "dialog header", - "string": "Delete Product Variants" - }, - "src_dot_products_dot_views_dot_ProductUpdate_dot_2297471173": { - "context": "delete product", - "string": "Are you sure you want to delete {name}?" - }, "src_dot_products_dot_views_dot_ProductUpdate_dot_3423943948": { "string": "Manage Products Channel Availability" }, "src_dot_products_dot_views_dot_ProductUpdate_dot_4108890645": { "string": "Product removed" }, - "src_dot_products_dot_views_dot_ProductUpdate_dot_879305849": { - "context": "dialog header", + "src_dot_products_dot_views_dot_ProductUpdate_dot_deleteProductDialogSubtitle": { + "context": "delete product dialog subtitle", + "string": "Are you sure you want to delete {name}?" + }, + "src_dot_products_dot_views_dot_ProductUpdate_dot_deleteProductDialogTitle": { + "context": "delete product dialog title", "string": "Delete Product" }, + "src_dot_products_dot_views_dot_ProductUpdate_dot_deleteVariantDialogSubtitle": { + "context": "delete variant dialog subtitle", + "string": "{counter,plural,one{Are you sure you want to delete this variant?} other{Are you sure you want to delete {displayQuantity} variants?}}" + }, + "src_dot_products_dot_views_dot_ProductUpdate_dot_deleteVariantDialogTitle": { + "context": "delete variant dialog title", + "string": "Delete Product Variants" + }, "src_dot_products_dot_views_dot_ProductVariantCreator_dot_2292700443": { "context": "success message", "string": "Successfully created variants" @@ -5940,20 +5992,21 @@ "context": "shipping method name", "string": "Name" }, - "src_dot_shipping_dot_components_dot_ShippingZoneWarehouses_dot_1221560277": { - "context": "section header", - "string": "Warehouse" - }, - "src_dot_shipping_dot_components_dot_ShippingZoneWarehouses_dot_2304484478": { - "context": "button", + "src_dot_shipping_dot_components_dot_ShippingZoneSettingsCard_dot_selectFieldAddText": { + "context": "WarehousesSection select field add text", "string": "Add New Warehouse" }, - "src_dot_shipping_dot_components_dot_ShippingZoneWarehouses_dot_3852102652": { + "src_dot_shipping_dot_components_dot_ShippingZoneSettingsCard_dot_selectFieldPlaceholder": { + "context": "WarehousesSection select field placeholder", + "string": "Select Warehouse" + }, + "src_dot_shipping_dot_components_dot_ShippingZoneSettingsCard_dot_subtitle": { + "context": "WarehousesSection subtitle", "string": "Select warehouse from which you will ship products for this shipping zone. This warehouse address will also be used to calculate taxes." }, - "src_dot_shipping_dot_components_dot_ShippingZoneWarehouses_dot_46197273": { - "context": "input placeholder", - "string": "Select Warehouse" + "src_dot_shipping_dot_components_dot_ShippingZoneSettingsCard_dot_title": { + "context": "ShippingZoneSettingsCard title", + "string": "Settings" }, "src_dot_shipping_dot_components_dot_ShippingZonesListPage_dot_1325966144": { "context": "header", @@ -6985,6 +7038,10 @@ "context": "event", "string": "Customer updated" }, + "src_dot_webhooks_dot_components_dot_WebhookInfo_dot_1089569085": { + "context": "webhook", + "string": "Secret Key" + }, "src_dot_webhooks_dot_components_dot_WebhookInfo_dot_1690209105": { "context": "webhook", "string": "Target URL" @@ -7005,10 +7062,6 @@ "context": "webhook secret key help text", "string": "secret key is used to create a hash signature with each payload. *optional field" }, - "src_dot_webhooks_dot_components_dot_WebhookInfo_dot_3995417850": { - "context": "webhook", - "string": "Secret Key" - }, "src_dot_webhooks_dot_components_dot_WebhookInfo_dot_4194304040": { "context": "webhook specific information", "string": "Webhook specific information" @@ -7074,6 +7127,10 @@ "context": "transaction reference subtitle", "string": "Transaction reference" }, + "unavailable for purchase": { + "context": "product unavailability", + "string": "Unavailable for purchase" + }, "voucherDetailsUnassignCategory": { "context": "unassign category from voucher, button", "string": "Unassign" @@ -7085,5 +7142,13 @@ "voucherDetailsUnassignProduct": { "context": "unassign product from voucher, button", "string": "Unassign" + }, + "will become available on": { + "context": "product available for purchase date", + "string": "will become available on {date}" + }, + "will become published on": { + "context": "product publication date label", + "string": "will become published on {date}" } -} +} \ No newline at end of file diff --git a/schema.graphql b/schema.graphql index 2ef3ed22b..e2f99deab 100644 --- a/schema.graphql +++ b/schema.graphql @@ -86,7 +86,6 @@ input AccountInput { lastName: String defaultBillingAddress: AddressInput defaultShippingAddress: AddressInput - languageCode: LanguageCodeEnum } type AccountRegister { @@ -103,7 +102,6 @@ input AccountRegisterInput { email: String! password: String! redirectUrl: String - languageCode: LanguageCodeEnum } type AccountRequestDeletion { @@ -575,7 +573,6 @@ input AttributeFilterInput { filterableInStorefront: Boolean filterableInDashboard: Boolean availableInGrid: Boolean - metadata: [MetadataInput] search: String ids: [ID] type: AttributeTypeEnum @@ -678,7 +675,6 @@ type AttributeValue implements Node { id: ID! name: String slug: String - value: String translation(languageCode: LanguageCodeEnum!): AttributeValueTranslation inputType: AttributeInputTypeEnum reference: ID @@ -879,7 +875,6 @@ type CategoryDelete { input CategoryFilterInput { search: String - metadata: [MetadataInput] ids: [ID] } @@ -956,6 +951,7 @@ type Channel implements Node { slug: String! currencyCode: String! hasOrders: Boolean! + shippingZones: [ShippingZone!]! } type ChannelActivate { @@ -981,6 +977,7 @@ input ChannelCreateInput { name: String! slug: String! currencyCode: String! + addShippingZones: [ID!] } type ChannelDeactivate { @@ -1009,6 +1006,7 @@ type ChannelError { field: String message: String code: ChannelErrorCode! + shippingZones: [ID!] } enum ChannelErrorCode { @@ -1021,6 +1019,7 @@ enum ChannelErrorCode { CHANNEL_TARGET_ID_MUST_BE_DIFFERENT CHANNELS_CURRENCY_MUST_BE_THE_SAME CHANNEL_WITH_ORDERS + DUPLICATED_INPUT_ITEM } type ChannelUpdate { @@ -1036,6 +1035,8 @@ input ChannelUpdateInput { isActive: Boolean name: String slug: String + addShippingZones: [ID!] + removeShippingZones: [ID!] } type Checkout implements Node & ObjectWithMetadata { @@ -1065,7 +1066,6 @@ type Checkout implements Node & ObjectWithMetadata { subtotalPrice: TaxedMoney token: UUID! totalPrice: TaxedMoney - languageCode: LanguageCodeEnum! } type CheckoutAddPromoCode { @@ -1124,7 +1124,6 @@ input CheckoutCreateInput { email: String shippingAddress: AddressInput billingAddress: AddressInput - languageCode: LanguageCodeEnum } type CheckoutCustomerAttach { @@ -1184,6 +1183,7 @@ enum CheckoutErrorCode { ZERO_QUANTITY MISSING_CHANNEL_SLUG CHANNEL_INACTIVE + UNAVAILABLE_VARIANT_IN_CHANNEL } type CheckoutLanguageCodeUpdate { @@ -1423,7 +1423,6 @@ enum CollectionErrorCode { input CollectionFilterInput { published: CollectionPublished search: String - metadata: [MetadataInput] ids: [ID] channel: String } @@ -1908,7 +1907,6 @@ input CustomerInput { email: String isActive: Boolean note: String - languageCode: LanguageCodeEnum } type CustomerUpdate { @@ -2628,6 +2626,13 @@ type InvoiceRequestDelete { invoice: Invoice } +type InvoiceSendEmail { + errors: [Error!]! + @deprecated( + reason: "Use typed errors with error codes. This field will be removed after 2020-07-31." + ) +} + type InvoiceSendNotification { errors: [Error!]! @deprecated( @@ -2826,7 +2831,6 @@ enum MenuErrorCode { input MenuFilterInput { search: String slug: [String] - metadata: [MetadataInput] } input MenuInput { @@ -2900,7 +2904,6 @@ type MenuItemDelete { input MenuItemFilterInput { search: String - metadata: [MetadataInput] } input MenuItemInput { @@ -3341,7 +3344,7 @@ type Mutation { invoiceCreate(input: InvoiceCreateInput!, orderId: ID!): InvoiceCreate invoiceDelete(id: ID!): InvoiceDelete invoiceUpdate(id: ID!, input: UpdateInvoiceInput!): InvoiceUpdate - invoiceSendNotification(id: ID!): InvoiceSendNotification + invoiceSendEmail(id: ID!): InvoiceSendEmail giftCardActivate(id: ID!): GiftCardActivate giftCardCreate(input: GiftCardCreateInput!): GiftCardCreate giftCardDeactivate(id: ID!): GiftCardDeactivate @@ -3366,6 +3369,7 @@ type Mutation { voucherDelete(id: ID!): VoucherDelete voucherBulkDelete(ids: [ID]!): VoucherBulkDelete voucherUpdate(id: ID!, input: VoucherInput!): VoucherUpdate + invoiceSendNotification(id: ID!): InvoiceSendNotification voucherCataloguesAdd(id: ID!, input: CatalogueInput!): VoucherAddCatalogues voucherCataloguesRemove( id: ID! @@ -3763,7 +3767,6 @@ input OrderDraftFilterInput { customer: String created: DateRangeInput search: String - metadata: [MetadataInput] channels: [ID] } @@ -3921,7 +3924,6 @@ input OrderFilterInput { customer: String created: DateRangeInput search: String - metadata: [MetadataInput] channels: [ID] } @@ -4303,7 +4305,6 @@ enum PageErrorCode { input PageFilterInput { search: String - metadata: [MetadataInput] } type PageInfo { @@ -4579,10 +4580,6 @@ enum PaymentErrorCode { NOT_SUPPORTED_GATEWAY } -input PaymentFilterInput { - checkouts: [ID] -} - type PaymentGateway { name: String! id: ID! @@ -4853,6 +4850,10 @@ type Product implements Node & ObjectWithMetadata { isAvailableForPurchase: Boolean } +input PaymentFilterInput { + checkouts: [ID] +} + type ProductAttributeAssign { errors: [Error!]! @deprecated( @@ -4911,6 +4912,8 @@ input ProductChannelListingAddInput { visibleInListings: Boolean isAvailableForPurchase: Boolean availableForPurchaseDate: Date + addVariants: [ID!] + removeVariants: [ID!] } type ProductChannelListingError { @@ -4920,6 +4923,7 @@ type ProductChannelListingError { attributes: [ID!] values: [ID!] channels: [ID!] + variants: [ID!] } type ProductChannelListingUpdate { @@ -4932,7 +4936,7 @@ type ProductChannelListingUpdate { } input ProductChannelListingUpdateInput { - addChannels: [ProductChannelListingAddInput!] + updateChannels: [ProductChannelListingAddInput!] removeChannels: [ID!] } @@ -5033,7 +5037,6 @@ input ProductFilterInput { productType: ID stocks: ProductStockFilterInput search: String - metadata: [MetadataInput] price: PriceRangeInput minimalPrice: PriceRangeInput productTypes: [ID] @@ -5299,7 +5302,6 @@ input ProductTypeFilterInput { search: String configurable: ProductTypeConfigurable productType: ProductTypeEnum - metadata: [MetadataInput] ids: [ID] } @@ -5472,7 +5474,6 @@ type ProductVariantDelete { input ProductVariantFilterInput { search: String sku: [String] - metadata: [MetadataInput] } input ProductVariantInput { @@ -5606,6 +5607,7 @@ type Query { orderSettings: OrderSettings shippingZone(id: ID!, channel: String): ShippingZone shippingZones( + filter: ShippingZoneFilterInput channel: String before: String after: String @@ -6312,7 +6314,8 @@ type ShippingZone implements Node & ObjectWithMetadata { priceRange: MoneyRange countries: [CountryDisplay] shippingMethods: [ShippingMethod] - warehouses: [Warehouse] + warehouses: [Warehouse!]! + channels: [Channel!]! description: String } @@ -6351,6 +6354,7 @@ input ShippingZoneCreateInput { countries: [String] default: Boolean addWarehouses: [ID] + addChannels: [ID!] } type ShippingZoneDelete { @@ -6362,6 +6366,10 @@ type ShippingZoneDelete { shippingZone: ShippingZone } +input ShippingZoneFilterInput { + search: String +} + type ShippingZoneUpdate { errors: [Error!]! @deprecated( @@ -6377,7 +6385,9 @@ input ShippingZoneUpdateInput { countries: [String] default: Boolean addWarehouses: [ID] + addChannels: [ID!] removeWarehouses: [ID] + removeChannels: [ID!] } type Shop { @@ -6728,11 +6738,23 @@ type Transaction implements Node { token: String! kind: TransactionKind! isSuccess: Boolean! - error: String - gatewayResponse: JSONString! + error: TransactionError amount: Money } +enum TransactionError { + TRANSACTIONERROR_INCORRECT_NUMBER + TRANSACTIONERROR_INVALID_NUMBER + TRANSACTIONERROR_INCORRECT_CVV + TRANSACTIONERROR_INVALID_CVV + TRANSACTIONERROR_INCORRECT_ZIP + TRANSACTIONERROR_INCORRECT_ADDRESS + TRANSACTIONERROR_INVALID_EXPIRY_DATE + TRANSACTIONERROR_EXPIRED + TRANSACTIONERROR_PROCESSING_ERROR + TRANSACTIONERROR_DECLINED +} + enum TransactionKind { EXTERNAL AUTH @@ -6882,7 +6904,6 @@ type User implements Node & ObjectWithMetadata { avatar(size: Int): Image events: [CustomerEvent] storedPaymentSources: [PaymentSource] - languageCode: LanguageCodeEnum! } type UserAvatarDelete { @@ -6931,7 +6952,6 @@ input UserCreateInput { email: String isActive: Boolean note: String - languageCode: LanguageCodeEnum redirectUrl: String } @@ -7447,7 +7467,6 @@ enum WebhookSampleEventTypeEnum { CHECKOUT_CREATED CHECKOUT_UPDATED FULFILLMENT_CREATED - NOTIFY_USER PAGE_CREATED PAGE_UPDATED PAGE_DELETED diff --git a/src/categories/components/CategoryProductList/CategoryProductList.tsx b/src/categories/components/CategoryProductList/CategoryProductList.tsx index f1fa0c32a..3eff4ada7 100644 --- a/src/categories/components/CategoryProductList/CategoryProductList.tsx +++ b/src/categories/components/CategoryProductList/CategoryProductList.tsx @@ -7,9 +7,8 @@ import Checkbox from "@saleor/components/Checkbox"; import MoneyRange from "@saleor/components/MoneyRange"; import ResponsiveTable from "@saleor/components/ResponsiveTable"; import Skeleton from "@saleor/components/Skeleton"; -import TableCellAvatar, { - AVATAR_MARGIN -} from "@saleor/components/TableCellAvatar"; +import TableCellAvatar from "@saleor/components/TableCellAvatar"; +import { AVATAR_MARGIN } from "@saleor/components/TableCellAvatar/Avatar"; import TableHead from "@saleor/components/TableHead"; import TablePagination from "@saleor/components/TablePagination"; import { maybe, renderCollection } from "@saleor/misc"; diff --git a/src/channels/ChannelsWithVariantsAvailabilityCard/ChannelWithVariantAvailabilityItemWrapper.tsx b/src/channels/ChannelsWithVariantsAvailabilityCard/ChannelWithVariantAvailabilityItemWrapper.tsx new file mode 100644 index 000000000..31d82aa7f --- /dev/null +++ b/src/channels/ChannelsWithVariantsAvailabilityCard/ChannelWithVariantAvailabilityItemWrapper.tsx @@ -0,0 +1,137 @@ +import { + ExpansionPanel, + ExpansionPanelSummary, + makeStyles, + Typography +} from "@material-ui/core"; +import { ChannelData } from "@saleor/channels/utils"; +import { Messages } from "@saleor/components/ChannelsAvailabilityCard/types"; +import IconChevronDown from "@saleor/icons/ChevronDown"; +import Label from "@saleor/orders/components/OrderHistory/Label"; +import { getById } from "@saleor/orders/components/OrderReturnPage/utils"; +import { ProductDetails_product_variants } from "@saleor/products/types/ProductDetails"; +import { ChannelsWithVariantsData } from "@saleor/products/views/ProductUpdate/types"; +import { areAllChannelVariantsSelected } from "@saleor/products/views/ProductUpdate/utils"; +import React from "react"; +import { useIntl } from "react-intl"; +import { defineMessages } 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: { + defaultMessage: "{variantsCount} variants", + description: "variants count label" + }, + allVariantsLabel: { + defaultMessage: "All variants", + description: "all variants label" + } +}); + +interface ChannelAvailabilityItemWrapperProps { + variants: ProductDetails_product_variants[]; + 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, { + 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 new file mode 100644 index 000000000..e8ede0ddb --- /dev/null +++ b/src/channels/ChannelsWithVariantsAvailabilityCard/ChannelsWithVariantsAvailabilityCard.stories.tsx @@ -0,0 +1,108 @@ +import { ProductDetails_product_variants } from "@saleor/products/types/ProductDetails"; +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 ProductDetails_product_variants[], + 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 new file mode 100644 index 000000000..d459adca8 --- /dev/null +++ b/src/channels/ChannelsWithVariantsAvailabilityCard/ChannelsWithVariantsAvailabilityCard.tsx @@ -0,0 +1,101 @@ +import CannotDefineChannelsAvailabilityCard from "@saleor/channels/components/CannotDefineChannelsAvailabilityCard/CannotDefineChannelsAvailabilityCard"; +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 useDateLocalize from "@saleor/hooks/useDateLocalize"; +import { getById } from "@saleor/orders/components/OrderReturnPage/utils"; +import { ProductDetails_product_variants } from "@saleor/products/types/ProductDetails"; +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" +>; + +export interface ChannelsWithVariantsAvailabilityCardProps + extends CommonChannelsAvailabilityProps { + channelsWithVariantsData: ChannelsWithVariantsData; + channels: ChannelData[]; + variants: ProductDetails_product_variants[]; + 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 + ); + + if (!variants?.length) { + return ; + } + + return ( + + {channels + .filter(({ id }) => + areAnyChannelVariantsSelected(channelsWithVariantsData[id]) + ) + .map(({ id }) => ( + + + + ))} + + ); +}; + +export default ChannelsWithVariantsAvailabilityCard; diff --git a/src/channels/components/CannotDefineChannelsAvailabilityCard/CannotDefineChannelsAvailabilityCard.stories.tsx b/src/channels/components/CannotDefineChannelsAvailabilityCard/CannotDefineChannelsAvailabilityCard.stories.tsx new file mode 100644 index 000000000..a3fc47af2 --- /dev/null +++ b/src/channels/components/CannotDefineChannelsAvailabilityCard/CannotDefineChannelsAvailabilityCard.stories.tsx @@ -0,0 +1,11 @@ +import CentralPlacementDecorator from "@saleor/storybook/CentralPlacementDecorator"; +import CommonDecorator from "@saleor/storybook/Decorator"; +import { storiesOf } from "@storybook/react"; +import React from "react"; + +import CannotDefineChannelsAvailabilityCard from "./CannotDefineChannelsAvailabilityCard"; + +storiesOf("Channels / Cannot define channels availability card", module) + .addDecorator(CommonDecorator) + .addDecorator(CentralPlacementDecorator) + .add("default", () => ); diff --git a/src/channels/components/CannotDefineChannelsAvailabilityCard/CannotDefineChannelsAvailabilityCard.tsx b/src/channels/components/CannotDefineChannelsAvailabilityCard/CannotDefineChannelsAvailabilityCard.tsx new file mode 100644 index 000000000..4443ba36d --- /dev/null +++ b/src/channels/components/CannotDefineChannelsAvailabilityCard/CannotDefineChannelsAvailabilityCard.tsx @@ -0,0 +1,28 @@ +import { Card, CardContent } from "@material-ui/core"; +import React from "react"; +import { defineMessages, FormattedMessage } from "react-intl"; + +import CardTitle from "../../../components/CardTitle"; + +const messages = defineMessages({ + title: { + defaultMessage: "Availability", + description: "CannotDefineChannelsAvailabilityCard title" + }, + subtitle: { + defaultMessage: + "You will be able to define availability of product after creating variants.", + description: "CannotDefineChannelsAvailabilityCard subtitle" + } +}); + +const CannotDefineChannelsAvailabilityCard: React.FC = () => ( + + } /> + + + + +); + +export default CannotDefineChannelsAvailabilityCard; diff --git a/src/channels/components/ChannelForm/ChannelForm.stories.tsx b/src/channels/components/ChannelForm/ChannelForm.stories.tsx index 27219435a..5cc6ae3a7 100644 --- a/src/channels/components/ChannelForm/ChannelForm.stories.tsx +++ b/src/channels/components/ChannelForm/ChannelForm.stories.tsx @@ -8,6 +8,8 @@ import ChannelForm, { ChannelFormProps } from "./ChannelForm"; const props: ChannelFormProps = { data: { currencyCode: "euro", + shippingZonesIdsToAdd: [], + shippingZonesIdsToRemove: [], name: "Test", slug: "test" }, diff --git a/src/channels/components/ChannelForm/ChannelForm.tsx b/src/channels/components/ChannelForm/ChannelForm.tsx index 2d362acaa..81014378d 100644 --- a/src/channels/components/ChannelForm/ChannelForm.tsx +++ b/src/channels/components/ChannelForm/ChannelForm.tsx @@ -26,6 +26,8 @@ export interface FormData { name: string; currencyCode: string; slug: string; + shippingZonesIdsToAdd: string[]; + shippingZonesIdsToRemove: string[]; } export interface ChannelFormProps { diff --git a/src/channels/components/ChannelsAvailabilityDialog/utils.ts b/src/channels/components/ChannelsAvailabilityDialog/utils.ts new file mode 100644 index 000000000..d3c5b7d2a --- /dev/null +++ b/src/channels/components/ChannelsAvailabilityDialog/utils.ts @@ -0,0 +1,11 @@ +import { filter } from "fuzzaldrin"; +import React from "react"; + +export const useChannelsSearch = function( + channels: T[] +) { + const [query, onQueryChange] = React.useState(""); + const filteredChannels = filter(channels, query, { key: "name" }); + + return { query, onQueryChange, filteredChannels }; +}; diff --git a/src/channels/components/ChannelsWithVariantsAvailabilityDialog/ChannelsWithVariantsAvailabilityDialog.stories.tsx b/src/channels/components/ChannelsWithVariantsAvailabilityDialog/ChannelsWithVariantsAvailabilityDialog.stories.tsx new file mode 100644 index 000000000..0c44ba5f3 --- /dev/null +++ b/src/channels/components/ChannelsWithVariantsAvailabilityDialog/ChannelsWithVariantsAvailabilityDialog.stories.tsx @@ -0,0 +1,82 @@ +import { ChannelData } from "@saleor/channels/utils"; +import { ProductDetails_product_variants } from "@saleor/products/types/ProductDetails"; +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" + }, + { + id: "2", + name: "Channel 2" + }, + { + id: "3", + name: "Channel 3" + } + ] as ChannelData[], + 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 ProductDetails_product_variants[], + onChannelsWithVariantsConfirm: () => undefined, + addVariantToChannel: () => undefined, + removeVariantFromChannel: () => undefined, + channelsWithVariantsData: { + ["1"]: { + selectedVariantsIds: ["variantA", "variantB"], + variantsIdsToRemove: [], + variantsIdsToAdd: [] + }, + ["2"]: { + selectedVariantsIds: ["variantA", "variantC"], + variantsIdsToRemove: [], + variantsIdsToAdd: [] + }, + ["3"]: { + selectedVariantsIds: [], + variantsIdsToRemove: [], + variantsIdsToAdd: [] + } + }, + onChannelsAvailiabilityModalClose: () => undefined, + isChannelsAvailabilityModalOpen: true, + toggleAllChannels: () => undefined, + toggleAllChannelVariants: () => () => undefined, + haveChannelsWithVariantsDataChanged: 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 new file mode 100644 index 000000000..6c2ace247 --- /dev/null +++ b/src/channels/components/ChannelsWithVariantsAvailabilityDialog/ChannelsWithVariantsAvailabilityDialog.tsx @@ -0,0 +1,120 @@ +import { ChannelData } from "@saleor/channels/utils"; +import ActionDialog from "@saleor/components/ActionDialog"; +import { ProductDetails_product_variants } from "@saleor/products/types/ProductDetails"; +import { UseChannelsWithProductVariants } from "@saleor/products/views/ProductUpdate/types"; +import { + areAllVariantsAtAllChannelsSelected, + areAnyChannelVariantsSelected +} from "@saleor/products/views/ProductUpdate/utils"; +import isEqual from "lodash/isEqual"; +import React, { useEffect, useRef, useState } from "react"; +import { useIntl } from "react-intl"; +import { defineMessages } from "react-intl"; + +import { useChannelsSearch } from "../../../components/ChannelsAvailabilityDialog/utils"; +import ChannelsAvailabilityContentWrapper from "../../../components/ChannelsAvailabilityDialogWrapper/ChannelsAvailabilityDialogWrapper"; +import ChannelsWithVariantsAvailabilityDialogContent from "./ChannelsWithVariantsAvailabilityDialogContent"; + +const messages = defineMessages({ + title: { + defaultMessage: "Manage Channels", + description: "channels variants availability dialog title" + } +}); + +type UseChannelsWithVariantsCommonProps = Omit< + UseChannelsWithProductVariants, + | "onChannelsAvailiabilityModalOpen" + | "setHaveChannelsWithVariantsChanged" + | "channelsData" + | "setChannelsData" +>; + +export interface ChannelsAvailabilityDialogProps + extends UseChannelsWithVariantsCommonProps { + channels: ChannelData[]; + contentType?: string; + variants: ProductDetails_product_variants[]; +} + +export const ChannelsWithVariantsAvailabilityDialog: React.FC = ({ + channels, + contentType, + variants, + isChannelsAvailabilityModalOpen, + toggleAllChannels, + channelsWithVariantsData, + onChannelsAvailiabilityModalClose, + haveChannelsWithVariantsDataChanged, + onChannelsWithVariantsConfirm, + ...rest +}) => { + const intl = useIntl(); + const [canConfirm, setCanConfirm] = useState(false); + const channelsWithVariantsDataRef = useRef(channelsWithVariantsData); + const { query, onQueryChange, filteredChannels } = useChannelsSearch( + channels + ); + + const handleSetCanConfirm = () => { + const hasDataInsideDialogChanged = !isEqual( + channelsWithVariantsData, + channelsWithVariantsDataRef.current + ); + + if (hasDataInsideDialogChanged) { + channelsWithVariantsDataRef.current = channelsWithVariantsData; + setCanConfirm(true); + } + }; + + useEffect(handleSetCanConfirm, [channelsWithVariantsData]); + + const hasAllChannelsSelected = areAllVariantsAtAllChannelsSelected( + variants, + channelsWithVariantsData + ); + + const isChannelSelected = (channelId: string) => + areAnyChannelVariantsSelected(channelsWithVariantsData[channelId]); + + const handleClose = () => { + setCanConfirm(false); + onChannelsAvailiabilityModalClose(); + }; + + const handleConfirm = () => { + setCanConfirm(false); + onChannelsWithVariantsConfirm(); + }; + + return ( + + + + + + ); +}; + +export default ChannelsWithVariantsAvailabilityDialog; diff --git a/src/channels/components/ChannelsWithVariantsAvailabilityDialog/ChannelsWithVariantsAvailabilityDialogContent.tsx b/src/channels/components/ChannelsWithVariantsAvailabilityDialog/ChannelsWithVariantsAvailabilityDialogContent.tsx new file mode 100644 index 000000000..dec986d8a --- /dev/null +++ b/src/channels/components/ChannelsWithVariantsAvailabilityDialog/ChannelsWithVariantsAvailabilityDialogContent.tsx @@ -0,0 +1,211 @@ +import placeholderImage from "@assets/images/placeholder60x60.png"; +import { + Divider, + ExpansionPanel, + ExpansionPanelSummary, + makeStyles, + Typography +} from "@material-ui/core"; +import { ChannelData } from "@saleor/channels/utils"; +import IconCheckboxChecked from "@saleor/icons/CheckboxChecked"; +import IconCheckboxSemiChecked from "@saleor/icons/CheckboxSemiChecked"; +import IconChevronDown from "@saleor/icons/ChevronDown"; +import Label from "@saleor/orders/components/OrderHistory/Label"; +import { getById } from "@saleor/orders/components/OrderReturnPage/utils"; +import { ProductDetails_product_variants } from "@saleor/products/types/ProductDetails"; +import { ChannelsWithVariantsData } from "@saleor/products/views/ProductUpdate/types"; +import { areAllChannelVariantsSelected } from "@saleor/products/views/ProductUpdate/utils"; +import map from "lodash-es/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 + }, + content: { + margin: 0 + } + }), + { name: "ChannelsWithVariantsAvailabilityChannelSummary" } +); + +const messages = defineMessages({ + variantsSelectedLabel: { + 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: ProductDetails_product_variants[]; +} + +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 selectChannelIcon = (channelId: string) => + areAllChannelVariantsSelected( + allVariants, + channelsWithVariants[channelId] + ) ? ( + + ) : ( + + ); + + return ( + <> + {map(channelsWithVariants, ({ selectedVariantsIds }, channelId) => { + const { name } = channels.find(getById(channelId)); + + 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 new file mode 100644 index 000000000..ff569276d --- /dev/null +++ b/src/channels/components/ChannelsWithVariantsAvailabilityDialog/index.tsx @@ -0,0 +1,2 @@ +export * from "./ChannelsWithVariantsAvailabilityDialog"; +export { default } from "./ChannelsWithVariantsAvailabilityDialog"; diff --git a/src/channels/components/ShippingZonesCard/ShippingZoneItem.tsx b/src/channels/components/ShippingZonesCard/ShippingZoneItem.tsx new file mode 100644 index 000000000..3a850a420 --- /dev/null +++ b/src/channels/components/ShippingZonesCard/ShippingZoneItem.tsx @@ -0,0 +1,43 @@ +import { Divider, Typography } from "@material-ui/core"; +import { makeStyles } from "@material-ui/core/styles"; +import { Channel_channel_shippingZones } from "@saleor/channels/types/Channel"; +import DeletableItem from "@saleor/components/DeletableItem"; +import React from "react"; + +const useStyles = makeStyles( + theme => ({ + container: { + paddingLeft: theme.spacing(3), + display: "flex", + flexDirection: "row", + justifyContent: "space-between", + alignItems: "center" + } + }), + { name: "ShippingZoneItem" } +); + +interface ShippingZoneItemProps { + zone: Channel_channel_shippingZones; + onDelete: (id: string) => void; +} + +const ShippingZoneItem: React.FC = ({ + zone, + onDelete +}) => { + const { id, name } = zone; + const classes = useStyles({}); + + return ( + <> +
+ {name} + +
+ + + ); +}; + +export default ShippingZoneItem; diff --git a/src/channels/components/ShippingZonesCard/ShippingZonesCard.stories.tsx b/src/channels/components/ShippingZonesCard/ShippingZonesCard.stories.tsx new file mode 100644 index 000000000..a5da41cbd --- /dev/null +++ b/src/channels/components/ShippingZonesCard/ShippingZonesCard.stories.tsx @@ -0,0 +1,42 @@ +import { Channel_channel_shippingZones } from "@saleor/channels/types/Channel"; +import CommonDecorator from "@saleor/storybook/Decorator"; +import { storiesOf } from "@storybook/react"; +import React from "react"; + +import ShippingZonesCard from "./ShippingZonesCard"; + +const shippingZones = [ + { + __typename: "ShippingZone", + id: "2", + name: "Fancy shipping zone" + }, + { + __typename: "ShippingZone", + id: "3", + name: "Nice shipping zone" + } +]; + +const baseProps = { + addShippingZone: () => undefined, + removeShippingZone: () => undefined, + searchShippingZones: () => undefined, + fetchMoreShippingZones: { + loading: false, + hasMore: false, + onFetchMore: () => undefined + }, + shippingZones: [], + shippingZonesChoices: shippingZones as Channel_channel_shippingZones[] +}; + +storiesOf("Shipping zones card", module) + .addDecorator(CommonDecorator) + .add("with no options selected", () => ) + .add("with options selected", () => ( + + )); diff --git a/src/channels/components/ShippingZonesCard/ShippingZonesCard.tsx b/src/channels/components/ShippingZonesCard/ShippingZonesCard.tsx new file mode 100644 index 000000000..d962c0013 --- /dev/null +++ b/src/channels/components/ShippingZonesCard/ShippingZonesCard.tsx @@ -0,0 +1,84 @@ +import { + Card, + CardContent, + Divider, + makeStyles, + Typography +} from "@material-ui/core"; +import ExpansionPanel from "@material-ui/core/ExpansionPanel"; +import CardTitle from "@saleor/components/CardTitle"; +import React from "react"; +import { defineMessages, useIntl } from "react-intl"; + +import ShippingZoneItem from "./ShippingZoneItem"; +import ShippingZonesCardListFooter from "./ShippingZonesCardListFooter"; +import ShippingZonesListHeader from "./ShippingZonesListHeader"; +import { ShippingZonesProps } from "./types"; + +const messages = defineMessages({ + title: { + defaultMessage: "Shipping Zones", + description: "card title" + }, + subtitle: { + defaultMessage: + "Select Shipping Zones that will be supplied via this channel. You can assign Shipping Zones to multiple channels.", + description: "card subtitle" + } +}); + +const useExpanderStyles = makeStyles( + () => ({ + // empty expanded needed for mui to use root styles + expanded: {}, + root: { + boxShadow: "none", + + "&:before": { + content: "none" + }, + + "&$expanded": { + margin: 0, + border: "none" + } + } + }), + { name: "ShippingZonesCardExpander" } +); + +type ShippingZonesCardProps = ShippingZonesProps; + +const ShippingZonesCard: React.FC = props => { + const { + shippingZones, + removeShippingZone, + fetchMoreShippingZones: { totalCount } + } = props; + + const expanderClasses = useExpanderStyles({}); + const intl = useIntl(); + + const hasMoreZonesToBeSelected = totalCount !== shippingZones.length; + + return ( + + + + {intl.formatMessage(messages.subtitle)} + + + + + {shippingZones.map(zone => ( + + ))} + {hasMoreZonesToBeSelected ? ( + + ) : null} + + + ); +}; + +export default ShippingZonesCard; diff --git a/src/channels/components/ShippingZonesCard/ShippingZonesCardListFooter.tsx b/src/channels/components/ShippingZonesCard/ShippingZonesCardListFooter.tsx new file mode 100644 index 000000000..ab56a0139 --- /dev/null +++ b/src/channels/components/ShippingZonesCard/ShippingZonesCardListFooter.tsx @@ -0,0 +1,79 @@ +import { ClickAwayListener } from "@material-ui/core"; +import { Channel_channel_shippingZones } from "@saleor/channels/types/Channel"; +import SingleAutocompleteSelectField from "@saleor/components/SingleAutocompleteSelectField"; +import CardAddItemsFooter from "@saleor/products/components/ProductStocks/CardAddItemsFooter"; +import { mapNodeToChoice } from "@saleor/utils/maps"; +import React, { useEffect, useRef, useState } from "react"; +import { defineMessages } from "react-intl"; + +import { ShippingZonesProps } from "./types"; + +const messages = defineMessages({ + addZoneTitle: { + defaultMessage: "Add Shipping Zones", + description: "add shipping zone title" + } +}); + +type ShippingZonesCardListFooterProps = ShippingZonesProps; + +const ShippingZonesCardListFooter: React.FC = ({ + shippingZonesChoices, + searchShippingZones, + fetchMoreShippingZones, + addShippingZone, + shippingZones +}) => { + const [isChoicesSelectShown, setIsChoicesSelectShown] = useState(false); + const shippingZonesRef = useRef( + shippingZones + ); + + // select holds value and displays it so it needs remounting + // to display empty input after adding new zone + useEffect(() => { + if (shippingZones.length > shippingZonesRef.current.length) { + setIsChoicesSelectShown(true); + } + + shippingZonesRef.current = shippingZones; + }, [shippingZones]); + + const handleChoice = ({ target }) => { + setIsChoicesSelectShown(false); + addShippingZone(target.value); + }; + + const handleFooterClickAway = () => { + setIsChoicesSelectShown(false); + searchShippingZones(""); + }; + + return isChoicesSelectShown ? ( + +
+ +
+
+ ) : ( + setIsChoicesSelectShown(true)} + title={messages.addZoneTitle} + testIds={{ + link: "add-shipping-zone-link", + button: "add-shipping-zone-button" + }} + /> + ); +}; + +export default ShippingZonesCardListFooter; diff --git a/src/channels/components/ShippingZonesCard/ShippingZonesListHeader.tsx b/src/channels/components/ShippingZonesCard/ShippingZonesListHeader.tsx new file mode 100644 index 000000000..1e254700e --- /dev/null +++ b/src/channels/components/ShippingZonesCard/ShippingZonesListHeader.tsx @@ -0,0 +1,73 @@ +import { ExpansionPanelSummary, Typography } from "@material-ui/core"; +import { makeStyles } from "@material-ui/core/styles"; +import HorizontalSpacer from "@saleor/apps/components/HorizontalSpacer"; +import { Channel_channel_shippingZones } from "@saleor/channels/types/Channel"; +import IconChevronDown from "@saleor/icons/ChevronDown"; +import React from "react"; +import { defineMessages, useIntl } from "react-intl"; + +const useStyles = makeStyles( + theme => ({ + container: { + width: "100%", + display: "flex", + flexDirection: "row", + alignItems: "center" + }, + // empty expanded needed for mui to use root styles + expanded: {}, + root: { + width: "100%", + border: "none", + marginRight: theme.spacing(1), + paddingBottom: theme.spacing(2), + minHeight: 0, + + "&$expanded": { + minHeight: 0 + } + }, + content: { + paddingLeft: theme.spacing(1), + margin: 0, + + "&$expanded": { + margin: 0 + } + } + }), + { name: "ShippingZonesListHeader" } +); + +const messages = defineMessages({ + title: { + defaultMessage: "{zonesCount} Shipping Zones", + description: "title" + } +}); + +interface ShippingZonesListHeaderProps { + shippingZones: Channel_channel_shippingZones[]; +} + +const ShippingZonesListHeader: React.FC = ({ + shippingZones +}) => { + const classes = useStyles({}); + const intl = useIntl(); + + return ( +
+ } classes={classes}> + + {intl.formatMessage(messages.title, { + zonesCount: shippingZones.length + })} + + + +
+ ); +}; + +export default ShippingZonesListHeader; diff --git a/src/channels/components/ShippingZonesCard/index.tsx b/src/channels/components/ShippingZonesCard/index.tsx new file mode 100644 index 000000000..c38cdab7d --- /dev/null +++ b/src/channels/components/ShippingZonesCard/index.tsx @@ -0,0 +1,2 @@ +export * from "./ShippingZonesCard"; +export { default } from "./ShippingZonesCard"; diff --git a/src/channels/components/ShippingZonesCard/types.ts b/src/channels/components/ShippingZonesCard/types.ts new file mode 100644 index 000000000..a6dcb4172 --- /dev/null +++ b/src/channels/components/ShippingZonesCard/types.ts @@ -0,0 +1,12 @@ +import { Channel_channel_shippingZones } from "@saleor/channels/types/Channel"; +import { SearchShippingZones_search_edges_node } from "@saleor/searches/types/SearchShippingZones"; +import { FetchMoreProps } from "@saleor/types"; + +export interface ShippingZonesProps { + addShippingZone: (id: string) => void; + removeShippingZone: (id: string) => void; + searchShippingZones: (searchPhrase: string) => void; + fetchMoreShippingZones: FetchMoreProps; + shippingZones: Channel_channel_shippingZones[]; + shippingZonesChoices: SearchShippingZones_search_edges_node[]; +} diff --git a/src/channels/fixtures.ts b/src/channels/fixtures.ts index 2a4617f63..d211e93cb 100644 --- a/src/channels/fixtures.ts +++ b/src/channels/fixtures.ts @@ -18,6 +18,7 @@ export const channelsList: Channels_channels[] = [ { __typename: "Channel", currencyCode: "euro", + shippingZones: [], hasOrders: false, id: "Q2hhbm5lcDoy", isActive: true, @@ -27,6 +28,7 @@ export const channelsList: Channels_channels[] = [ { __typename: "Channel", currencyCode: "euro", + shippingZones: [], hasOrders: false, id: "Q2hhbm7lbDoy213", isActive: true, @@ -37,6 +39,7 @@ export const channelsList: Channels_channels[] = [ __typename: "Channel", currencyCode: "euro", hasOrders: false, + shippingZones: [], id: "Q2hhbn5lbDoytr", isActive: true, name: "Channel test", @@ -45,6 +48,7 @@ export const channelsList: Channels_channels[] = [ { __typename: "Channel", currencyCode: "euro", + shippingZones: [], hasOrders: false, id: "Q2hhbm5lbDo5bot", isActive: true, @@ -54,6 +58,7 @@ export const channelsList: Channels_channels[] = [ { __typename: "Channel", currencyCode: "euro", + shippingZones: [], hasOrders: false, id: "Q2hhbm7lbDoyr0tr", isActive: true, @@ -63,6 +68,7 @@ export const channelsList: Channels_channels[] = [ { __typename: "Channel", currencyCode: "euro", + shippingZones: [], hasOrders: false, id: "Q2hhbn5lbDoyya", isActive: true, @@ -72,6 +78,7 @@ export const channelsList: Channels_channels[] = [ { __typename: "Channel", currencyCode: "euro", + shippingZones: [], hasOrders: false, id: "Q2hhbm5lbDo5w0z", isActive: true, @@ -83,6 +90,7 @@ export const channelsList: Channels_channels[] = [ export const channel: Channel_channel = { __typename: "Channel", currencyCode: "zl", + shippingZones: [], hasOrders: false, id: "Q2hhbm5lbDov78", isActive: true, diff --git a/src/channels/pages/ChannelDetailsPage/ChannelDetailsPage.stories.tsx b/src/channels/pages/ChannelDetailsPage/ChannelDetailsPage.stories.tsx index 16726830c..1b4cdb21c 100644 --- a/src/channels/pages/ChannelDetailsPage/ChannelDetailsPage.stories.tsx +++ b/src/channels/pages/ChannelDetailsPage/ChannelDetailsPage.stories.tsx @@ -18,7 +18,14 @@ const props: ChannelDetailsPageProps = { onBack: () => undefined, onSubmit: () => undefined, saveButtonBarState: "default", - updateChannelStatus: () => undefined + updateChannelStatus: () => undefined, + searchShippingZones: () => undefined, + searchShippingZonesData: undefined, + fetchMoreShippingZones: { + loading: false, + hasMore: false, + onFetchMore: () => undefined + } }; storiesOf("Views / Channels / Channel details", module) diff --git a/src/channels/pages/ChannelDetailsPage/ChannelDetailsPage.tsx b/src/channels/pages/ChannelDetailsPage/ChannelDetailsPage.tsx index 4a668a4e5..d1f1dcc24 100644 --- a/src/channels/pages/ChannelDetailsPage/ChannelDetailsPage.tsx +++ b/src/channels/pages/ChannelDetailsPage/ChannelDetailsPage.tsx @@ -1,15 +1,30 @@ +import ShippingZonesCard from "@saleor/channels/components/ShippingZonesCard/ShippingZonesCard"; +import CardSpacer from "@saleor/components/CardSpacer"; import { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton"; import Form from "@saleor/components/Form"; import Grid from "@saleor/components/Grid"; import SaveButtonBar from "@saleor/components/SaveButtonBar"; import { SingleAutocompleteChoiceType } from "@saleor/components/SingleAutocompleteSelectField"; import { ChannelErrorFragment } from "@saleor/fragments/types/ChannelErrorFragment"; +import { SearchData } from "@saleor/hooks/makeTopLevelSearch"; +import { getParsedSearchData } from "@saleor/hooks/makeTopLevelSearch/utils"; +import useStateFromProps from "@saleor/hooks/useStateFromProps"; +import { + getById, + getByUnmatchingId +} from "@saleor/orders/components/OrderReturnPage/utils"; +import { SearchShippingZones_search_edges_node } from "@saleor/searches/types/SearchShippingZones"; +import { FetchMoreProps } from "@saleor/types"; import createSingleAutocompleteSelectHandler from "@saleor/utils/handlers/singleAutocompleteSelectChangeHandler"; -import React from "react"; +import React, { useState } from "react"; import { ChannelForm, FormData } from "../../components/ChannelForm"; import { ChannelStatus } from "../../components/ChannelStatus/ChannelStatus"; -import { Channel_channel } from "../../types/Channel"; +import { + Channel_channel, + Channel_channel_shippingZones +} from "../../types/Channel"; +import { getUpdatedIdsWithNewId, getUpdatedIdsWithoutNewId } from "./utils"; export interface ChannelDetailsPageProps { channel?: Channel_channel; @@ -20,38 +35,97 @@ export interface ChannelDetailsPageProps { saveButtonBarState: ConfirmButtonTransitionState; onBack?: () => void; onDelete?: () => void; - onSubmit?: (data: FormData) => void; + onSubmit: (data: FormData) => void; updateChannelStatus?: () => void; + searchShippingZones: (query: string) => void; + searchShippingZonesData?: SearchData; + fetchMoreShippingZones: FetchMoreProps; } -const initialData: FormData = { - currencyCode: "", - name: "", - slug: "" -}; - export const ChannelDetailsPage: React.FC = ({ channel, currencyCodes, disabled, disabledStatus, + onSubmit, errors, onBack, - onSubmit, onDelete, saveButtonBarState, - updateChannelStatus + updateChannelStatus, + searchShippingZones, + searchShippingZonesData, + fetchMoreShippingZones }) => { - const [selectedCurrencyCode, setSelectedCurrencyCode] = React.useState(""); + const [selectedCurrencyCode, setSelectedCurrencyCode] = useState(""); + + const [shippingZonesToDisplay, setShippingZonesToDisplay] = useStateFromProps< + Channel_channel_shippingZones[] + >(channel?.shippingZones || []); + + const initialData: FormData = { + currencyCode: "", + name: "", + slug: "", + shippingZonesIdsToAdd: [], + shippingZonesIdsToRemove: [], + ...channel + }; + + const getFilteredShippingZonesChoices = (): SearchShippingZones_search_edges_node[] => + getParsedSearchData({ data: searchShippingZonesData }).filter( + ({ id: searchedZoneId }) => + !shippingZonesToDisplay.some(({ id }) => id === searchedZoneId) + ); return ( -
- {({ change, data, hasChanged, submit }) => { + + {({ change, data, hasChanged, submit, set }) => { const handleCurrencyCodeSelect = createSingleAutocompleteSelectHandler( change, setSelectedCurrencyCode, currencyCodes ); + + const addShippingZone = (zoneId: string) => { + set({ + ...data, + shippingZonesIdsToRemove: getUpdatedIdsWithoutNewId( + data.shippingZonesIdsToRemove, + zoneId + ), + shippingZonesIdsToAdd: getUpdatedIdsWithNewId( + data.shippingZonesIdsToAdd, + zoneId + ) + }); + + setShippingZonesToDisplay([ + ...shippingZonesToDisplay, + getParsedSearchData({ data: searchShippingZonesData }).find( + getById(zoneId) + ) + ]); + }; + + const removeShippingZone = (zoneId: string) => { + set({ + ...data, + shippingZonesIdsToAdd: getUpdatedIdsWithoutNewId( + data.shippingZonesIdsToAdd, + zoneId + ), + shippingZonesIdsToRemove: getUpdatedIdsWithNewId( + data.shippingZonesIdsToRemove, + zoneId + ) + }); + + setShippingZonesToDisplay( + shippingZonesToDisplay.filter(getByUnmatchingId(zoneId)) + ); + }; + const formDisabled = !data.name || !data.slug || !data.currencyCode; return ( @@ -68,15 +142,26 @@ export const ChannelDetailsPage: React.FC = ({ errors={errors} /> - {!!updateChannelStatus && ( -
- -
- )} +
+ {!!updateChannelStatus && ( + <> + + + + )} + +
+ uniq([...ids, newId]); + +export const getUpdatedIdsWithoutNewId = (ids: string[], newId: string) => + ids.filter(id => id !== newId); diff --git a/src/channels/types/Channel.ts b/src/channels/types/Channel.ts index eaebfe162..687cd95dd 100644 --- a/src/channels/types/Channel.ts +++ b/src/channels/types/Channel.ts @@ -7,6 +7,34 @@ // GraphQL query operation: Channel // ==================================================== +export interface Channel_channel_shippingZones_metadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + +export interface Channel_channel_shippingZones_privateMetadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + +export interface Channel_channel_shippingZones_countries { + __typename: "CountryDisplay"; + code: string; + country: string; +} + +export interface Channel_channel_shippingZones { + __typename: "ShippingZone"; + metadata: (Channel_channel_shippingZones_metadata | null)[]; + privateMetadata: (Channel_channel_shippingZones_privateMetadata | null)[]; + id: string; + countries: (Channel_channel_shippingZones_countries | null)[] | null; + name: string; + description: string | null; +} + export interface Channel_channel { __typename: "Channel"; id: string; @@ -15,6 +43,7 @@ export interface Channel_channel { slug: string; currencyCode: string; hasOrders: boolean; + shippingZones: Channel_channel_shippingZones[]; } export interface Channel { diff --git a/src/channels/types/ChannelActivate.ts b/src/channels/types/ChannelActivate.ts index ec2c451fb..6ca7f2ede 100644 --- a/src/channels/types/ChannelActivate.ts +++ b/src/channels/types/ChannelActivate.ts @@ -9,6 +9,34 @@ import { ChannelErrorCode } from "./../../types/globalTypes"; // GraphQL mutation operation: ChannelActivate // ==================================================== +export interface ChannelActivate_channelActivate_channel_shippingZones_metadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + +export interface ChannelActivate_channelActivate_channel_shippingZones_privateMetadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + +export interface ChannelActivate_channelActivate_channel_shippingZones_countries { + __typename: "CountryDisplay"; + code: string; + country: string; +} + +export interface ChannelActivate_channelActivate_channel_shippingZones { + __typename: "ShippingZone"; + metadata: (ChannelActivate_channelActivate_channel_shippingZones_metadata | null)[]; + privateMetadata: (ChannelActivate_channelActivate_channel_shippingZones_privateMetadata | null)[]; + id: string; + countries: (ChannelActivate_channelActivate_channel_shippingZones_countries | null)[] | null; + name: string; + description: string | null; +} + export interface ChannelActivate_channelActivate_channel { __typename: "Channel"; id: string; @@ -17,6 +45,7 @@ export interface ChannelActivate_channelActivate_channel { slug: string; currencyCode: string; hasOrders: boolean; + shippingZones: ChannelActivate_channelActivate_channel_shippingZones[]; } export interface ChannelActivate_channelActivate_errors { diff --git a/src/channels/types/ChannelCreate.ts b/src/channels/types/ChannelCreate.ts index bb6b70d1a..586027f78 100644 --- a/src/channels/types/ChannelCreate.ts +++ b/src/channels/types/ChannelCreate.ts @@ -9,6 +9,34 @@ import { ChannelCreateInput, ChannelErrorCode } from "./../../types/globalTypes" // GraphQL mutation operation: ChannelCreate // ==================================================== +export interface ChannelCreate_channelCreate_channel_shippingZones_metadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + +export interface ChannelCreate_channelCreate_channel_shippingZones_privateMetadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + +export interface ChannelCreate_channelCreate_channel_shippingZones_countries { + __typename: "CountryDisplay"; + code: string; + country: string; +} + +export interface ChannelCreate_channelCreate_channel_shippingZones { + __typename: "ShippingZone"; + metadata: (ChannelCreate_channelCreate_channel_shippingZones_metadata | null)[]; + privateMetadata: (ChannelCreate_channelCreate_channel_shippingZones_privateMetadata | null)[]; + id: string; + countries: (ChannelCreate_channelCreate_channel_shippingZones_countries | null)[] | null; + name: string; + description: string | null; +} + export interface ChannelCreate_channelCreate_channel { __typename: "Channel"; id: string; @@ -17,6 +45,7 @@ export interface ChannelCreate_channelCreate_channel { slug: string; currencyCode: string; hasOrders: boolean; + shippingZones: ChannelCreate_channelCreate_channel_shippingZones[]; } export interface ChannelCreate_channelCreate_errors { diff --git a/src/channels/types/ChannelDeactivate.ts b/src/channels/types/ChannelDeactivate.ts index e99f790d7..e88185d06 100644 --- a/src/channels/types/ChannelDeactivate.ts +++ b/src/channels/types/ChannelDeactivate.ts @@ -9,6 +9,34 @@ import { ChannelErrorCode } from "./../../types/globalTypes"; // GraphQL mutation operation: ChannelDeactivate // ==================================================== +export interface ChannelDeactivate_channelDeactivate_channel_shippingZones_metadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + +export interface ChannelDeactivate_channelDeactivate_channel_shippingZones_privateMetadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + +export interface ChannelDeactivate_channelDeactivate_channel_shippingZones_countries { + __typename: "CountryDisplay"; + code: string; + country: string; +} + +export interface ChannelDeactivate_channelDeactivate_channel_shippingZones { + __typename: "ShippingZone"; + metadata: (ChannelDeactivate_channelDeactivate_channel_shippingZones_metadata | null)[]; + privateMetadata: (ChannelDeactivate_channelDeactivate_channel_shippingZones_privateMetadata | null)[]; + id: string; + countries: (ChannelDeactivate_channelDeactivate_channel_shippingZones_countries | null)[] | null; + name: string; + description: string | null; +} + export interface ChannelDeactivate_channelDeactivate_channel { __typename: "Channel"; id: string; @@ -17,6 +45,7 @@ export interface ChannelDeactivate_channelDeactivate_channel { slug: string; currencyCode: string; hasOrders: boolean; + shippingZones: ChannelDeactivate_channelDeactivate_channel_shippingZones[]; } export interface ChannelDeactivate_channelDeactivate_errors { diff --git a/src/channels/types/ChannelUpdate.ts b/src/channels/types/ChannelUpdate.ts index 2380a32e9..febe5af80 100644 --- a/src/channels/types/ChannelUpdate.ts +++ b/src/channels/types/ChannelUpdate.ts @@ -9,6 +9,34 @@ import { ChannelUpdateInput, ChannelErrorCode } from "./../../types/globalTypes" // GraphQL mutation operation: ChannelUpdate // ==================================================== +export interface ChannelUpdate_channelUpdate_channel_shippingZones_metadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + +export interface ChannelUpdate_channelUpdate_channel_shippingZones_privateMetadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + +export interface ChannelUpdate_channelUpdate_channel_shippingZones_countries { + __typename: "CountryDisplay"; + code: string; + country: string; +} + +export interface ChannelUpdate_channelUpdate_channel_shippingZones { + __typename: "ShippingZone"; + metadata: (ChannelUpdate_channelUpdate_channel_shippingZones_metadata | null)[]; + privateMetadata: (ChannelUpdate_channelUpdate_channel_shippingZones_privateMetadata | null)[]; + id: string; + countries: (ChannelUpdate_channelUpdate_channel_shippingZones_countries | null)[] | null; + name: string; + description: string | null; +} + export interface ChannelUpdate_channelUpdate_channel { __typename: "Channel"; id: string; @@ -17,6 +45,7 @@ export interface ChannelUpdate_channelUpdate_channel { slug: string; currencyCode: string; hasOrders: boolean; + shippingZones: ChannelUpdate_channelUpdate_channel_shippingZones[]; } export interface ChannelUpdate_channelUpdate_errors { diff --git a/src/channels/types/Channels.ts b/src/channels/types/Channels.ts index 4a6c2418c..c4283a016 100644 --- a/src/channels/types/Channels.ts +++ b/src/channels/types/Channels.ts @@ -7,6 +7,34 @@ // GraphQL query operation: Channels // ==================================================== +export interface Channels_channels_shippingZones_metadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + +export interface Channels_channels_shippingZones_privateMetadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + +export interface Channels_channels_shippingZones_countries { + __typename: "CountryDisplay"; + code: string; + country: string; +} + +export interface Channels_channels_shippingZones { + __typename: "ShippingZone"; + metadata: (Channels_channels_shippingZones_metadata | null)[]; + privateMetadata: (Channels_channels_shippingZones_privateMetadata | null)[]; + id: string; + countries: (Channels_channels_shippingZones_countries | null)[] | null; + name: string; + description: string | null; +} + export interface Channels_channels { __typename: "Channel"; id: string; @@ -15,6 +43,7 @@ export interface Channels_channels { slug: string; currencyCode: string; hasOrders: boolean; + shippingZones: Channels_channels_shippingZones[]; } export interface Channels { diff --git a/src/channels/utils.ts b/src/channels/utils.ts index 120c1cd45..930d51d7b 100644 --- a/src/channels/utils.ts +++ b/src/channels/utils.ts @@ -3,9 +3,15 @@ import { CollectionDetails_collection } from "@saleor/collections/types/Collecti import { SaleDetails_sale } from "@saleor/discounts/types/SaleDetails"; import { VoucherDetails_voucher } from "@saleor/discounts/types/VoucherDetails"; import { RequireOnlyOne } from "@saleor/misc"; -import { ProductDetails_product } from "@saleor/products/types/ProductDetails"; +import { + ProductDetails_product, + ProductDetails_product_variants +} from "@saleor/products/types/ProductDetails"; import { ProductVariantDetails_productVariant } from "@saleor/products/types/ProductVariantDetails"; -import { ShippingZone_shippingZone_shippingMethods_channelListings } from "@saleor/shipping/types/ShippingZone"; +import { + ShippingZone_shippingZone_channels, + ShippingZone_shippingZone_shippingMethods_channelListings +} from "@saleor/shipping/types/ShippingZone"; import { mapNodeToChoice } from "@saleor/utils/maps"; import uniqBy from "lodash-es/uniqBy"; @@ -16,15 +22,16 @@ export interface Channel { export interface ChannelData { id: string; - isPublished: boolean; name: string; - publicationDate: string | null; - currency: string; - price: string; - costPrice: string; - availableForPurchase: string; - isAvailableForPurchase: boolean; - visibleInListings: boolean; + isPublished?: boolean; + publicationDate?: string | null; + currency?: string; + variantsIds?: string[]; + price?: string; + costPrice?: string; + availableForPurchase?: string; + isAvailableForPurchase?: boolean; + visibleInListings?: boolean; } export interface ChannelPriceData { @@ -95,21 +102,13 @@ export const createVariantChannels = ( data?: ProductVariantDetails_productVariant ): ChannelPriceData[] => { if (data) { - const productChannels = data?.product.channelListings.map(listing => ({ - costPrice: "", - currency: listing.channel.currencyCode, - id: listing.channel.id, - name: listing.channel.name, - price: "" - })); - const variantChannels = data?.channelListings.map(listing => ({ + return data?.channelListings.map(listing => ({ costPrice: listing.costPrice?.amount.toString() || "", currency: listing.channel.currencyCode, id: listing.channel.id, name: listing.channel.name, - price: listing.price.amount.toString() + price: listing.price?.amount?.toString() })); - return uniqBy([...variantChannels, ...productChannels], obj => obj.id); } return []; }; @@ -147,6 +146,7 @@ export const createChannelsData = (data?: Channels_channels[]): ChannelData[] => currency: channel.currencyCode, id: channel.id, isAvailableForPurchase: false, + variantsIds: [], isPublished: false, name: channel.name, price: "", @@ -168,7 +168,7 @@ export const createChannelsDataWithPrice = ( }; export const createShippingChannels = ( - data?: Channels_channels[] + data?: ShippingZone_shippingZone_channels[] ): ChannelShippingData[] => data?.map(channel => ({ currency: channel.currencyCode, @@ -240,29 +240,54 @@ export const createChannelsDataFromSale = (saleData?: SaleDetails_sale) => export const createChannelsDataFromProduct = ( productData?: ProductDetails_product ) => - productData?.channelListings?.map(option => { - const variantChannel = productData.variants[0]?.channelListings.find( - listing => listing.channel.id === option.channel.id - ); - const price = variantChannel?.price; - const costPrice = variantChannel?.costPrice; - return { - availableForPurchase: option?.availableForPurchase, - costPrice: costPrice ? costPrice.amount.toString() : "", - currency: price ? price.currency : "", - id: option.channel.id, - isAvailableForPurchase: !!option?.isAvailableForPurchase, - isPublished: option.isPublished, - name: option.channel.name, - price: price ? price.amount.toString() : "", - publicationDate: option.publicationDate, - visibleInListings: !!option.visibleInListings - }; - }) || []; + productData?.channelListings?.map( + ({ + channel, + availableForPurchase, + isAvailableForPurchase, + visibleInListings, + publicationDate, + isPublished + }) => { + const variantChannel = productData.variants[0]?.channelListings.find( + listing => listing.channel.id === channel.id + ); + const price = variantChannel?.price; + const costPrice = variantChannel?.costPrice; + const variantsIds = extractVariantsIdsForChannel( + productData.variants, + channel.id + ); + + return { + availableForPurchase, + isPublished, + publicationDate, + variantsIds, + costPrice: costPrice?.amount.toString() ?? "", + currency: price ? price.currency : "", + id: channel.id, + isAvailableForPurchase: !!isAvailableForPurchase, + name: channel.name, + price: price ? price.amount.toString() : "", + visibleInListings: !!visibleInListings + }; + } + ) || []; + +export const extractVariantsIdsForChannel = ( + productVariants: ProductDetails_product_variants[], + channelId: string +) => + productVariants + ?.filter(({ channelListings }) => + channelListings.some(({ channel }) => channel.id === channelId) + ) + .map(({ id }) => id) || []; export const createSortedChannelsDataFromProduct = ( productData?: ProductDetails_product -) => +): ChannelData[] => createChannelsDataFromProduct(productData).sort((channel, nextChannel) => channel.name.localeCompare(nextChannel.name) ); @@ -272,7 +297,9 @@ export const createSortedChannelsData = (data?: Channels_channels[]) => channel.name.localeCompare(nextChannel.name) ); -export const createSortedShippingChannels = (data?: Channels_channels[]) => +export const createSortedShippingChannels = ( + data?: ShippingZone_shippingZone_channels[] +) => createShippingChannels(data)?.sort((channel, nextChannel) => channel.name.localeCompare(nextChannel.name) ); diff --git a/src/channels/views/ChannelCreate/ChannelCreate.tsx b/src/channels/views/ChannelCreate/ChannelCreate.tsx index 24f89d2ca..e3cbf9d46 100644 --- a/src/channels/views/ChannelCreate/ChannelCreate.tsx +++ b/src/channels/views/ChannelCreate/ChannelCreate.tsx @@ -1,19 +1,22 @@ +import { FormData } from "@saleor/channels/components/ChannelForm/ChannelForm"; +import { ChannelCreate } from "@saleor/channels/types/ChannelCreate"; import AppHeader from "@saleor/components/AppHeader"; import Container from "@saleor/components/Container"; import PageHeader from "@saleor/components/PageHeader"; import { WindowTitle } from "@saleor/components/WindowTitle"; +import { DEFAULT_INITIAL_SEARCH_DATA } from "@saleor/config"; +import { getSearchFetchMoreProps } from "@saleor/hooks/makeTopLevelSearch/utils"; import useNavigator from "@saleor/hooks/useNavigator"; import useNotifier from "@saleor/hooks/useNotifier"; -import { commonMessages } from "@saleor/intl"; +import { getDefaultNotifierSuccessErrorData } from "@saleor/hooks/useNotifier/utils"; import { sectionNames } from "@saleor/intl"; +import useShippingZonesSearch from "@saleor/searches/useShippingZonesSearch"; import currencyCodes from "currency-codes"; import React from "react"; import { useIntl } from "react-intl"; -import { ChannelCreateInput } from "../../../types/globalTypes"; import { useChannelCreateMutation } from "../../mutations"; import ChannelDetailsPage from "../../pages/ChannelDetailsPage"; -import { ChannelCreate } from "../../types/ChannelCreate"; import { channelPath, channelsListUrl } from "../../urls"; export const ChannelCreateView = ({}) => { @@ -23,27 +26,40 @@ export const ChannelCreateView = ({}) => { const handleBack = () => navigate(channelsListUrl()); - const onSubmit = (data: ChannelCreate) => { - if (!data.channelCreate.errors.length) { - notify({ - status: "success", - text: intl.formatMessage(commonMessages.savedChanges) - }); - navigate(channelPath(data.channelCreate.channel.id)); - } - }; - const [createChannel, createChannelOpts] = useChannelCreateMutation({ - onCompleted: onSubmit + onCompleted: ({ channelCreate: { errors, channel } }: ChannelCreate) => { + notify(getDefaultNotifierSuccessErrorData(errors, intl)); + + if (!errors.length) { + navigate(channelPath(channel.id)); + } + } }); - const handleSubmit = (data: ChannelCreateInput) => + const handleSubmit = ({ + shippingZonesIdsToAdd, + shippingZonesIdsToRemove, + currencyCode, + ...rest + }: FormData) => createChannel({ variables: { - input: { ...data, currencyCode: data.currencyCode.toUpperCase() } + input: { + ...rest, + currencyCode: currencyCode.toUpperCase(), + addShippingZones: shippingZonesIdsToAdd + } } }); + const { + loadMore: fetchMoreShippingZones, + search: searchShippingZones, + result: searchShippingZonesResult + } = useShippingZonesSearch({ + variables: DEFAULT_INITIAL_SEARCH_DATA + }); + const currencyCodeChoices = currencyCodes.data.map(currencyData => ({ label: intl.formatMessage( { @@ -77,6 +93,12 @@ export const ChannelCreateView = ({}) => { })} /> = ({ ChannelUrlQueryParams >(navigate, params => channelUrl(id, params), params); - const onSubmit = (data: ChannelUpdate) => { - if (!data.channelUpdate.errors.length) { - notify({ - status: "success", - text: intl.formatMessage(commonMessages.savedChanges) - }); - } - }; + const [updateChannel, updateChannelOpts] = useChannelUpdateMutation({ + onCompleted: ({ channelUpdate: { errors } }: ChannelUpdate) => + notify(getDefaultNotifierSuccessErrorData(errors, intl)) + }); + + const { data, loading } = useChannelDetails({ + displayLoader: true, + variables: { id } + }); const handleError = (error: ChannelErrorFragment) => { notify({ @@ -70,15 +74,6 @@ export const ChannelDetails: React.FC = ({ }); }; - const { data, loading } = useChannelDetails({ - displayLoader: true, - variables: { id } - }); - - const [updateChannel, updateChannelOpts] = useChannelUpdateMutation({ - onCompleted: onSubmit - }); - const [activateChannel, activateChannelOpts] = useChannelActivateMutation({ onCompleted: data => { const errors = data.channelActivate.errors; @@ -100,15 +95,25 @@ export const ChannelDetails: React.FC = ({ } }); - const handleSubmit = (data: ChannelUpdateInput) => + const handleSubmit = ({ + name, + slug, + shippingZonesIdsToRemove, + shippingZonesIdsToAdd + }: FormData) => updateChannel({ variables: { - id, - input: { name: data.name, slug: data.slug } + id: data?.channel.id, + input: { + name, + slug, + addShippingZones: shippingZonesIdsToAdd, + removeShippingZones: shippingZonesIdsToRemove + } } }); - const onCompleted = (data: ChannelDelete) => { + const onDeleteCompleted = (data: ChannelDelete) => { const errors = data.channelDelete.errors; if (errors.length === 0) { notify({ @@ -130,7 +135,7 @@ export const ChannelDetails: React.FC = ({ }; const [deleteChannel, deleteChannelOpts] = useChannelDeleteMutation({ - onCompleted + onCompleted: onDeleteCompleted }); const channelsChoices = getChannelsCurrencyChoices( @@ -146,6 +151,14 @@ export const ChannelDetails: React.FC = ({ deleteChannel({ variables: data }); }; + const { + loadMore: fetchMoreShippingZones, + search: searchShippingZones, + result: searchShippingZonesResult + } = useShippingZonesSearch({ + variables: DEFAULT_INITIAL_SEARCH_DATA + }); + return ( <> = ({ openModal("remove")} + onSubmit={handleSubmit} updateChannelStatus={() => data?.channel?.isActive ? deactivateChannel({ variables: { id } }) diff --git a/src/collections/components/CollectionCreatePage/CollectionCreatePage.tsx b/src/collections/components/CollectionCreatePage/CollectionCreatePage.tsx index 1c344628d..e9a2bac00 100644 --- a/src/collections/components/CollectionCreatePage/CollectionCreatePage.tsx +++ b/src/collections/components/CollectionCreatePage/CollectionCreatePage.tsx @@ -1,7 +1,7 @@ import { ChannelCollectionData } from "@saleor/channels/utils"; import AppHeader from "@saleor/components/AppHeader"; -import { AvailabilityCard } from "@saleor/components/AvailabilityCard"; import { CardSpacer } from "@saleor/components/CardSpacer"; +import ChannelsAvailabilityCard from "@saleor/components/ChannelsAvailabilityCard"; import { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton"; import { Container } from "@saleor/components/Container"; import Grid from "@saleor/components/Grid"; @@ -129,7 +129,7 @@ const CollectionCreatePage: React.FC = ({
- = ({
- undefined, - openModal: () => undefined, - selectedChannelsCount: 3 -}; - -storiesOf("Generics / AvailabilityCard", module) - .addDecorator(Decorator) - .add("default", () => ); diff --git a/src/components/AvailabilityCard/AvailabilityCard.tsx b/src/components/AvailabilityCard/AvailabilityCard.tsx deleted file mode 100644 index fa325a342..000000000 --- a/src/components/AvailabilityCard/AvailabilityCard.tsx +++ /dev/null @@ -1,94 +0,0 @@ -import ChannelsAvailability, { - ChannelsAvailabilityProps, - Message -} from "@saleor/components/ChannelsAvailability"; -import useDateLocalize from "@saleor/hooks/useDateLocalize"; -import React from "react"; -import { useIntl } from "react-intl"; - -interface AvailabilityCardProps { - messages: Message; -} - -export const AvailabilityCard: React.FC> = props => { - const intl = useIntl(); - const localizeDate = useDateLocalize(); - - return ( - ({ - ...prevVal, - [currVal.id]: { - ...props.messages, - availableDateText: - currVal.publicationDate && !currVal.isPublished - ? intl.formatMessage( - { - defaultMessage: "Will become available on {date}", - description: "channel publication date" - }, - { - date: localizeDate(currVal.publicationDate, "L") - } - ) - : currVal.publicationDate - ? intl.formatMessage( - { - defaultMessage: "Visible since {date}", - description: "channel publication date" - }, - { - date: localizeDate(currVal.publicationDate, "L") - } - ) - : currVal.isPublished - ? intl.formatMessage({ - defaultMessage: "Visible", - description: "channel publication status" - }) - : intl.formatMessage({ - defaultMessage: "Hidden", - description: "channel publication status" - }), - availableLabel: intl.formatMessage({ - defaultMessage: "Available for purchase", - description: "product availability" - }), - availableSecondLabel: intl.formatMessage( - { - defaultMessage: "will become available on {date}", - description: "product available for purchase date" - }, - { - date: localizeDate(currVal.availableForPurchase, "L") - } - ), - hiddenSecondLabel: intl.formatMessage( - { - defaultMessage: "will become published on {date}", - description: "product publication date label" - }, - { - date: localizeDate(currVal.publicationDate, "L") - } - ), - setAvailabilityDateLabel: intl.formatMessage({ - defaultMessage: "Set availability date", - description: "product availability date label" - }), - unavailableLabel: intl.formatMessage({ - defaultMessage: "Unavailable for purchase", - description: "product unavailability" - }) - } - }), - {} - )} - /> - ); -}; - -export default AvailabilityCard; diff --git a/src/components/AvailabilityCard/index.ts b/src/components/AvailabilityCard/index.ts deleted file mode 100644 index f50012021..000000000 --- a/src/components/AvailabilityCard/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./AvailabilityCard"; -export { default } from "./AvailabilityCard"; diff --git a/src/components/ChannelsAvailability/ChannelsAvailability.tsx b/src/components/ChannelsAvailability/ChannelsAvailability.tsx deleted file mode 100644 index b2f36976d..000000000 --- a/src/components/ChannelsAvailability/ChannelsAvailability.tsx +++ /dev/null @@ -1,475 +0,0 @@ -import Button from "@material-ui/core/Button"; -import Card from "@material-ui/core/Card"; -import CardContent from "@material-ui/core/CardContent"; -import TextField from "@material-ui/core/TextField"; -import Typography from "@material-ui/core/Typography"; -import { Channel as ChannelList } from "@saleor/channels/utils"; -import CardTitle from "@saleor/components/CardTitle"; -import ControlledCheckbox from "@saleor/components/ControlledCheckbox"; -import Hr from "@saleor/components/Hr"; -import RadioSwitchField from "@saleor/components/RadioSwitchField"; -import RequirePermissions from "@saleor/components/RequirePermissions"; -import { CollectionChannelListingErrorFragment } from "@saleor/fragments/types/CollectionChannelListingErrorFragment"; -import { ProductChannelListingErrorFragment } from "@saleor/fragments/types/ProductChannelListingErrorFragment"; -import useDateLocalize from "@saleor/hooks/useDateLocalize"; -import useUser from "@saleor/hooks/useUser"; -import ArrowDropdown from "@saleor/icons/ArrowDropdown"; -import { RequireOnlyOne } from "@saleor/misc"; -import { PermissionEnum } from "@saleor/types/globalTypes"; -import { getFormErrors, getProductErrorMessage } from "@saleor/utils/errors"; -import classNames from "classnames"; -import React, { useState } from "react"; -import { useIntl } from "react-intl"; - -import { DateContext } from "../Date/DateContext"; -import { useStyles } from "./styles"; - -export interface ChannelData { - id: string; - isPublished: boolean; - name: string; - publicationDate: string | null; - availableForPurchase?: string; - isAvailableForPurchase?: boolean; - visibleInListings?: boolean; -} - -export interface Message { - visibleLabel: string; - hiddenLabel: string; - visibleSecondLabel?: string; - hiddenSecondLabel?: string; - availableDateText?: string; - availableLabel?: string; - unavailableLabel?: string; - availableSecondLabel?: string; - setAvailabilityDateLabel?: string; -} - -type Error = - | ProductChannelListingErrorFragment - | CollectionChannelListingErrorFragment; - -interface Value { - availableForPurchase?: string; - isAvailableForPurchase?: boolean; - isPublished: boolean; - publicationDate: string | null; - visibleInListings?: boolean; -} -interface ChannelsAvailability { - channels: ChannelData[]; - channelsList: ChannelList[]; - channelsMessages?: { [id: string]: Message }; - errors?: Error[]; - selectedChannelsCount: number; - allChannelsCount: number; - disabled?: boolean; - onChange?: (id: string, data: Value) => void; - openModal: () => void; -} -export type ChannelsAvailabilityProps = RequireOnlyOne< - ChannelsAvailability, - "channels" | "channelsList" ->; - -interface ChannelProps { - disabled?: boolean; - data: ChannelData; - errors: Error[]; - messages: Message; - onChange: (id: string, data: Value) => void; -} - -const Channel: React.FC = ({ - data, - disabled, - errors, - messages, - onChange -}) => { - const { - availableForPurchase, - isAvailableForPurchase: isAvailable, - isPublished, - publicationDate, - visibleInListings, - id, - name - } = data; - const formData = { - ...(availableForPurchase !== undefined ? { availableForPurchase } : {}), - ...(isAvailable !== undefined - ? { isAvailableForPurchase: isAvailable } - : {}), - isPublished, - publicationDate, - ...(visibleInListings !== undefined ? { visibleInListings } : {}) - }; - const dateNow = React.useContext(DateContext); - const localizeDate = useDateLocalize(); - const hasAvailableProps = - isAvailable !== undefined && availableForPurchase !== undefined; - - const [isPublicationDate, setPublicationDate] = useState( - publicationDate === null ? true : false - ); - const [isAvailableDate, setAvailableDate] = useState(false); - const [isOpen, setOpen] = useState(false); - const intl = useIntl(); - const classes = useStyles({}); - - const todayDate = localizeDate(new Date(dateNow).toISOString(), "YYYY-MM-DD"); - - const visibleMessage = (date: string) => - intl.formatMessage( - { - defaultMessage: "since {date}", - description: "date" - }, - { - date: localizeDate(date, "L") - } - ); - - const formErrors = getFormErrors( - ["availableForPurchaseDate", "publicationDate"], - errors - ); - - return ( - <> -
-
setOpen(open => !open)} - > -
- {name} - -
- - {messages.availableDateText} - -
- {isOpen && ( - <> - -

{messages.visibleLabel}

- {isPublished && - publicationDate && - Date.parse(publicationDate) < dateNow && ( - - {messages.visibleSecondLabel || - visibleMessage(publicationDate)} - - )} - - } - name="isPublished" - secondOptionLabel={ - <> -

{messages.hiddenLabel}

- {publicationDate && - !isPublished && - Date.parse(publicationDate) >= dateNow && ( - - {messages.hiddenSecondLabel} - - )} - - } - value={isPublished} - onChange={() => { - onChange(id, { - ...formData, - isPublished: !isPublished, - publicationDate: - !isPublished && !publicationDate - ? todayDate - : publicationDate - }); - }} - /> - {!isPublished && ( - <> - setPublicationDate(!isPublicationDate)} - > - {intl.formatMessage({ - defaultMessage: "Set publication date" - })} - - {isPublicationDate && ( - - onChange(id, { - ...formData, - publicationDate: e.target.value || null - }) - } - className={classes.date} - InputLabelProps={{ - shrink: true - }} - /> - )} - - )} - {hasAvailableProps && ( - <> -
- -

{messages.availableLabel}

- {isAvailable && - availableForPurchase && - Date.parse(availableForPurchase) < dateNow && ( - - {visibleMessage(availableForPurchase)} - - )} - - } - name={`channel:isAvailableForPurchase:${id}`} - secondOptionLabel={ - <> -

- {messages.unavailableLabel} -

- {availableForPurchase && !isAvailable && ( - - {messages.availableSecondLabel} - - )} - - } - value={isAvailable} - onChange={e => { - const { value } = e.target; - return onChange(id, { - ...formData, - availableForPurchase: !value - ? null - : availableForPurchase, - isAvailableForPurchase: value - }); - }} - /> - {!isAvailable && ( - <> - setAvailableDate(!isAvailableDate)} - > - {messages.setAvailabilityDateLabel} - - {isAvailableDate && ( - - onChange(id, { - ...formData, - availableForPurchase: e.target.value - }) - } - className={classes.date} - InputLabelProps={{ - shrink: true - }} - /> - )} - - )} - - )} - {visibleInListings !== undefined && ( - <> -
- -

- {intl.formatMessage({ - defaultMessage: "Show in product listings" - })} -

- - - {intl.formatMessage({ - defaultMessage: - "Disabling this checkbox will remove product from search and category pages. It will be available on collection pages." - })} - - - } - onChange={e => - onChange(id, { - ...formData, - visibleInListings: e.target.value - }) - } - /> - - )} - - )} -
-
- - ); -}; - -export const ChannelsAvailability: React.FC = props => { - const { - channelsList, - errors = [], - selectedChannelsCount, - allChannelsCount, - channels, - channelsMessages, - openModal, - onChange - } = props; - const intl = useIntl(); - const classes = useStyles({}); - const { user } = useUser(); - const channelsAvailabilityText = intl.formatMessage( - { - defaultMessage: - "Available at {selectedChannelsCount} out of {allChannelsCount, plural, one {# channel} other {# channels}}", - - description: "channels availability text" - }, - { - allChannelsCount, - selectedChannelsCount - } - ); - - return ( - <> - - - - - } - /> - - {!!channelsAvailabilityText && ( - <> - - {channelsAvailabilityText} - -
- - )} - {channels - ? channels.map(data => { - const channelErrors = - errors?.filter(error => error.channels.includes(data.id)) || - []; - return ( - - ); - }) - : channelsList - ? channelsList.map(data => ( - -
-
- {data.name} -
-
-
-
- )) - : null} -
-
- - ); -}; -ChannelsAvailability.displayName = "ChannelsAvailability"; -export default ChannelsAvailability; diff --git a/src/components/ChannelsAvailability/index.ts b/src/components/ChannelsAvailability/index.ts deleted file mode 100644 index b59832015..000000000 --- a/src/components/ChannelsAvailability/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./ChannelsAvailability"; -export { default } from "./ChannelsAvailability"; diff --git a/src/components/ChannelsAvailabilityCard/Channel/ChannelAvailabilityItemContent.tsx b/src/components/ChannelsAvailabilityCard/Channel/ChannelAvailabilityItemContent.tsx new file mode 100644 index 000000000..b6903368e --- /dev/null +++ b/src/components/ChannelsAvailabilityCard/Channel/ChannelAvailabilityItemContent.tsx @@ -0,0 +1,274 @@ +import TextField from "@material-ui/core/TextField"; +import Typography from "@material-ui/core/Typography"; +import { ChannelData } from "@saleor/channels/utils"; +import ControlledCheckbox from "@saleor/components/ControlledCheckbox"; +import Hr from "@saleor/components/Hr"; +import RadioSwitchField from "@saleor/components/RadioSwitchField"; +import useDateLocalize from "@saleor/hooks/useDateLocalize"; +import { getFormErrors, getProductErrorMessage } from "@saleor/utils/errors"; +import classNames from "classnames"; +import React, { useState } from "react"; +import { useIntl } from "react-intl"; + +import { DateContext } from "../../Date/DateContext"; +import { useStyles } from "../styles"; +import { ChannelOpts, ChannelsAvailabilityError, Messages } from "../types"; +export interface ChannelContentProps { + disabled?: boolean; + data: ChannelData; + errors: ChannelsAvailabilityError[]; + messages: Messages; + onChange: (id: string, data: ChannelOpts) => void; +} +const ChannelContent: React.FC = ({ + data, + disabled, + errors, + messages, + onChange +}) => { + const { + availableForPurchase, + isAvailableForPurchase: isAvailable, + isPublished, + publicationDate, + visibleInListings, + id + } = data; + const formData = { + ...(availableForPurchase !== undefined ? { availableForPurchase } : {}), + ...(isAvailable !== undefined + ? { isAvailableForPurchase: isAvailable } + : {}), + isPublished, + publicationDate, + ...(visibleInListings !== undefined ? { visibleInListings } : {}) + }; + const dateNow = React.useContext(DateContext); + const localizeDate = useDateLocalize(); + const hasAvailableProps = + isAvailable !== undefined && availableForPurchase !== undefined; + const [isPublicationDate, setPublicationDate] = useState( + publicationDate === null + ); + const [isAvailableDate, setAvailableDate] = useState(false); + const intl = useIntl(); + const classes = useStyles({}); + + const todayDate = localizeDate(new Date(dateNow).toISOString(), "YYYY-MM-DD"); + + const visibleMessage = (date: string) => + intl.formatMessage( + { + defaultMessage: "since {date}", + description: "date" + }, + { + date: localizeDate(date, "L") + } + ); + const formErrors = getFormErrors( + ["availableForPurchaseDate", "publicationDate"], + errors + ); + return ( +
+ +

{messages.visibleLabel}

+ {isPublished && + publicationDate && + Date.parse(publicationDate) < dateNow && ( + + {messages.visibleSecondLabel || + visibleMessage(publicationDate)} + + )} + + } + name="isPublished" + secondOptionLabel={ + <> +

{messages.hiddenLabel}

+ {publicationDate && + !isPublished && + Date.parse(publicationDate) >= dateNow && ( + + {messages.hiddenSecondLabel} + + )} + + } + value={isPublished} + onChange={() => { + onChange(id, { + ...formData, + isPublished: !isPublished, + publicationDate: + !isPublished && !publicationDate ? todayDate : publicationDate + }); + }} + /> + {!isPublished && ( + <> + setPublicationDate(!isPublicationDate)} + > + {intl.formatMessage({ + defaultMessage: "Set publication date" + })} + + {isPublicationDate && ( + + onChange(id, { + ...formData, + publicationDate: e.target.value || null + }) + } + className={classes.date} + InputLabelProps={{ + shrink: true + }} + /> + )} + + )} + {hasAvailableProps && ( + <> +
+ +

{messages.availableLabel}

+ {isAvailable && + availableForPurchase && + Date.parse(availableForPurchase) < dateNow && ( + + {visibleMessage(availableForPurchase)} + + )} + + } + name={`channel:isAvailableForPurchase:${id}`} + secondOptionLabel={ + <> +

{messages.unavailableLabel}

+ {availableForPurchase && !isAvailable && ( + + {messages.availableSecondLabel} + + )} + + } + value={isAvailable} + onChange={e => { + const { value } = e.target; + return onChange(id, { + ...formData, + availableForPurchase: !value ? null : availableForPurchase, + isAvailableForPurchase: value + }); + }} + /> + {!isAvailable && ( + <> + setAvailableDate(!isAvailableDate)} + > + {messages.setAvailabilityDateLabel} + + {isAvailableDate && ( + + onChange(id, { + ...formData, + availableForPurchase: e.target.value + }) + } + className={classes.date} + InputLabelProps={{ + shrink: true + }} + /> + )} + + )} + + )} + {visibleInListings !== undefined && ( + <> +
+ +

+ {intl.formatMessage({ + defaultMessage: "Show in product listings" + })} +

+ + {intl.formatMessage({ + defaultMessage: + "Disabling this checkbox will remove product from search and category pages. It will be available on collection pages." + })} + + + } + onChange={e => + onChange(id, { + ...formData, + visibleInListings: e.target.value + }) + } + /> + + )} +
+ ); +}; +export default ChannelContent; diff --git a/src/components/ChannelsAvailabilityCard/Channel/ChannelAvailabilityItemWrapper.tsx b/src/components/ChannelsAvailabilityCard/Channel/ChannelAvailabilityItemWrapper.tsx new file mode 100644 index 000000000..7d6150611 --- /dev/null +++ b/src/components/ChannelsAvailabilityCard/Channel/ChannelAvailabilityItemWrapper.tsx @@ -0,0 +1,95 @@ +import { + ExpansionPanel, + ExpansionPanelSummary, + makeStyles +} from "@material-ui/core"; +import Typography from "@material-ui/core/Typography"; +import { ChannelData } from "@saleor/channels/utils"; +import IconChevronDown from "@saleor/icons/ChevronDown"; +import React from "react"; + +import { Messages } from "../types"; + +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: "ChannelContentWrapperExpander" } +); + +const useSummaryStyles = makeStyles( + theme => ({ + expanded: {}, + root: { + width: "100%", + border: "none", + margin: 0, + padding: 0, + minHeight: 0, + paddingTop: theme.spacing(2), + + "&$expanded": { + minHeight: 0, + padding: theme.spacing(2, 0) + } + }, + content: { + margin: 0, + + "&$expanded": { + margin: 0 + } + } + }), + { name: "ChannelContentWrapperExpanderSummary" } +); + +export interface ChannelContentWrapperProps { + data: ChannelData; + children: React.ReactNode; + messages: Messages; +} + +const ChannelContentWrapper: React.FC = ({ + data, + messages, + children +}) => { + const expanderClasses = useExpanderStyles({}); + const summaryClasses = useSummaryStyles({}); + + const { name } = data; + + return ( + + } + classes={summaryClasses} + > + {name} + {messages.availableDateText} + + {children} + + ); +}; + +export default ChannelContentWrapper; diff --git a/src/components/ChannelsAvailability/ChannelsAvailability.stories.tsx b/src/components/ChannelsAvailabilityCard/ChannelsAvailabilityCard.stories.tsx similarity index 78% rename from src/components/ChannelsAvailability/ChannelsAvailability.stories.tsx rename to src/components/ChannelsAvailabilityCard/ChannelsAvailabilityCard.stories.tsx index 262d18eaa..3dcd6a02e 100644 --- a/src/components/ChannelsAvailability/ChannelsAvailability.stories.tsx +++ b/src/components/ChannelsAvailabilityCard/ChannelsAvailabilityCard.stories.tsx @@ -7,9 +7,10 @@ import { PermissionEnum } from "@saleor/types/globalTypes"; import { storiesOf } from "@storybook/react"; import React from "react"; -import ChannelsAvailability, { - ChannelsAvailabilityProps -} from "./ChannelsAvailability"; +import ChannelsAvailabilityCard, { + ChannelsAvailabilityCardProps +} from "./ChannelsAvailabilityCard"; +import { Messages } from "./types"; const user: User = { __typename: "User", @@ -30,7 +31,7 @@ const user: User = { const productChannels = createChannelsDataFromProduct(product("")); -const props: ChannelsAvailabilityProps = { +const props: ChannelsAvailabilityCardProps = { allChannelsCount: 4, channelsList: productChannels.map(channel => ({ id: channel.id, @@ -42,16 +43,16 @@ const props: ChannelsAvailabilityProps = { selectedChannelsCount: 3 }; -storiesOf("Generics / ChannelsAvailability", module) +storiesOf("Generics / Channels availability card", module) .addDecorator(Decorator) .addDecorator(UserDecorator(user)) - .add("default", () => ) + .add("default", () => ) .add("with onChange", () => ( - ({ ...prevVal, [currVal.id]: { @@ -60,7 +61,7 @@ storiesOf("Generics / ChannelsAvailability", module) hiddenSecondLabel: "Will become published" } }), - {} + {} as Messages )} /> )); diff --git a/src/components/ChannelsAvailabilityCard/ChannelsAvailabilityCard.tsx b/src/components/ChannelsAvailabilityCard/ChannelsAvailabilityCard.tsx new file mode 100644 index 000000000..86e00f9fa --- /dev/null +++ b/src/components/ChannelsAvailabilityCard/ChannelsAvailabilityCard.tsx @@ -0,0 +1,93 @@ +import Typography from "@material-ui/core/Typography"; +import { Channel as ChannelList, ChannelData } from "@saleor/channels/utils"; +import Hr from "@saleor/components/Hr"; +import useDateLocalize from "@saleor/hooks/useDateLocalize"; +import { RequireOnlyOne } from "@saleor/misc"; +import React from "react"; +import { useIntl } from "react-intl"; + +import ChannelAvailabilityItemContent from "./Channel/ChannelAvailabilityItemContent"; +import ChannelAvailabilityItemWrapper from "./Channel/ChannelAvailabilityItemWrapper"; +import ChannelsAvailabilityCardWrapper, { + ChannelsAvailabilityWrapperProps +} from "./ChannelsAvailabilityCardWrapper"; +import { useStyles } from "./styles"; +import { ChannelOpts, ChannelsAvailabilityError, Messages } from "./types"; +import { getChannelsAvailabilityMessages } from "./utils"; + +export interface ChannelsAvailability + extends Omit { + channels: ChannelData[]; + channelsList: ChannelList[]; + errors?: ChannelsAvailabilityError[]; + disabled?: boolean; + messages?: Messages; + onChange?: (id: string, data: ChannelOpts) => void; +} + +export type ChannelsAvailabilityCardProps = RequireOnlyOne< + ChannelsAvailability, + "channels" | "channelsList" +>; + +export const ChannelsAvailability: React.FC = props => { + const { + channelsList, + errors = [], + selectedChannelsCount = 0, + allChannelsCount = 0, + channels, + messages, + onChange, + openModal + } = props; + const intl = useIntl(); + const localizeDate = useDateLocalize(); + const classes = useStyles({}); + + const channelsMessages = getChannelsAvailabilityMessages({ + messages, + channels, + intl, + localizeDate + }); + + return ( + + {channels + ? channels.map(data => { + const channelErrors = + errors?.filter(error => error.channels.includes(data.id)) || []; + + return ( + + + + ); + }) + : channelsList + ? channelsList.map(data => ( + +
+
+ {data.name} +
+
+
+
+ )) + : null} +
+ ); +}; + +export default ChannelsAvailability; diff --git a/src/components/ChannelsAvailabilityCard/ChannelsAvailabilityCardWrapper.tsx b/src/components/ChannelsAvailabilityCard/ChannelsAvailabilityCardWrapper.tsx new file mode 100644 index 000000000..1436d598e --- /dev/null +++ b/src/components/ChannelsAvailabilityCard/ChannelsAvailabilityCardWrapper.tsx @@ -0,0 +1,87 @@ +import Button from "@material-ui/core/Button"; +import Card from "@material-ui/core/Card"; +import CardContent from "@material-ui/core/CardContent"; +import Typography from "@material-ui/core/Typography"; +import CardTitle from "@saleor/components/CardTitle"; +import Hr from "@saleor/components/Hr"; +import RequirePermissions from "@saleor/components/RequirePermissions"; +import useUser from "@saleor/hooks/useUser"; +import { PermissionEnum } from "@saleor/types/globalTypes"; +import React from "react"; +import { useIntl } from "react-intl"; + +import { useStyles } from "./styles"; + +export interface ChannelsAvailabilityWrapperProps { + selectedChannelsCount: number; + allChannelsCount: number; + openModal: () => void; + children: React.ReactNode; +} + +export const ChannelsAvailabilityWrapper: React.FC = props => { + const { + selectedChannelsCount, + allChannelsCount, + openModal, + children + } = props; + const intl = useIntl(); + const classes = useStyles({}); + const { user } = useUser(); + const channelsAvailabilityText = intl.formatMessage( + { + defaultMessage: + "Available at {selectedChannelsCount} out of {allChannelsCount, plural, one {# channel} other {# channels}}", + + description: "channels availability text" + }, + { + allChannelsCount, + selectedChannelsCount + } + ); + + return ( + <> + + + + + } + /> + + {!!channelsAvailabilityText && ( + <> + + {channelsAvailabilityText} + +
+ + )} + {children} +
+
+ + ); +}; + +export default ChannelsAvailabilityWrapper; diff --git a/src/components/ChannelsAvailabilityCard/index.ts b/src/components/ChannelsAvailabilityCard/index.ts new file mode 100644 index 000000000..18b192222 --- /dev/null +++ b/src/components/ChannelsAvailabilityCard/index.ts @@ -0,0 +1,2 @@ +export * from "./ChannelsAvailabilityCard"; +export { default } from "./ChannelsAvailabilityCard"; diff --git a/src/components/ChannelsAvailabilityCard/styles.ts b/src/components/ChannelsAvailabilityCard/styles.ts new file mode 100644 index 000000000..e07503248 --- /dev/null +++ b/src/components/ChannelsAvailabilityCard/styles.ts @@ -0,0 +1,85 @@ +import { makeStyles } from "@material-ui/core/styles"; + +export const useStyles = makeStyles( + theme => ({ + container: { + marginTop: theme.spacing(1), + marginBottom: theme.spacing(1) + }, + radioField: { + paddingLeft: theme.spacing(1) + }, + arrow: { + transition: theme.transitions.duration.short + "ms" + }, + card: { + "&:last-child": { + paddingBottom: 0 + }, + paddingTop: 0 + }, + channelBtn: { + "&:focus": { + outline: "none" + }, + background: "transparent", + border: "none", + cursor: "pointer", + textAlign: "left" + }, + channelInfo: { + fontSize: 14, + padding: theme.spacing(2, 0) + }, + channelItem: { + "&:last-child hr": { + display: "none" + }, + padding: theme.spacing(2, 0) + }, + channelName: { + alignItems: "center", + display: "flex", + justifyContent: "space-between", + marginBottom: theme.spacing(0.5) + }, + checkbox: { + alignItems: "flex-start", + marginTop: 10 + }, + date: { + "& svg": { + fill: theme.palette.primary.main + }, + marginTop: theme.spacing(1) + }, + hr: { + left: theme.spacing(-3), + position: "relative", + width: `calc(100% + ${theme.spacing(6)}px)` + }, + label: { + lineHeight: 1.2, + marginBottom: 5, + marginTop: 0 + }, + listingLabel: { + marginTop: 9 + }, + rotate: { + transform: "rotate(180deg)" + }, + secondLabel: { + color: theme.palette.text.hint, + fontSize: 12 + }, + setPublicationDate: { + color: theme.palette.primary.main, + cursor: "pointer", + fontSize: 14, + paddingBottom: 10, + paddingTop: 0 + } + }), + { name: "ChannelsAvailabilityCard" } +); diff --git a/src/components/ChannelsAvailabilityCard/types.tsx b/src/components/ChannelsAvailabilityCard/types.tsx new file mode 100644 index 000000000..dc82443a2 --- /dev/null +++ b/src/components/ChannelsAvailabilityCard/types.tsx @@ -0,0 +1,26 @@ +import { CollectionChannelListingErrorFragment } from "@saleor/fragments/types/CollectionChannelListingErrorFragment"; +import { ProductChannelListingErrorFragment } from "@saleor/fragments/types/ProductChannelListingErrorFragment"; + +export interface ChannelOpts { + availableForPurchase?: string; + isAvailableForPurchase?: boolean; + isPublished: boolean; + publicationDate: string | null; + visibleInListings?: boolean; +} + +export interface Messages { + visibleLabel: string; + hiddenLabel: string; + visibleSecondLabel?: string; + hiddenSecondLabel?: string; + availableDateText?: string; + availableLabel?: string; + unavailableLabel?: string; + availableSecondLabel?: string; + setAvailabilityDateLabel?: string; +} + +export type ChannelsAvailabilityError = + | ProductChannelListingErrorFragment + | CollectionChannelListingErrorFragment; diff --git a/src/components/ChannelsAvailabilityCard/utils.ts b/src/components/ChannelsAvailabilityCard/utils.ts new file mode 100644 index 000000000..e86f9e7bf --- /dev/null +++ b/src/components/ChannelsAvailabilityCard/utils.ts @@ -0,0 +1,95 @@ +import { ChannelData } from "@saleor/channels/utils"; +import { LocalizeDate } from "@saleor/hooks/useDateLocalize"; +import { IntlShape } from "react-intl"; + +import { Messages } from "./types"; + +export const getChannelsAvailabilityMessages = ({ + messages, + channels = [], + intl, + localizeDate +}: { + messages?: Messages; + channels?: ChannelData[]; + intl: IntlShape; + localizeDate: LocalizeDate; +}): Messages => + channels.reduce( + (prevVal, currVal) => ({ + ...prevVal, + [currVal.id]: { + ...messages, + availableDateText: + currVal.publicationDate && !currVal.isPublished + ? intl.formatMessage( + { + defaultMessage: "Will become available on {date}", + description: "channel publication date", + id: "channel publication date will become available" + }, + { + date: localizeDate(currVal.publicationDate, "L") + } + ) + : currVal.publicationDate + ? intl.formatMessage( + { + defaultMessage: "Visible since {date}", + description: "channel publication date", + id: "channel publication date visible since" + }, + { + date: localizeDate(currVal.publicationDate, "L") + } + ) + : currVal.isPublished + ? intl.formatMessage({ + defaultMessage: "Visible", + description: "channel publication status", + id: "channel visible" + }) + : intl.formatMessage({ + defaultMessage: "Hidden", + description: "channel publication status", + id: "channel hidden" + }), + availableLabel: intl.formatMessage({ + defaultMessage: "Available for purchase", + description: "product availability", + id: "product available for purchase" + }), + availableSecondLabel: intl.formatMessage( + { + defaultMessage: "will become available on {date}", + description: "product available for purchase date", + id: "will become available on" + }, + { + date: localizeDate(currVal.availableForPurchase, "L") + } + ), + hiddenSecondLabel: intl.formatMessage( + { + defaultMessage: "will become published on {date}", + description: "product publication date label", + id: "will become published on" + }, + { + date: localizeDate(currVal.publicationDate, "L") + } + ), + setAvailabilityDateLabel: intl.formatMessage({ + defaultMessage: "Set availability date", + description: "product availability date label", + id: "set availability date" + }), + unavailableLabel: intl.formatMessage({ + defaultMessage: "Unavailable for purchase", + description: "product unavailability", + id: "unavailable for purchase" + }) + } + }), + {} as Messages + ); diff --git a/src/components/ChannelsAvailabilityContent/index.ts b/src/components/ChannelsAvailabilityContent/index.ts deleted file mode 100644 index 2ca745b1f..000000000 --- a/src/components/ChannelsAvailabilityContent/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./ChannelsAvailabilityContent"; -export { default } from "./ChannelsAvailabilityContent"; diff --git a/src/components/ChannelsAvailabilityDialog/ChannelsAvailabilityDialog.tsx b/src/components/ChannelsAvailabilityDialog/ChannelsAvailabilityDialog.tsx index 8f26c0e88..64cf6a3b6 100644 --- a/src/components/ChannelsAvailabilityDialog/ChannelsAvailabilityDialog.tsx +++ b/src/components/ChannelsAvailabilityDialog/ChannelsAvailabilityDialog.tsx @@ -1,15 +1,18 @@ import { Channel } from "@saleor/channels/utils"; import ActionDialog from "@saleor/components/ActionDialog"; -import { ChannelsAvailabilityContent } from "@saleor/components/ChannelsAvailabilityContent"; import { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton"; import React from "react"; +import ChannelsAvailabilityDialogChannelsList from "../ChannelsAvailabilityDialogChannelsList"; +import ChannelsAvailabilityDialogWrapper from "../ChannelsAvailabilityDialogWrapper"; +import { useChannelsSearch } from "./utils"; + export interface ChannelsAvailabilityDialogProps { isSelected: (option: Channel) => boolean; channels: Channel[]; confirmButtonState: ConfirmButtonTransitionState; contentType?: string; - disabled: boolean; + disabled?: boolean; open: boolean; onClose: () => void; onChange: (option: Channel) => void; @@ -23,34 +26,49 @@ export const ChannelsAvailabilityDialog: React.FC ( - - { + const { query, onQueryChange, filteredChannels } = useChannelsSearch( + channels + ); + + const handleToggleAll = () => toggleAll(channels, selected); + + const hasAllSelected = selected === channels.length; + + return ( + - -); -ChannelsAvailabilityDialog.displayName = "ChannelsAvailabilityDialog"; + > + + + + + ); +}; + export default ChannelsAvailabilityDialog; diff --git a/src/components/ChannelsAvailabilityDialog/utils.ts b/src/components/ChannelsAvailabilityDialog/utils.ts new file mode 100644 index 000000000..d3c5b7d2a --- /dev/null +++ b/src/components/ChannelsAvailabilityDialog/utils.ts @@ -0,0 +1,11 @@ +import { filter } from "fuzzaldrin"; +import React from "react"; + +export const useChannelsSearch = function( + channels: T[] +) { + const [query, onQueryChange] = React.useState(""); + const filteredChannels = filter(channels, query, { key: "name" }); + + return { query, onQueryChange, filteredChannels }; +}; diff --git a/src/components/ChannelsAvailabilityDialogChannelsList/ChannelsAvailabilityDialogChannelsList.tsx b/src/components/ChannelsAvailabilityDialogChannelsList/ChannelsAvailabilityDialogChannelsList.tsx new file mode 100644 index 000000000..b2925177b --- /dev/null +++ b/src/components/ChannelsAvailabilityDialogChannelsList/ChannelsAvailabilityDialogChannelsList.tsx @@ -0,0 +1,61 @@ +import { makeStyles } from "@material-ui/core"; +import Typography from "@material-ui/core/Typography"; +import { Channel } from "@saleor/channels/utils"; +import { ControlledCheckbox } from "@saleor/components/ControlledCheckbox"; +import Hr from "@saleor/components/Hr"; +import React from "react"; + +export const useStyles = makeStyles( + theme => ({ + label: { + fontSize: 14 + }, + option: { + "&:last-child": { + "& hr": { + display: "none" + } + }, + margin: theme.spacing(1, 0) + } + }), + { name: "ChannelsAvailabilityContent" } +); + +export interface ChannelsAvailabilityContentProps { + isChannelSelected: (channel: Channel) => boolean; + channels: Channel[]; + onChange: (option: Channel) => void; +} + +const ChannelsAvailabilityContent: React.FC = ({ + isChannelSelected, + channels, + onChange +}) => { + const classes = useStyles({}); + + return ( + <> + {channels.map(option => ( +
+ {option.name} + } + onChange={() => onChange(option)} + /> +
+
+ ))} + + ); +}; + +export default ChannelsAvailabilityContent; diff --git a/src/components/ChannelsAvailabilityDialogChannelsList/index.ts b/src/components/ChannelsAvailabilityDialogChannelsList/index.ts new file mode 100644 index 000000000..03929d1bb --- /dev/null +++ b/src/components/ChannelsAvailabilityDialogChannelsList/index.ts @@ -0,0 +1,2 @@ +export * from "./ChannelsAvailabilityDialogChannelsList"; +export { default } from "./ChannelsAvailabilityDialogChannelsList"; diff --git a/src/components/ChannelsAvailabilityDialogWrapper/ChannelsAvailabilityDialogWrapper.tsx b/src/components/ChannelsAvailabilityDialogWrapper/ChannelsAvailabilityDialogWrapper.tsx new file mode 100644 index 000000000..faa118841 --- /dev/null +++ b/src/components/ChannelsAvailabilityDialogWrapper/ChannelsAvailabilityDialogWrapper.tsx @@ -0,0 +1,147 @@ +import { makeStyles } from "@material-ui/core"; +import TextField from "@material-ui/core/TextField"; +import Typography from "@material-ui/core/Typography"; +import { ControlledCheckbox } from "@saleor/components/ControlledCheckbox"; +import Hr from "@saleor/components/Hr"; +import Label from "@saleor/orders/components/OrderHistory/Label"; +import React from "react"; +import { useIntl } from "react-intl"; +import { defineMessages, FormattedMessage } from "react-intl"; + +export const useStyles = makeStyles( + theme => ({ + content: { + "& hr": { + left: -24, + position: "relative", + width: "calc(100% + 48px)" + } + }, + contentTitle: { + margin: theme.spacing(1, 0) + }, + dialog: { + marginBottom: -30, + marginTop: theme.spacing(2) + }, + input: { + "& label": { + overflowX: "inherit" + } + }, + notFound: { + paddingBottom: theme.spacing(2) + }, + scrollArea: { + maxHeight: 400, + overflowY: "scroll" + }, + text: { + marginBottom: 5 + } + }), + { name: "ChannelsAvailabilityContent" } +); + +const messages = defineMessages({ + selectTitle: { + defaultMessage: + "Select channels you want for {contentType} to be available on", + description: "select title" + }, + selectAllChannelsLabel: { + defaultMessage: "Select All Channels", + description: "select all channels label" + }, + channelsAlphabeticallyTitle: { + defaultMessage: "Channels from A to Z", + description: "channels alphabetically title" + }, + notFoundTitle: { + defaultMessage: "No Channels Found", + description: "no channels found title" + } +}); + +export interface ChannelsAvailabilityContentProps { + contentType?: string; + toggleAll?: () => void; + children: React.ReactNode; + toggleAllLabel?: React.ReactNode; + query: string; + onQueryChange: (query: string) => void; + hasAnyChannelsToDisplay: boolean; + hasAllSelected: boolean; +} + +export const ChannelsAvailabilityContentWrapper: React.FC = ({ + contentType = "", + toggleAll, + toggleAllLabel, + children, + hasAnyChannelsToDisplay, + query, + onQueryChange, + hasAllSelected +}) => { + const classes = useStyles({}); + const intl = useIntl(); + const searchText = intl.formatMessage({ + defaultMessage: "Search through channels" + }); + + return ( +
+ {!!contentType && ( + + + + )} + onQueryChange(e.target.value)} + label={searchText} + placeholder={searchText} + fullWidth + /> +
+ {!!toggleAll && ( + <> + + ) + } + onChange={toggleAll} + /> +
+ + )} + + + +
+ {hasAnyChannelsToDisplay ? ( + children + ) : ( +
+ +
+ )} +
+
+
+ ); +}; + +export default ChannelsAvailabilityContentWrapper; diff --git a/src/components/ChannelsAvailabilityDialogWrapper/index.ts b/src/components/ChannelsAvailabilityDialogWrapper/index.ts new file mode 100644 index 000000000..e7fc7a56c --- /dev/null +++ b/src/components/ChannelsAvailabilityDialogWrapper/index.ts @@ -0,0 +1,2 @@ +export * from "./ChannelsAvailabilityDialogWrapper"; +export { default } from "./ChannelsAvailabilityDialogWrapper"; diff --git a/src/components/ControlledCheckbox.tsx b/src/components/ControlledCheckbox.tsx index 97c7c2410..3d60f76f0 100644 --- a/src/components/ControlledCheckbox.tsx +++ b/src/components/ControlledCheckbox.tsx @@ -8,6 +8,7 @@ interface ControlledCheckboxProps { label?: React.ReactNode; checked: boolean; disabled?: boolean; + checkedIcon?: React.ReactNode; onChange(event: any); } @@ -17,12 +18,14 @@ export const ControlledCheckbox: React.FC = ({ name, label, onChange, + checkedIcon, ...props }) => ( undefined} />); diff --git a/src/components/DeletableItem/DeletableItem.tsx b/src/components/DeletableItem/DeletableItem.tsx new file mode 100644 index 000000000..ed28b6a11 --- /dev/null +++ b/src/components/DeletableItem/DeletableItem.tsx @@ -0,0 +1,35 @@ +import { makeStyles } from "@material-ui/core/styles"; +import TrashIcon from "@saleor/icons/Trash"; +import React from "react"; + +const useStyles = makeStyles( + theme => ({ + container: { + cursor: "pointer", + padding: theme.spacing(3), + display: "flex", + justifyContent: "center", + alignItems: "center" + } + }), + { name: "DeletableItem" } +); + +interface DeletableItemProps { + onDelete: (id: string) => void; + id: string; +} + +const DeletableItem: React.FC = ({ onDelete, id }) => { + const classes = useStyles({}); + + const handleDelete = () => onDelete(id); + + return ( +
+ +
+ ); +}; + +export default DeletableItem; diff --git a/src/components/DeletableItem/index.tsx b/src/components/DeletableItem/index.tsx new file mode 100644 index 000000000..816523cc0 --- /dev/null +++ b/src/components/DeletableItem/index.tsx @@ -0,0 +1,2 @@ +export * from "./DeletableItem"; +export { default } from "./DeletableItem"; diff --git a/src/components/SingleAutocompleteSelectField/SingleAutocompleteSelectField.stories.tsx b/src/components/SingleAutocompleteSelectField/SingleAutocompleteSelectField.stories.tsx index 791cf77e8..72ce287f2 100644 --- a/src/components/SingleAutocompleteSelectField/SingleAutocompleteSelectField.stories.tsx +++ b/src/components/SingleAutocompleteSelectField/SingleAutocompleteSelectField.stories.tsx @@ -31,7 +31,7 @@ const Story: React.FC> = ({ allowCustomValues, emptyOption, enableLoadMore }) => { +>> = ({ allowCustomValues, emptyOption, enableLoadMore, nakedInput }) => { const [displayValue, setDisplayValue] = React.useState(suggestions[0].label); return ( @@ -59,6 +59,7 @@ const Story: React.FC ); }} @@ -104,6 +105,7 @@ storiesOf("Generics / Select with autocomplete", module) .add("no data", () => ( )) + .add("naked", () => ) .add("interactive", () => ) .add("interactive with custom option", () => ( diff --git a/src/components/SingleAutocompleteSelectField/SingleAutocompleteSelectField.tsx b/src/components/SingleAutocompleteSelectField/SingleAutocompleteSelectField.tsx index 8ab523cc2..be27d6b8a 100644 --- a/src/components/SingleAutocompleteSelectField/SingleAutocompleteSelectField.tsx +++ b/src/components/SingleAutocompleteSelectField/SingleAutocompleteSelectField.tsx @@ -1,4 +1,5 @@ import { InputProps } from "@material-ui/core/Input"; +import InputBase from "@material-ui/core/InputBase"; import TextField from "@material-ui/core/TextField"; import { ExtendedFormHelperTextProps } from "@saleor/channels/components/ChannelForm/types"; import { makeStyles } from "@saleor/theme"; @@ -16,12 +17,15 @@ import SingleAutocompleteSelectFieldContent, { } from "./SingleAutocompleteSelectFieldContent"; const useStyles = makeStyles( - { + theme => ({ container: { flexGrow: 1, position: "relative" + }, + nakedInput: { + padding: theme.spacing(2, 3) } - }, + }), { name: "SingleAutocompleteSelectField" } ); @@ -44,6 +48,7 @@ export interface SingleAutocompleteSelectFieldProps fetchChoices?: (value: string) => void; onChange: (event: React.ChangeEvent) => void; FormHelperTextProps?: ExtendedFormHelperTextProps; + nakedInput?: boolean; } const DebounceAutocomplete: React.ComponentType + +
+ ), + error, + id: undefined, + onBlur: handleBlur, + onClick: toggleMenu + }; + + const nakedInputProps = nakedInput + ? { + "aria-label": "naked", + ...commonInputProps, + autoFocus: true, + className: classes.nakedInput + } + : {}; + return (
- - -
- ), - error, - id: undefined, - onBlur: handleBlur, - onClick: toggleMenu - }} + ({ + alignRight: { + justifyContent: "flex-end" + }, + avatar: { + background: "none", + border: `1px solid ${theme.palette.divider}`, + borderRadius: 2, + color: "#bdbdbd", + display: "inline-flex", + padding: theme.spacing(0.5) + }, + children: { + alignSelf: "center", + marginLeft: theme.spacing(2), + width: "100%" + }, + content: { + alignItems: "center", + display: "flex" + }, + root: { + "&:not(first-child)": { + paddingLeft: 0 + }, + paddingRight: theme.spacing(3), + width: "1%" + } + }), + { name: "Avatar" } +); + +export interface AvatarProps { + thumbnail?: string; + alignRight?: boolean; + avatarProps?: string; + children?: React.ReactNode | React.ReactNodeArray; +} + +const Avatar: React.FC = ({ + children, + alignRight, + thumbnail, + avatarProps +}) => { + const classes = useStyles({}); + + return ( +
+ {thumbnail === undefined ? ( + + + + ) : thumbnail === null ? ( + + + + ) : ( + + )} + {!alignRight &&
{children}
} +
+ ); +}; + +export default Avatar; diff --git a/src/components/TableCellAvatar/TableCellAvatar.tsx b/src/components/TableCellAvatar/TableCellAvatar.tsx index 39460d3df..bee967026 100644 --- a/src/components/TableCellAvatar/TableCellAvatar.tsx +++ b/src/components/TableCellAvatar/TableCellAvatar.tsx @@ -1,36 +1,12 @@ -import Avatar from "@material-ui/core/Avatar"; import TableCell, { TableCellProps } from "@material-ui/core/TableCell"; -import Cached from "@material-ui/icons/Cached"; import { makeStyles } from "@saleor/theme"; import classNames from "classnames"; import React from "react"; -import Image from "../../icons/Image"; - -export const AVATAR_MARGIN = 32; +import Avatar, { AvatarProps } from "./Avatar"; const useStyles = makeStyles( theme => ({ - alignRight: { - justifyContent: "flex-end" - }, - avatar: { - background: "none", - border: `1px solid ${theme.palette.divider}`, - borderRadius: 2, - color: "#bdbdbd", - display: "inline-flex", - padding: theme.spacing(0.5) - }, - children: { - alignSelf: "center", - marginLeft: theme.spacing(2), - width: "100%" - }, - content: { - alignItems: "center", - display: "flex" - }, root: { "&:not(first-child)": { paddingLeft: 0 @@ -42,51 +18,20 @@ const useStyles = makeStyles( { name: "TableCellAvatar" } ); -interface TableCellAvatarProps extends TableCellProps { +interface TableCellAvatarProps extends AvatarProps, TableCellProps { className?: string; - thumbnail?: string; - alignRight?: boolean; - avatarProps?: string; - children?: React.ReactNode | React.ReactNodeArray; } const TableCellAvatar: React.FC = props => { - const { - children, - className, - alignRight, - thumbnail, - avatarProps, - ...rest - } = props; + const { className, ...rest } = props; const classes = useStyles(props); return ( -
- {thumbnail === undefined ? ( - - - - ) : thumbnail === null ? ( - - - - ) : ( - - )} - {!alignRight &&
{children}
} -
+
); }; -TableCellAvatar.displayName = "TableCellAvatar"; + export default TableCellAvatar; diff --git a/src/discounts/components/DiscountProducts/DiscountProducts.tsx b/src/discounts/components/DiscountProducts/DiscountProducts.tsx index 3ac72f735..951e95eba 100644 --- a/src/discounts/components/DiscountProducts/DiscountProducts.tsx +++ b/src/discounts/components/DiscountProducts/DiscountProducts.tsx @@ -11,9 +11,8 @@ import { ChannelsAvailabilityDropdown } from "@saleor/components/ChannelsAvailab import Checkbox from "@saleor/components/Checkbox"; import ResponsiveTable from "@saleor/components/ResponsiveTable"; import Skeleton from "@saleor/components/Skeleton"; -import TableCellAvatar, { - AVATAR_MARGIN -} from "@saleor/components/TableCellAvatar"; +import TableCellAvatar from "@saleor/components/TableCellAvatar"; +import { AVATAR_MARGIN } from "@saleor/components/TableCellAvatar/Avatar"; import TableHead from "@saleor/components/TableHead"; import TablePagination from "@saleor/components/TablePagination"; import { makeStyles } from "@saleor/theme"; diff --git a/src/discounts/components/SaleCreatePage/SaleCreatePage.tsx b/src/discounts/components/SaleCreatePage/SaleCreatePage.tsx index b39100bd6..8d71241a8 100644 --- a/src/discounts/components/SaleCreatePage/SaleCreatePage.tsx +++ b/src/discounts/components/SaleCreatePage/SaleCreatePage.tsx @@ -1,7 +1,7 @@ import { ChannelSaleData } from "@saleor/channels/utils"; import AppHeader from "@saleor/components/AppHeader"; import CardSpacer from "@saleor/components/CardSpacer"; -import ChannelsAvailability from "@saleor/components/ChannelsAvailability"; +import ChannelsAvailabilityCard from "@saleor/components/ChannelsAvailabilityCard"; import { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton"; import Container from "@saleor/components/Container"; import Form from "@saleor/components/Form"; @@ -117,7 +117,7 @@ const SaleCreatePage: React.FC = ({ />
- ({ diff --git a/src/discounts/components/SaleDetailsPage/SaleDetailsPage.tsx b/src/discounts/components/SaleDetailsPage/SaleDetailsPage.tsx index f2d1dcb2b..ea11fa26e 100644 --- a/src/discounts/components/SaleDetailsPage/SaleDetailsPage.tsx +++ b/src/discounts/components/SaleDetailsPage/SaleDetailsPage.tsx @@ -1,7 +1,7 @@ import { ChannelSaleData } from "@saleor/channels/utils"; import AppHeader from "@saleor/components/AppHeader"; import CardSpacer from "@saleor/components/CardSpacer"; -import ChannelsAvailability from "@saleor/components/ChannelsAvailability"; +import ChannelsAvailabilityCard from "@saleor/components/ChannelsAvailabilityCard"; import { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton"; import Container from "@saleor/components/Container"; import Form from "@saleor/components/Form"; @@ -293,7 +293,7 @@ const SaleDetailsPage: React.FC = ({ sale={sale} /> - ({ diff --git a/src/discounts/components/VoucherCreatePage/VoucherCreatePage.tsx b/src/discounts/components/VoucherCreatePage/VoucherCreatePage.tsx index 3e97b6f81..119267e21 100644 --- a/src/discounts/components/VoucherCreatePage/VoucherCreatePage.tsx +++ b/src/discounts/components/VoucherCreatePage/VoucherCreatePage.tsx @@ -1,7 +1,7 @@ import { ChannelVoucherData } from "@saleor/channels/utils"; import AppHeader from "@saleor/components/AppHeader"; import CardSpacer from "@saleor/components/CardSpacer"; -import ChannelsAvailability from "@saleor/components/ChannelsAvailability"; +import ChannelsAvailabilityCard from "@saleor/components/ChannelsAvailabilityCard"; import { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton"; import Container from "@saleor/components/Container"; import Form from "@saleor/components/Form"; @@ -170,7 +170,7 @@ const VoucherCreatePage: React.FC = ({ />
- ({ diff --git a/src/discounts/components/VoucherDetailsPage/VoucherDetailsPage.tsx b/src/discounts/components/VoucherDetailsPage/VoucherDetailsPage.tsx index 189af5aa0..e0ea0ee3f 100644 --- a/src/discounts/components/VoucherDetailsPage/VoucherDetailsPage.tsx +++ b/src/discounts/components/VoucherDetailsPage/VoucherDetailsPage.tsx @@ -2,7 +2,7 @@ import Typography from "@material-ui/core/Typography"; import { ChannelVoucherData } from "@saleor/channels/utils"; import AppHeader from "@saleor/components/AppHeader"; import CardSpacer from "@saleor/components/CardSpacer"; -import ChannelsAvailability from "@saleor/components/ChannelsAvailability"; +import ChannelsAvailabilityCard from "@saleor/components/ChannelsAvailabilityCard"; import { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton"; import Container from "@saleor/components/Container"; import CountryList from "@saleor/components/CountryList"; @@ -394,7 +394,7 @@ const VoucherDetailsPage: React.FC = ({ selectedChannelId={selectedChannelId} /> - ({ diff --git a/src/fragments/channels.ts b/src/fragments/channels.ts index 494dec1cd..a2512ff9d 100644 --- a/src/fragments/channels.ts +++ b/src/fragments/channels.ts @@ -1,5 +1,7 @@ import gql from "graphql-tag"; +import { shippingZoneFragment } from "./shipping"; + export const channelErrorFragment = gql` fragment ChannelErrorFragment on ChannelError { code @@ -20,9 +22,12 @@ export const channelFragment = gql` export const channelDetailsFragment = gql` ${channelFragment} - + ${shippingZoneFragment} fragment ChannelDetailsFragment on Channel { ...ChannelFragment hasOrders + shippingZones { + ...ShippingZoneFragment + } } `; diff --git a/src/fragments/products.ts b/src/fragments/products.ts index e2fb2273f..e2434bdee 100644 --- a/src/fragments/products.ts +++ b/src/fragments/products.ts @@ -202,6 +202,9 @@ export const productFragmentDetails = gql` sku name margin + media { + url(size: 200) + } stocks { ...StockFragment } @@ -256,6 +259,7 @@ export const selectedVariantAttributeFragment = gql` `; export const fragmentVariant = gql` + ${fragmentProductMedia} ${selectedVariantAttributeFragment} ${priceRangeFragment} ${fragmentProductMedia} diff --git a/src/fragments/types/ChannelDetailsFragment.ts b/src/fragments/types/ChannelDetailsFragment.ts index c29a147e2..305cac159 100644 --- a/src/fragments/types/ChannelDetailsFragment.ts +++ b/src/fragments/types/ChannelDetailsFragment.ts @@ -7,6 +7,34 @@ // GraphQL fragment: ChannelDetailsFragment // ==================================================== +export interface ChannelDetailsFragment_shippingZones_metadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + +export interface ChannelDetailsFragment_shippingZones_privateMetadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + +export interface ChannelDetailsFragment_shippingZones_countries { + __typename: "CountryDisplay"; + code: string; + country: string; +} + +export interface ChannelDetailsFragment_shippingZones { + __typename: "ShippingZone"; + metadata: (ChannelDetailsFragment_shippingZones_metadata | null)[]; + privateMetadata: (ChannelDetailsFragment_shippingZones_privateMetadata | null)[]; + id: string; + countries: (ChannelDetailsFragment_shippingZones_countries | null)[] | null; + name: string; + description: string | null; +} + export interface ChannelDetailsFragment { __typename: "Channel"; id: string; @@ -15,4 +43,5 @@ export interface ChannelDetailsFragment { slug: string; currencyCode: string; hasOrders: boolean; + shippingZones: ChannelDetailsFragment_shippingZones[]; } diff --git a/src/fragments/types/Product.ts b/src/fragments/types/Product.ts index 14ef89acd..ac389a1be 100644 --- a/src/fragments/types/Product.ts +++ b/src/fragments/types/Product.ts @@ -186,6 +186,11 @@ export interface Product_media { oembedData: any; } +export interface Product_variants_media { + __typename: "ProductMedia"; + url: string; +} + export interface Product_variants_stocks_warehouse { __typename: "Warehouse"; id: string; @@ -232,6 +237,7 @@ export interface Product_variants { sku: string; name: string; margin: number | null; + media: Product_variants_media[] | null; stocks: (Product_variants_stocks | null)[] | null; trackInventory: boolean; channelListings: Product_variants_channelListings[] | null; diff --git a/src/fragments/types/ShippingZoneDetailsFragment.ts b/src/fragments/types/ShippingZoneDetailsFragment.ts index 991928aa2..f2f30d470 100644 --- a/src/fragments/types/ShippingZoneDetailsFragment.ts +++ b/src/fragments/types/ShippingZoneDetailsFragment.ts @@ -124,5 +124,5 @@ export interface ShippingZoneDetailsFragment { description: string | null; default: boolean; shippingMethods: (ShippingZoneDetailsFragment_shippingMethods | null)[] | null; - warehouses: (ShippingZoneDetailsFragment_warehouses | null)[] | null; + warehouses: ShippingZoneDetailsFragment_warehouses[]; } diff --git a/src/hooks/makeTopLevelSearch/index.ts b/src/hooks/makeTopLevelSearch/index.ts new file mode 100644 index 000000000..658103603 --- /dev/null +++ b/src/hooks/makeTopLevelSearch/index.ts @@ -0,0 +1,2 @@ +export { default } from "./makeTopLevelSearch"; +export * from "./makeTopLevelSearch"; diff --git a/src/hooks/makeTopLevelSearch.ts b/src/hooks/makeTopLevelSearch/makeTopLevelSearch.ts similarity index 89% rename from src/hooks/makeTopLevelSearch.ts rename to src/hooks/makeTopLevelSearch/makeTopLevelSearch.ts index 0e81770b7..0ff6c4a3f 100644 --- a/src/hooks/makeTopLevelSearch.ts +++ b/src/hooks/makeTopLevelSearch/makeTopLevelSearch.ts @@ -1,7 +1,7 @@ import { PageInfoFragment } from "@saleor/fragments/types/PageInfoFragment"; import { DocumentNode } from "graphql"; -import makeSearch, { SearchVariables, UseSearchHook } from "./makeSearch"; +import makeSearch, { SearchVariables, UseSearchHook } from "../makeSearch"; export interface SearchData { search: { @@ -12,6 +12,10 @@ export interface SearchData { }; } +export interface ResultSearchData { + data: SearchData; +} + function makeTopLevelSearch< TData extends SearchData, TVariables extends SearchVariables diff --git a/src/hooks/makeTopLevelSearch/types.ts b/src/hooks/makeTopLevelSearch/types.ts new file mode 100644 index 000000000..b4670e8e0 --- /dev/null +++ b/src/hooks/makeTopLevelSearch/types.ts @@ -0,0 +1,12 @@ +export interface CommonSearchOpts { + onLoadMore?: () => void; + loading?: boolean; + data?: { + search?: { + totalCount?: number; + pageInfo?: { + hasNextPage: boolean; + }; + }; + }; +} diff --git a/src/hooks/makeTopLevelSearch/utils.ts b/src/hooks/makeTopLevelSearch/utils.ts new file mode 100644 index 000000000..61d2e8058 --- /dev/null +++ b/src/hooks/makeTopLevelSearch/utils.ts @@ -0,0 +1,17 @@ +import { FetchMoreProps } from "@saleor/types"; + +import { ResultSearchData } from "./makeTopLevelSearch"; +import { CommonSearchOpts } from "./types"; + +export const getSearchFetchMoreProps = ( + { data, loading }: CommonSearchOpts, + onFetchMore: any +): FetchMoreProps => ({ + hasMore: !!data?.search?.pageInfo?.hasNextPage, + totalCount: data?.search?.totalCount, + loading: !!loading, + onFetchMore +}); + +export const getParsedSearchData = ({ data }: ResultSearchData) => + data?.search?.edges?.map(({ node }) => node) || []; diff --git a/src/hooks/useDateLocalize.ts b/src/hooks/useDateLocalize.ts index 635e7b765..6820f15ea 100644 --- a/src/hooks/useDateLocalize.ts +++ b/src/hooks/useDateLocalize.ts @@ -2,7 +2,9 @@ import { LocaleContext } from "@saleor/components/Locale"; import moment from "moment-timezone"; import { useContext } from "react"; -function useDateLocalize(): (date: string, format?: string) => string { +export type LocalizeDate = (date: string, format?: string) => string; + +function useDateLocalize(): LocalizeDate { const { locale } = useContext(LocaleContext); return (date: string, format?: string) => diff --git a/src/hooks/useForm.ts b/src/hooks/useForm.ts index cb2df629e..6dfa66831 100644 --- a/src/hooks/useForm.ts +++ b/src/hooks/useForm.ts @@ -100,11 +100,12 @@ function useForm( setData(initial); } - function set(newData: Partial) { + function set(newData: Partial, setHasChanged = true) { setData(data => ({ ...data, ...newData })); + setChanged(setHasChanged); } async function submit() { diff --git a/src/hooks/useNotifier/index.ts b/src/hooks/useNotifier/index.ts new file mode 100644 index 000000000..4f87156dc --- /dev/null +++ b/src/hooks/useNotifier/index.ts @@ -0,0 +1,2 @@ +export { default } from "./useNotifier"; +export * from "./useNotifier"; diff --git a/src/hooks/useNotifier.ts b/src/hooks/useNotifier/useNotifier.ts similarity index 100% rename from src/hooks/useNotifier.ts rename to src/hooks/useNotifier/useNotifier.ts diff --git a/src/hooks/useNotifier/utils.ts b/src/hooks/useNotifier/utils.ts new file mode 100644 index 000000000..c0105c45b --- /dev/null +++ b/src/hooks/useNotifier/utils.ts @@ -0,0 +1,18 @@ +import { IMessage } from "@saleor/components/messages"; +import { commonMessages } from "@saleor/intl"; +import commonErrorMessages from "@saleor/utils/errors/common"; +import { IntlShape } from "react-intl"; + +export const getDefaultNotifierSuccessErrorData = ( + errors: any[], + intl: IntlShape +): IMessage => + !errors.length + ? { + status: "success", + text: intl.formatMessage(commonMessages.savedChanges) + } + : { + status: "error", + text: intl.formatMessage(commonErrorMessages.unknownError) + }; diff --git a/src/icons/CheckboxSemiChecked.tsx b/src/icons/CheckboxSemiChecked.tsx new file mode 100644 index 000000000..30a8d336e --- /dev/null +++ b/src/icons/CheckboxSemiChecked.tsx @@ -0,0 +1,12 @@ +import createSvgIcon from "@material-ui/icons/utils/createSvgIcon"; +import React from "react"; + +const CheckboxSemiChecked = createSvgIcon( + + + + , + "CheckboxSemiChecked" +); + +export default CheckboxSemiChecked; diff --git a/src/icons/ChevronDown.tsx b/src/icons/ChevronDown.tsx new file mode 100644 index 000000000..f07eb96b7 --- /dev/null +++ b/src/icons/ChevronDown.tsx @@ -0,0 +1,25 @@ +import useTheme from "@saleor/hooks/useTheme"; +import React from "react"; + +const ChevronDown: React.FC = () => { + const { isDark } = useTheme(); + + return ( + + + + ); +}; + +export default ChevronDown; diff --git a/src/icons/ChevronUp.tsx b/src/icons/ChevronUp.tsx new file mode 100644 index 000000000..39d51dc04 --- /dev/null +++ b/src/icons/ChevronUp.tsx @@ -0,0 +1,16 @@ +import createSvgIcon from "@material-ui/icons/utils/createSvgIcon"; +import React from "react"; + +const ChevronUp = createSvgIcon( + + + , + "ChevronUp" +); + +export default ChevronUp; diff --git a/src/icons/Trash.tsx b/src/icons/Trash.tsx new file mode 100644 index 000000000..8d881a028 --- /dev/null +++ b/src/icons/Trash.tsx @@ -0,0 +1,30 @@ +import createSvgIcon from "@material-ui/icons/utils/createSvgIcon"; +import useTheme from "@saleor/hooks/useTheme"; +import React from "react"; + +const Trash: React.FC = () => { + const { isDark } = useTheme(); + + const TrashComponent = createSvgIcon( + + + , + "Trash" + ); + + return ; +}; + +export default Trash; diff --git a/src/orders/components/OrderDraftDetailsProducts/OrderDraftDetailsProducts.tsx b/src/orders/components/OrderDraftDetailsProducts/OrderDraftDetailsProducts.tsx index f09d071bf..872e5ad68 100644 --- a/src/orders/components/OrderDraftDetailsProducts/OrderDraftDetailsProducts.tsx +++ b/src/orders/components/OrderDraftDetailsProducts/OrderDraftDetailsProducts.tsx @@ -3,7 +3,7 @@ import TableCell from "@material-ui/core/TableCell"; import TableHead from "@material-ui/core/TableHead"; import TableRow from "@material-ui/core/TableRow"; import ResponsiveTable from "@saleor/components/ResponsiveTable"; -import { AVATAR_MARGIN } from "@saleor/components/TableCellAvatar"; +import { AVATAR_MARGIN } from "@saleor/components/TableCellAvatar/Avatar"; import { OrderLineDiscountConsumer, OrderLineDiscountContextConsumerProps diff --git a/src/orders/components/OrderDraftDetailsProducts/TableLine.tsx b/src/orders/components/OrderDraftDetailsProducts/TableLine.tsx index affdece21..997466061 100644 --- a/src/orders/components/OrderDraftDetailsProducts/TableLine.tsx +++ b/src/orders/components/OrderDraftDetailsProducts/TableLine.tsx @@ -5,9 +5,8 @@ import Typography from "@material-ui/core/Typography"; import DeleteIcon from "@material-ui/icons/Delete"; import Link from "@saleor/components/Link"; import Money from "@saleor/components/Money"; -import TableCellAvatar, { - AVATAR_MARGIN -} from "@saleor/components/TableCellAvatar"; +import TableCellAvatar from "@saleor/components/TableCellAvatar"; +import { AVATAR_MARGIN } from "@saleor/components/TableCellAvatar/Avatar"; import { OrderLineDiscountContextConsumerProps } from "@saleor/products/components/OrderDiscountProviders/OrderLineDiscountProvider"; import { makeStyles } from "@saleor/theme"; import React, { useRef } from "react"; diff --git a/src/orders/components/OrderFulfillmentDialog/OrderFulfillmentDialog.tsx b/src/orders/components/OrderFulfillmentDialog/OrderFulfillmentDialog.tsx index d057198b9..99a4a4202 100644 --- a/src/orders/components/OrderFulfillmentDialog/OrderFulfillmentDialog.tsx +++ b/src/orders/components/OrderFulfillmentDialog/OrderFulfillmentDialog.tsx @@ -15,9 +15,8 @@ import ConfirmButton, { import Form from "@saleor/components/Form"; import { FormSpacer } from "@saleor/components/FormSpacer"; import ResponsiveTable from "@saleor/components/ResponsiveTable"; -import TableCellAvatar, { - AVATAR_MARGIN -} from "@saleor/components/TableCellAvatar"; +import TableCellAvatar from "@saleor/components/TableCellAvatar"; +import { AVATAR_MARGIN } from "@saleor/components/TableCellAvatar/Avatar"; import { OrderErrorFragment } from "@saleor/fragments/types/OrderErrorFragment"; import { buttonMessages } from "@saleor/intl"; import { makeStyles } from "@saleor/theme"; diff --git a/src/orders/components/OrderProductAddDialog/OrderProductAddDialog.tsx b/src/orders/components/OrderProductAddDialog/OrderProductAddDialog.tsx index 5a11cabeb..9de4b629a 100644 --- a/src/orders/components/OrderProductAddDialog/OrderProductAddDialog.tsx +++ b/src/orders/components/OrderProductAddDialog/OrderProductAddDialog.tsx @@ -189,21 +189,49 @@ const OrderProductAddDialog: React.FC = props => { onClose: () => setVariants([]) }); + const isValidVariant = ({ + channelListings + }: SearchOrderVariant_search_edges_node_variants) => { + const currentListing = channelListings.find( + listing => listing.channel.id === selectedChannelId + ); + + const listingPrice = currentListing?.price?.amount; + + const isVariantPriceSet = + listingPrice !== null && listingPrice !== undefined; + + return !!currentListing && isVariantPriceSet; + }; + + const getValidProductVariants = ({ + variants + }: SearchOrderVariant_search_edges_node) => variants.filter(isValidVariant); + const productChoices = - products?.filter(product => product.variants?.length > 0) || []; + products?.filter(product => getValidProductVariants(product).length > 0) || + []; + const selectedVariantsToProductsMap = productChoices ? productChoices.map(product => - product.variants.map(variant => isVariantSelected(variant, variants)) + getValidProductVariants(product).map(variant => + isVariantSelected(variant, variants) + ) ) : []; + const productsWithAllVariantsSelected = productChoices ? productChoices.map(product => - hasAllVariantsSelected(product.variants, variants) + hasAllVariantsSelected(getValidProductVariants(product), variants) ) : []; const handleSubmit = () => onSubmit(variants); + const productChoicesWithValidVariants = productChoices.filter( + ({ variants }) => variants.some(isValidVariant) + ); + return ( = props => { {renderCollection( - productChoices, - (product, productIndex) => - product.variants.some(variant => - variant.channelListings.some( - listing => listing.channel.id === selectedChannelId - ) - ) ? ( - - - - - onProductAdd( - product, - productIndex, - productsWithAllVariantsSelected, - variants, - setVariants - ) - } - /> - - product.thumbnail.url)} + productChoicesWithValidVariants, + (product, productIndex) => ( + + + + + onProductAdd( + product, + productIndex, + productsWithAllVariantsSelected, + variants, + setVariants + ) + } /> - - {maybe(() => product.name)} - - - {maybe(() => product.variants, []).map( - (variant, variantIndex) => - variant.channelListings.some( - listing => listing.channel.id === selectedChannelId - ) ? ( - - - - - onVariantAdd( - variant, - variantIndex, - productIndex, - variants, - selectedVariantsToProductsMap, - setVariants - ) - } - /> - - -
{variant.name}
-
- -
-
- - {variant.channelListings.map( - listing => - listing.channel.id === - selectedChannelId && ( - - ) - )} - -
- ) : null - )} -
- ) : null, + + product.thumbnail.url)} + /> + + {maybe(() => product.name)} + +
+ {maybe(() => product.variants, []) + .filter(isValidVariant) + .map((variant, variantIndex) => ( + + + + + onVariantAdd( + variant, + variantIndex, + productIndex, + variants, + selectedVariantsToProductsMap, + setVariants + ) + } + /> + + +
{variant.name}
+
+ +
+
+ + {variant?.channelListings[0]?.price && ( + + )} + +
+ ))} +
+ ), () => ( diff --git a/src/orders/components/OrderProductsCardElements/OrderProductsTableRow.tsx b/src/orders/components/OrderProductsCardElements/OrderProductsTableRow.tsx index 04dba0f15..4075d3348 100644 --- a/src/orders/components/OrderProductsCardElements/OrderProductsTableRow.tsx +++ b/src/orders/components/OrderProductsCardElements/OrderProductsTableRow.tsx @@ -1,9 +1,8 @@ import { makeStyles, TableCell, TableRow } from "@material-ui/core"; import Money from "@saleor/components/Money"; import Skeleton from "@saleor/components/Skeleton"; -import TableCellAvatar, { - AVATAR_MARGIN -} from "@saleor/components/TableCellAvatar"; +import TableCellAvatar from "@saleor/components/TableCellAvatar"; +import { AVATAR_MARGIN } from "@saleor/components/TableCellAvatar/Avatar"; import { maybe } from "@saleor/misc"; import { OrderDetails_order_fulfillments_lines, diff --git a/src/orders/components/OrderReturnPage/utils.tsx b/src/orders/components/OrderReturnPage/utils.tsx index 98b534ce4..b57b2075e 100644 --- a/src/orders/components/OrderReturnPage/utils.tsx +++ b/src/orders/components/OrderReturnPage/utils.tsx @@ -95,3 +95,30 @@ export const getById = (idToCompare: string) => (obj: { id: string }) => export const getByUnmatchingId = (idToCompare: string) => (obj: { id: string; }) => obj.id !== idToCompare; + +const isIncludedInIds = function( + arrayToCompare: string[] | T[], + obj: { id: string } +) { + const isSimpleIdsArray = (arrayToCompare as string[]).every( + value => typeof value === "string" + ); + + const idsToCompare = isSimpleIdsArray + ? (arrayToCompare as string[]) + : ((arrayToCompare as T[]).map(({ id }) => id) as string[]); + + return idsToCompare.includes(obj.id); +}; + +export function getByIds( + arrayToCompare: string[] | T[] +) { + return (obj: { id: string }) => isIncludedInIds(arrayToCompare, obj); +} + +export function getByUnmatchingIds( + arrayToCompare: string[] | T[] +) { + return (obj: { id: string }) => !isIncludedInIds(arrayToCompare, obj); +} diff --git a/src/plugins/components/PluginsDetailsPage/PluginsDetailsPage.tsx b/src/plugins/components/PluginsDetailsPage/PluginsDetailsPage.tsx index bbab1b6ce..027ff0a87 100644 --- a/src/plugins/components/PluginsDetailsPage/PluginsDetailsPage.tsx +++ b/src/plugins/components/PluginsDetailsPage/PluginsDetailsPage.tsx @@ -76,7 +76,7 @@ const PluginsDetailsPage: React.FC = props => { return ( - {({ data, hasChanged, submit, set, triggerChange }) => { + {({ data, hasChanged, submit, set }) => { const onChange = (event: ChangeEvent) => { const { name, value } = event.target; const newData = { @@ -98,7 +98,6 @@ const PluginsDetailsPage: React.FC = props => { }); } - triggerChange(); set(newData); }; return ( diff --git a/src/products/components/OrderDiscountProviders/OrderDiscountProvider.tsx b/src/products/components/OrderDiscountProviders/OrderDiscountProvider.tsx index 2e14575fe..e64195eae 100644 --- a/src/products/components/OrderDiscountProviders/OrderDiscountProvider.tsx +++ b/src/products/components/OrderDiscountProviders/OrderDiscountProvider.tsx @@ -1,6 +1,7 @@ /* eslint-disable sort-keys */ import { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton"; import useNotifier from "@saleor/hooks/useNotifier"; +import { getDefaultNotifierSuccessErrorData } from "@saleor/hooks/useNotifier/utils"; import { OrderDiscountCommonInput } from "@saleor/orders/components/OrderDiscountCommonModal/types"; import { useOrderDiscountAddMutation, @@ -15,7 +16,7 @@ import React, { createContext } from "react"; import { useIntl } from "react-intl"; import { OrderDiscountConsumerCommonProps, OrderDiscountData } from "./types"; -import { getDiscountNotifierData, useDiscountDialog } from "./utils"; +import { useDiscountDialog } from "./utils"; import { getManualOrderDiscount, getParsedDiscountData } from "./utils"; export interface OrderDiscountContextConsumerProps @@ -68,7 +69,7 @@ export const OrderDiscountProvider: React.FC = ({ const handleDiscountDataSubmission = (errors: any[]) => { closeDialog(); - notify(getDiscountNotifierData(errors, intl)); + notify(getDefaultNotifierSuccessErrorData(errors, intl)); }; const addOrderDiscount = (data: OrderDiscountCommonInput) => diff --git a/src/products/components/OrderDiscountProviders/OrderLineDiscountProvider.tsx b/src/products/components/OrderDiscountProviders/OrderLineDiscountProvider.tsx index 4cf33acd7..a5f1b5e84 100644 --- a/src/products/components/OrderDiscountProviders/OrderLineDiscountProvider.tsx +++ b/src/products/components/OrderDiscountProviders/OrderLineDiscountProvider.tsx @@ -1,6 +1,7 @@ /* eslint-disable sort-keys */ import { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton"; import useNotifier from "@saleor/hooks/useNotifier"; +import { getDefaultNotifierSuccessErrorData } from "@saleor/hooks/useNotifier/utils"; import { OrderDiscountCommonInput } from "@saleor/orders/components/OrderDiscountCommonModal/types"; import { getById } from "@saleor/orders/components/OrderReturnPage/utils"; import { @@ -19,7 +20,7 @@ import { OrderLineDiscountConsumerProps, OrderLineDiscountData } from "./types"; -import { getDiscountNotifierData, useDiscountDialog } from "./utils"; +import { useDiscountDialog } from "./utils"; import { getOrderLineDiscount, getParsedDiscountData } from "./utils"; export interface OrderLineDiscountContextConsumerProps @@ -79,7 +80,7 @@ export const OrderLineDiscountProvider: React.FC = ({ const handleDiscountDataSubmission = (errors: any[]) => { closeDialog(); - notify(getDiscountNotifierData(errors, intl)); + notify(getDefaultNotifierSuccessErrorData(errors, intl)); }; const addOrUpdateOrderLineDiscount = (orderLineId: string) => ( diff --git a/src/products/components/OrderDiscountProviders/utils.ts b/src/products/components/OrderDiscountProviders/utils.ts index 5dd410a7e..64a8254b5 100644 --- a/src/products/components/OrderDiscountProviders/utils.ts +++ b/src/products/components/OrderDiscountProviders/utils.ts @@ -1,5 +1,3 @@ -import { IMessage } from "@saleor/components/messages"; -import { commonMessages } from "@saleor/intl"; import { OrderDiscountCommonInput } from "@saleor/orders/components/OrderDiscountCommonModal/types"; import { OrderDetails_order, @@ -7,9 +5,7 @@ import { OrderDetails_order_lines } from "@saleor/orders/types/OrderDetails"; import { OrderDiscountType } from "@saleor/types/globalTypes"; -import commonErrorMessages from "@saleor/utils/errors/common"; import { useState } from "react"; -import { IntlShape } from "react-intl"; import { OrderLineDiscountData } from "./types"; @@ -21,21 +17,6 @@ export const useDiscountDialog = () => { return { closeDialog, isDialogOpen, openDialog }; }; - -export const getDiscountNotifierData = ( - errors: any[], - intl: IntlShape -): IMessage => - !errors.length - ? { - status: "success", - text: intl.formatMessage(commonMessages.savedChanges) - } - : { - status: "error", - text: intl.formatMessage(commonErrorMessages.unknownError) - }; - export const getManualOrderDiscount = (order: OrderDetails_order) => order ? getOrderDiscount(order, OrderDiscountType.MANUAL) : null; diff --git a/src/products/components/ProductCreatePage/ProductCreatePage.tsx b/src/products/components/ProductCreatePage/ProductCreatePage.tsx index 4c071596a..90897f6f5 100644 --- a/src/products/components/ProductCreatePage/ProductCreatePage.tsx +++ b/src/products/components/ProductCreatePage/ProductCreatePage.tsx @@ -2,12 +2,13 @@ import { getAttributeValuesFromReferences, mergeAttributeValues } from "@saleor/attributes/utils/data"; +import CannotDefineChannelsAvailabilityCard from "@saleor/channels/components/CannotDefineChannelsAvailabilityCard/CannotDefineChannelsAvailabilityCard"; import { ChannelData } from "@saleor/channels/utils"; import AppHeader from "@saleor/components/AppHeader"; import AssignAttributeValueDialog from "@saleor/components/AssignAttributeValueDialog"; import Attributes, { AttributeInput } from "@saleor/components/Attributes"; -import AvailabilityCard from "@saleor/components/AvailabilityCard"; import CardSpacer from "@saleor/components/CardSpacer"; +import ChannelsAvailabilityCard from "@saleor/components/ChannelsAvailabilityCard"; import { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton"; import Container from "@saleor/components/Container"; import Grid from "@saleor/components/Grid"; @@ -307,26 +308,30 @@ export const ProductCreatePage: React.FC = ({ collectionsInputDisplayValue={selectedCollections} /> - + visibleLabel: intl.formatMessage({ + defaultMessage: "Published", + description: "product label" + }) + }} + errors={channelsErrors} + selectedChannelsCount={data.channelListings?.length || 0} + allChannelsCount={allChannelsCount} + channels={data.channelListings || []} + disabled={loading} + onChange={handlers.changeChannels} + openModal={openChannelsModal} + /> + ) : ( + + )} = ({ const intl = useIntl(); const [query, onQueryChange] = useSearchQuery(onFetch); const getFieldLabel = useProductExportFieldMessages(); + const { + query: channelsQuery, + onQueryChange: onChannelsQueryChange, + filteredChannels + } = useChannelsSearch(channels); const handleFieldChange = (event: ChangeEvent) => onChange({ @@ -283,6 +290,9 @@ const ProductExportDialogInfo: React.FC = ({ const selectedAllInventoryFields = selectedInventoryFields.length === inventoryFields.length; + const handleSelectAllChannels = () => + onSelectAllChannels(channels, channels.length); + return ( <> @@ -321,19 +331,21 @@ const ProductExportDialogInfo: React.FC = ({ } data-test="channels" > - - !!selectedChannels.find(channel => channel.id === option.id) - } - onChange={onChannelSelect} - toggleAll={onSelectAllChannels} - /> + + + !!selectedChannels.find(channel => channel.id === option.id) + } + onChange={onChannelSelect} + /> + ({ + container: { + paddingTop: theme.spacing(1), + paddingBottom: theme.spacing(1), + paddingLeft: theme.spacing(3), + paddingRight: theme.spacing(2), + display: "flex", + flexDirection: "row", + justifyContent: "space-between", + alignItems: "center", + width: "100%", + position: "relative" + } + }), + { name: "CardAddItemsFooter" } +); + +interface CardAddItemsFooterProps { + title: MessageDescriptor; + onAdd: () => void; + testIds: { + link: string; + button: string; + }; + ref?: MutableRefObject; +} + +const CardAddItemsFooter: React.FC = ({ + title, + onAdd, + testIds, + ref, + children +}) => { + const intl = useIntl(); + const classes = useStyles({}); + + return ( +
+ + {intl.formatMessage(title)} + + + + + {children} +
+ ); +}; + +export default CardAddItemsFooter; diff --git a/src/products/components/ProductUpdatePage/ProductUpdatePage.test.tsx b/src/products/components/ProductUpdatePage/ProductUpdatePage.test.tsx index 0047a0e78..caee93953 100644 --- a/src/products/components/ProductUpdatePage/ProductUpdatePage.test.tsx +++ b/src/products/components/ProductUpdatePage/ProductUpdatePage.test.tsx @@ -14,10 +14,6 @@ import ProductUpdatePage, { ProductUpdatePageProps } from "./ProductUpdatePage"; const product = productFixture(placeholderImage); const channels = createChannelsData(channelsList); -const channelChoices = product.channelListings.map(listing => ({ - label: listing.channel.name, - value: listing.channel.id -})); import Adapter from "enzyme-adapter-react-16"; configure({ adapter: new Adapter() }); @@ -28,7 +24,10 @@ const props: ProductUpdatePageProps = { ...listActionsProps, allChannelsCount: 5, categories: [product.category], - channelChoices, + channelsData: [], + channelsWithVariantsData: {}, + isSimpleProduct: false, + setChannelsData: () => undefined, channelsErrors: [], collections, currentChannels: channels, diff --git a/src/products/components/ProductUpdatePage/ProductUpdatePage.tsx b/src/products/components/ProductUpdatePage/ProductUpdatePage.tsx index b8cc69ba4..e900bf934 100644 --- a/src/products/components/ProductUpdatePage/ProductUpdatePage.tsx +++ b/src/products/components/ProductUpdatePage/ProductUpdatePage.tsx @@ -7,8 +7,8 @@ import { ChannelData } from "@saleor/channels/utils"; import AppHeader from "@saleor/components/AppHeader"; import AssignAttributeValueDialog from "@saleor/components/AssignAttributeValueDialog"; import Attributes, { AttributeInput } from "@saleor/components/Attributes"; -import AvailabilityCard from "@saleor/components/AvailabilityCard"; import CardSpacer from "@saleor/components/CardSpacer"; +import ChannelsAvailabilityCard from "@saleor/components/ChannelsAvailabilityCard"; import { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton"; import Container from "@saleor/components/Container"; import Grid from "@saleor/components/Grid"; @@ -17,7 +17,6 @@ import PageHeader from "@saleor/components/PageHeader"; import SaveButtonBar from "@saleor/components/SaveButtonBar"; import SeoForm from "@saleor/components/SeoForm"; import { RefreshLimits_shop_limits } from "@saleor/components/Shop/types/RefreshLimits"; -import { SingleAutocompleteChoiceType } from "@saleor/components/SingleAutocompleteSelectField"; import { ProductChannelListingErrorFragment } from "@saleor/fragments/types/ProductChannelListingErrorFragment"; import { ProductErrorWithAttributesFragment } from "@saleor/fragments/types/ProductErrorWithAttributesFragment"; import { TaxTypeFragment } from "@saleor/fragments/types/TaxTypeFragment"; @@ -29,6 +28,7 @@ import { sectionNames } from "@saleor/intl"; import { maybe } from "@saleor/misc"; import ProductExternalMediaDialog from "@saleor/products/components/ProductExternalMediaDialog"; import ProductVariantPrice from "@saleor/products/components/ProductVariantPrice"; +import { ChannelsWithVariantsData } from "@saleor/products/views/ProductUpdate/types"; import { SearchCategories_search_edges_node } from "@saleor/searches/types/SearchCategories"; import { SearchCollections_search_edges_node } from "@saleor/searches/types/SearchCollections"; import { SearchPages_search_edges_node } from "@saleor/searches/types/SearchPages"; @@ -42,6 +42,7 @@ import { import React from "react"; import { useIntl } from "react-intl"; +import ChannelsWithVariantsAvailabilityCard from "../../../channels/ChannelsWithVariantsAvailabilityCard/ChannelsWithVariantsAvailabilityCard"; import { ProductDetails_product, ProductDetails_product_media, @@ -61,12 +62,15 @@ import ProductUpdateForm, { } from "./form"; export interface ProductUpdatePageProps extends ListActions, ChannelProps { + channelsWithVariantsData: ChannelsWithVariantsData; + setChannelsData: (data: ChannelData[]) => void; + onChannelsChange: (data: ChannelData[]) => void; + channelsData: ChannelData[]; + currentChannels: ChannelData[]; + allChannelsCount: number; + channelsErrors: ProductChannelListingErrorFragment[]; defaultWeightUnit: string; errors: ProductErrorWithAttributesFragment[]; - channelsErrors: ProductChannelListingErrorFragment[]; - allChannelsCount: number; - currentChannels: ChannelData[]; - channelChoices: SingleAutocompleteChoiceType[]; placeholderImage: string; collections: SearchCollections_search_edges_node[]; categories: SearchCategories_search_edges_node[]; @@ -88,6 +92,7 @@ export interface ProductUpdatePageProps extends ListActions, ChannelProps { assignReferencesAttributeId?: string; fetchMoreReferencePages?: FetchMoreProps; fetchMoreReferenceProducts?: FetchMoreProps; + isSimpleProduct: boolean; fetchCategories: (query: string) => void; fetchCollections: (query: string) => void; fetchReferencePages?: (data: string) => void; @@ -100,7 +105,6 @@ export interface ProductUpdatePageProps extends ListActions, ChannelProps { onImageDelete: (id: string) => () => void; onSubmit: (data: ProductUpdatePageSubmitData) => SubmitPromise; openChannelsModal: () => void; - onChannelsChange: (data: ChannelData[]) => void; onBack?(); onDelete(); onImageEdit?(id: string); @@ -113,9 +117,7 @@ export interface ProductUpdatePageProps extends ListActions, ChannelProps { onWarehouseConfigure(); } -export interface ProductUpdatePageSubmitData - extends ProductUpdatePageFormData, - ChannelProps { +export interface ProductUpdatePageSubmitData extends ProductUpdatePageFormData { addStocks: ProductStockInput[]; attributes: AttributeInput[]; attributesWithNewFileValue: FormsetData; @@ -130,9 +132,8 @@ export const ProductUpdatePage: React.FC = ({ disabled, categories: categoryChoiceList, channelsErrors, - allChannelsCount, - currentChannels = [], collections: collectionChoiceList, + isSimpleProduct, errors, fetchCategories, fetchCollections, @@ -147,21 +148,24 @@ export const ProductUpdatePage: React.FC = ({ saveButtonBarState, variants, warehouses, + setChannelsData, taxTypes, referencePages = [], referenceProducts = [], onBack, onDelete, + allChannelsCount, + currentChannels, onImageDelete, onImageEdit, onImageReorder, onImageUpload, onMediaUrlUpload, - onChannelsChange, openChannelsModal, onSeoClick, onSubmit, onVariantAdd, + channelsData, onVariantsAdd, onSetDefaultVariant, onVariantShow, @@ -180,7 +184,9 @@ export const ProductUpdatePage: React.FC = ({ fetchMoreReferencePages, fetchReferenceProducts, fetchMoreReferenceProducts, - onCloseDialog + onCloseDialog, + channelsWithVariantsData, + onChannelsChange }) => { const intl = useIntl(); @@ -229,10 +235,15 @@ export const ProductUpdatePage: React.FC = ({ return ( = ({ setChannels={onChannelsChange} taxTypes={taxTypeChoices} warehouses={warehouses} - currentChannels={currentChannels} hasVariants={hasVariants} referencePages={referencePages} referenceProducts={referenceProducts} @@ -299,7 +309,7 @@ export const ProductUpdatePage: React.FC = ({ /> )} - {!!product?.productType && !hasVariants && ( + {isSimpleProduct && ( <> = ({ onCollectionChange={handlers.selectCollection} /> - + visibleLabel: intl.formatMessage({ + defaultMessage: "Published", + description: "product label" + }) + }} + errors={channelsErrors} + selectedChannelsCount={data.channelListings.length} + allChannelsCount={allChannelsCount} + channels={data.channelListings} + disabled={disabled} + onChange={handlers.changeChannels} + openModal={openChannelsModal} + /> + ) : ( + + )} >; setSelectedTaxType: React.Dispatch>; - setChannels: (channels: ChannelData[]) => void; selectedCollections: MultiAutocompleteChoiceType[]; warehouses: SearchWarehouses_search_edges_node[]; - currentChannels: ChannelData[]; + channelsData: ChannelData[]; hasVariants: boolean; + currentChannels: ChannelData[]; + setChannels: (data: ChannelData[]) => void; + setChannelsData: (data: ChannelData[]) => void; referencePages: SearchPages_search_edges_node[]; referenceProducts: SearchProducts_search_edges_node[]; fetchReferencePages?: (data: string) => void; @@ -154,6 +159,8 @@ export interface UseProductUpdateFormOpts fetchReferenceProducts?: (data: string) => void; fetchMoreReferenceProducts?: FetchMoreProps; assignReferencesAttributeId?: string; + channelsWithVariants: ChannelsWithVariantsData; + isSimpleProduct: boolean; } export interface ProductUpdateFormProps extends UseProductUpdateFormOpts { @@ -198,7 +205,9 @@ function useProductUpdateForm( getProductUpdatePageFormData( product, product?.variants, - opts.currentChannels + opts.currentChannels, + opts.channelsData, + opts.channelsWithVariants ) ); const attributes = useFormset(getAttributeInputFromProduct(product)); @@ -292,19 +301,23 @@ function useProductUpdateForm( opts.taxTypes ); const changeMetadata = makeMetadataChangeHandler(handleChange); + const handleChannelsChange = createChannelsChangeHandler( - opts.currentChannels, - opts.setChannels, + opts.isSimpleProduct ? opts.currentChannels : opts.channelsData, + opts.isSimpleProduct ? opts.setChannels : opts.setChannelsData, triggerChange ); + const handleChannelPriceChange = createChannelsPriceChangeHandler( - opts.currentChannels, - opts.setChannels, + opts.isSimpleProduct ? opts.currentChannels : opts.channelsData, + opts.isSimpleProduct ? opts.setChannels : opts.setChannelsData, triggerChange ); const data: ProductUpdateData = { ...form.data, + channelListings: opts.currentChannels, + channelsData: opts.channelsData, attributes: getAttributesDisplayData( attributes.data, attributesWithNewFileValue.data, @@ -314,6 +327,7 @@ function useProductUpdateForm( description: description.current, stocks: stocks.data }; + // Need to make it function to always have description.current up to date const getSubmitData = (): ProductUpdateSubmitData => ({ ...data, diff --git a/src/products/components/ProductVariantPrice/ProductVariantPrice.tsx b/src/products/components/ProductVariantPrice/ProductVariantPrice.tsx index 1e3ee19a8..ebeb16817 100644 --- a/src/products/components/ProductVariantPrice/ProductVariantPrice.tsx +++ b/src/products/components/ProductVariantPrice/ProductVariantPrice.tsx @@ -7,7 +7,7 @@ import { } from "@material-ui/core"; import Card from "@material-ui/core/Card"; import CardContent from "@material-ui/core/CardContent"; -import { ChannelPriceArgs, ChannelPriceData } from "@saleor/channels/utils"; +import { ChannelData, ChannelPriceArgs } from "@saleor/channels/utils"; import CardTitle from "@saleor/components/CardTitle"; import PriceField from "@saleor/components/PriceField"; import ResponsiveTable from "@saleor/components/ResponsiveTable"; @@ -62,7 +62,7 @@ const useStyles = makeStyles( ); interface ProductVariantPriceProps { - ProductVariantChannelListings: ChannelPriceData[]; + ProductVariantChannelListings: ChannelData[]; errors: ProductChannelListingErrorFragment[]; loading?: boolean; onChange: (id: string, data: ChannelPriceArgs) => void; diff --git a/src/products/types/ProductChannelListingUpdate.ts b/src/products/types/ProductChannelListingUpdate.ts index 312fbb71f..00c3d915c 100644 --- a/src/products/types/ProductChannelListingUpdate.ts +++ b/src/products/types/ProductChannelListingUpdate.ts @@ -186,6 +186,11 @@ export interface ProductChannelListingUpdate_productChannelListingUpdate_product oembedData: any; } +export interface ProductChannelListingUpdate_productChannelListingUpdate_product_variants_media { + __typename: "ProductMedia"; + url: string; +} + export interface ProductChannelListingUpdate_productChannelListingUpdate_product_variants_stocks_warehouse { __typename: "Warehouse"; id: string; @@ -232,6 +237,7 @@ export interface ProductChannelListingUpdate_productChannelListingUpdate_product sku: string; name: string; margin: number | null; + media: ProductChannelListingUpdate_productChannelListingUpdate_product_variants_media[] | null; stocks: (ProductChannelListingUpdate_productChannelListingUpdate_product_variants_stocks | null)[] | null; trackInventory: boolean; channelListings: ProductChannelListingUpdate_productChannelListingUpdate_product_variants_channelListings[] | null; diff --git a/src/products/types/ProductCreate.ts b/src/products/types/ProductCreate.ts index 387fb7184..a52ad3f0d 100644 --- a/src/products/types/ProductCreate.ts +++ b/src/products/types/ProductCreate.ts @@ -193,6 +193,11 @@ export interface ProductCreate_productCreate_product_media { oembedData: any; } +export interface ProductCreate_productCreate_product_variants_media { + __typename: "ProductMedia"; + url: string; +} + export interface ProductCreate_productCreate_product_variants_stocks_warehouse { __typename: "Warehouse"; id: string; @@ -239,6 +244,7 @@ export interface ProductCreate_productCreate_product_variants { sku: string; name: string; margin: number | null; + media: ProductCreate_productCreate_product_variants_media[] | null; stocks: (ProductCreate_productCreate_product_variants_stocks | null)[] | null; trackInventory: boolean; channelListings: ProductCreate_productCreate_product_variants_channelListings[] | null; diff --git a/src/products/types/ProductDetails.ts b/src/products/types/ProductDetails.ts index 824578fd0..661fed2dd 100644 --- a/src/products/types/ProductDetails.ts +++ b/src/products/types/ProductDetails.ts @@ -186,6 +186,11 @@ export interface ProductDetails_product_media { oembedData: any; } +export interface ProductDetails_product_variants_media { + __typename: "ProductMedia"; + url: string; +} + export interface ProductDetails_product_variants_stocks_warehouse { __typename: "Warehouse"; id: string; @@ -232,6 +237,7 @@ export interface ProductDetails_product_variants { sku: string; name: string; margin: number | null; + media: ProductDetails_product_variants_media[] | null; stocks: (ProductDetails_product_variants_stocks | null)[] | null; trackInventory: boolean; channelListings: ProductDetails_product_variants_channelListings[] | null; diff --git a/src/products/types/ProductMediaCreate.ts b/src/products/types/ProductMediaCreate.ts index 192358dcd..6c1176e95 100644 --- a/src/products/types/ProductMediaCreate.ts +++ b/src/products/types/ProductMediaCreate.ts @@ -192,6 +192,11 @@ export interface ProductMediaCreate_productMediaCreate_product_media { oembedData: any; } +export interface ProductMediaCreate_productMediaCreate_product_variants_media { + __typename: "ProductMedia"; + url: string; +} + export interface ProductMediaCreate_productMediaCreate_product_variants_stocks_warehouse { __typename: "Warehouse"; id: string; @@ -238,6 +243,7 @@ export interface ProductMediaCreate_productMediaCreate_product_variants { sku: string; name: string; margin: number | null; + media: ProductMediaCreate_productMediaCreate_product_variants_media[] | null; stocks: (ProductMediaCreate_productMediaCreate_product_variants_stocks | null)[] | null; trackInventory: boolean; channelListings: ProductMediaCreate_productMediaCreate_product_variants_channelListings[] | null; diff --git a/src/products/types/ProductMediaUpdate.ts b/src/products/types/ProductMediaUpdate.ts index 9ec1e8d56..2fed1599e 100644 --- a/src/products/types/ProductMediaUpdate.ts +++ b/src/products/types/ProductMediaUpdate.ts @@ -192,6 +192,11 @@ export interface ProductMediaUpdate_productMediaUpdate_product_media { oembedData: any; } +export interface ProductMediaUpdate_productMediaUpdate_product_variants_media { + __typename: "ProductMedia"; + url: string; +} + export interface ProductMediaUpdate_productMediaUpdate_product_variants_stocks_warehouse { __typename: "Warehouse"; id: string; @@ -238,6 +243,7 @@ export interface ProductMediaUpdate_productMediaUpdate_product_variants { sku: string; name: string; margin: number | null; + media: ProductMediaUpdate_productMediaUpdate_product_variants_media[] | null; stocks: (ProductMediaUpdate_productMediaUpdate_product_variants_stocks | null)[] | null; trackInventory: boolean; channelListings: ProductMediaUpdate_productMediaUpdate_product_variants_channelListings[] | null; diff --git a/src/products/types/ProductUpdate.ts b/src/products/types/ProductUpdate.ts index 34bcbeef9..93ce99645 100644 --- a/src/products/types/ProductUpdate.ts +++ b/src/products/types/ProductUpdate.ts @@ -193,6 +193,11 @@ export interface ProductUpdate_productUpdate_product_media { oembedData: any; } +export interface ProductUpdate_productUpdate_product_variants_media { + __typename: "ProductMedia"; + url: string; +} + export interface ProductUpdate_productUpdate_product_variants_stocks_warehouse { __typename: "Warehouse"; id: string; @@ -239,6 +244,7 @@ export interface ProductUpdate_productUpdate_product_variants { sku: string; name: string; margin: number | null; + media: ProductUpdate_productUpdate_product_variants_media[] | null; stocks: (ProductUpdate_productUpdate_product_variants_stocks | null)[] | null; trackInventory: boolean; channelListings: ProductUpdate_productUpdate_product_variants_channelListings[] | null; diff --git a/src/products/types/ProductVariantReorder.ts b/src/products/types/ProductVariantReorder.ts index 7e2410b80..bb291f5ce 100644 --- a/src/products/types/ProductVariantReorder.ts +++ b/src/products/types/ProductVariantReorder.ts @@ -192,6 +192,11 @@ export interface ProductVariantReorder_productVariantReorder_product_media { oembedData: any; } +export interface ProductVariantReorder_productVariantReorder_product_variants_media { + __typename: "ProductMedia"; + url: string; +} + export interface ProductVariantReorder_productVariantReorder_product_variants_stocks_warehouse { __typename: "Warehouse"; id: string; @@ -238,6 +243,7 @@ export interface ProductVariantReorder_productVariantReorder_product_variants { sku: string; name: string; margin: number | null; + media: ProductVariantReorder_productVariantReorder_product_variants_media[] | null; stocks: (ProductVariantReorder_productVariantReorder_product_variants_stocks | null)[] | null; trackInventory: boolean; channelListings: ProductVariantReorder_productVariantReorder_product_variants_channelListings[] | null; diff --git a/src/products/types/ProductVariantSetDefault.ts b/src/products/types/ProductVariantSetDefault.ts index 779483e5d..9edfddb02 100644 --- a/src/products/types/ProductVariantSetDefault.ts +++ b/src/products/types/ProductVariantSetDefault.ts @@ -192,6 +192,11 @@ export interface ProductVariantSetDefault_productVariantSetDefault_product_media oembedData: any; } +export interface ProductVariantSetDefault_productVariantSetDefault_product_variants_media { + __typename: "ProductMedia"; + url: string; +} + export interface ProductVariantSetDefault_productVariantSetDefault_product_variants_stocks_warehouse { __typename: "Warehouse"; id: string; @@ -238,6 +243,7 @@ export interface ProductVariantSetDefault_productVariantSetDefault_product_varia sku: string; name: string; margin: number | null; + media: ProductVariantSetDefault_productVariantSetDefault_product_variants_media[] | null; stocks: (ProductVariantSetDefault_productVariantSetDefault_product_variants_stocks | null)[] | null; trackInventory: boolean; channelListings: ProductVariantSetDefault_productVariantSetDefault_product_variants_channelListings[] | null; diff --git a/src/products/types/SimpleProductUpdate.ts b/src/products/types/SimpleProductUpdate.ts index 236938ed1..36531d900 100644 --- a/src/products/types/SimpleProductUpdate.ts +++ b/src/products/types/SimpleProductUpdate.ts @@ -193,6 +193,11 @@ export interface SimpleProductUpdate_productUpdate_product_media { oembedData: any; } +export interface SimpleProductUpdate_productUpdate_product_variants_media { + __typename: "ProductMedia"; + url: string; +} + export interface SimpleProductUpdate_productUpdate_product_variants_stocks_warehouse { __typename: "Warehouse"; id: string; @@ -239,6 +244,7 @@ export interface SimpleProductUpdate_productUpdate_product_variants { sku: string; name: string; margin: number | null; + media: SimpleProductUpdate_productUpdate_product_variants_media[] | null; stocks: (SimpleProductUpdate_productUpdate_product_variants_stocks | null)[] | null; trackInventory: boolean; channelListings: SimpleProductUpdate_productUpdate_product_variants_channelListings[] | null; diff --git a/src/products/utils/data.ts b/src/products/utils/data.ts index 802cbe69f..b1b6d31ec 100644 --- a/src/products/utils/data.ts +++ b/src/products/utils/data.ts @@ -22,6 +22,7 @@ import { mapMetadataItemToInput } from "@saleor/utils/maps"; import { ProductStockInput } from "../components/ProductStocks"; import { ProductType_productType_productAttributes } from "../types/ProductType"; import { ProductVariantCreateData_product } from "../types/ProductVariantCreateData"; +import { ChannelsWithVariantsData } from "../views/ProductUpdate/types"; export interface Collection { id: string; @@ -204,7 +205,9 @@ 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; @@ -222,17 +225,21 @@ export interface ProductUpdatePageFormData extends MetadataFormData { export function getProductUpdatePageFormData( product: ProductDetails_product, variants: ProductDetails_product_variants[], - currentChannels: ChannelData[] + currentChannels: ChannelData[], + channelsData: ChannelData[], + channelsWithVariants: ChannelsWithVariantsData ): ProductUpdatePageFormData { return { + channelsWithVariants, + channelsData, category: maybe(() => product.category.id, ""), changeTaxCode: !!product?.taxType.taxCode, - channelListings: currentChannels, chargeTaxes: maybe(() => product.chargeTaxes, false), collections: maybe( () => product.collections.map(collection => collection.id), [] ), + channelListings: currentChannels, 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 017c14fa2..49f9666cc 100644 --- a/src/products/utils/handlers.ts +++ b/src/products/utils/handlers.ts @@ -6,8 +6,8 @@ import { import { FormChange } from "@saleor/hooks/useForm"; export function createChannelsPriceChangeHandler( - channelListings: ChannelPriceData[], - updateChannels: (data: ChannelPriceData[]) => void, + channelListings: ChannelData[], + updateChannels: (data: ChannelData[]) => void, triggerChange: () => void ) { return (id: string, priceData: ChannelPriceArgs) => { @@ -27,12 +27,13 @@ export function createChannelsPriceChangeHandler( ...channelListings.slice(channelIndex + 1) ]; updateChannels(updatedChannels); + triggerChange(); }; } export function createChannelsChangeHandler( - channelListings: ChannelData[], + channelsData: ChannelData[], updateChannels: (data: ChannelData[]) => void, triggerChange: () => void ) { @@ -40,20 +41,20 @@ export function createChannelsChangeHandler( id: string, data: Omit ) => { - const channelIndex = channelListings.findIndex( - channel => channel.id === id - ); - const channel = channelListings[channelIndex]; + const channelIndex = channelsData.findIndex(channel => channel.id === id); + const channel = channelsData[channelIndex]; const updatedChannels = [ - ...channelListings.slice(0, channelIndex), + ...channelsData.slice(0, channelIndex), { ...channel, ...data }, - ...channelListings.slice(channelIndex + 1) + ...channelsData.slice(channelIndex + 1) ]; + updateChannels(updatedChannels); + triggerChange(); }; } diff --git a/src/products/views/ProductCreate/handlers.ts b/src/products/views/ProductCreate/handlers.ts index ea2ddeda2..16099dff0 100644 --- a/src/products/views/ProductCreate/handlers.ts +++ b/src/products/views/ProductCreate/handlers.ts @@ -44,7 +44,7 @@ const getChannelsVariables = (productId: string, channels: ChannelData[]) => ({ variables: { id: productId, input: { - addChannels: getAvailabilityVariables(channels) + updateChannels: getAvailabilityVariables(channels) } } }); diff --git a/src/products/views/ProductUpdate/ProductUpdate.tsx b/src/products/views/ProductUpdate/ProductUpdate.tsx index 202ca1ea2..407b27406 100644 --- a/src/products/views/ProductUpdate/ProductUpdate.tsx +++ b/src/products/views/ProductUpdate/ProductUpdate.tsx @@ -3,6 +3,7 @@ import DialogContentText from "@material-ui/core/DialogContentText"; import IconButton from "@material-ui/core/IconButton"; import DeleteIcon from "@material-ui/icons/Delete"; import { useAttributeValueDeleteMutation } from "@saleor/attributes/mutations"; +import ChannelsWithVariantsAvailabilityDialog from "@saleor/channels/components/ChannelsWithVariantsAvailabilityDialog"; import { useChannelsList } from "@saleor/channels/queries"; import { ChannelData, @@ -18,6 +19,7 @@ import { useShopLimitsQuery } from "@saleor/components/Shop/query"; import { WindowTitle } from "@saleor/components/WindowTitle"; import { DEFAULT_INITIAL_SEARCH_DATA } from "@saleor/config"; import { useFileUploadMutation } from "@saleor/files/mutations"; +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"; @@ -52,10 +54,12 @@ import { import { useWarehouseList } from "@saleor/warehouses/queries"; import { warehouseAddPath } from "@saleor/warehouses/urls"; import React from "react"; -import { FormattedMessage, useIntl } from "react-intl"; +import { defineMessages, FormattedMessage, useIntl } from "react-intl"; import { getMutationState } from "../../../misc"; -import ProductUpdatePage from "../../components/ProductUpdatePage"; +import ProductUpdatePage, { + ProductUpdatePageSubmitData +} from "../../components/ProductUpdatePage"; import { useProductDetails } from "../../queries"; import { ProductMediaCreateVariables } from "../../types/ProductMediaCreate"; import { ProductUpdate as ProductUpdateMutationResult } from "../../types/ProductUpdate"; @@ -75,6 +79,27 @@ import { createUpdateHandler, createVariantReorderHandler } from "./handlers"; +import useChannelsWithProductVariants from "./useChannelsWithProductVariants"; + +const messages = defineMessages({ + deleteProductDialogTitle: { + defaultMessage: "Delete Product", + description: "delete product dialog title" + }, + deleteProductDialogSubtitle: { + defaultMessage: "Are you sure you want to delete {name}?", + description: "delete product dialog subtitle" + }, + deleteVariantDialogTitle: { + defaultMessage: "Delete Product Variants", + description: "delete variant dialog title" + }, + deleteVariantDialogSubtitle: { + defaultMessage: + "{counter,plural,one{Are you sure you want to delete this variant?} other{Are you sure you want to delete {displayQuantity} variants?}}", + description: "delete variant dialog subtitle" + } +}); interface ProductUpdateProps { id: string; @@ -130,7 +155,7 @@ export const ProductUpdate: React.FC = ({ id, params }) => { productVariantCreateOpts ] = useVariantCreateMutation({}); - const { data: channelsData } = useChannelsList({}); + const { data: channelsListData } = useChannelsList({}); const { data, loading, refetch } = useProductDetails({ displayLoader: true, @@ -228,14 +253,33 @@ export const ProductUpdate: React.FC = ({ id, params }) => { const allChannels: ChannelData[] = createChannelsDataWithPrice( product, - channelsData?.channels + channelsListData?.channels ).sort((channel, nextChannel) => channel.name.localeCompare(nextChannel.name) ); + const isSimpleProduct = !data?.product?.productType?.hasVariants; + + const { + channelsWithVariantsData, + haveChannelsWithVariantsDataChanged, + setHaveChannelsWithVariantsChanged, + onChannelsAvailiabilityModalOpen, + channelsData, + setChannelsData, + ...channelsWithVariantsProps + } = useChannelsWithProductVariants({ + channels: allChannels, + variants: product?.variants, + action: params?.action, + openModal, + closeModal + }); + const productChannelsChoices: ChannelData[] = createSortedChannelsDataFromProduct( product ); + const { channelListElements, channelsToggle, @@ -254,24 +298,17 @@ export const ProductUpdate: React.FC = ({ id, params }) => { const [updateChannels, updateChannelsOpts] = useProductChannelListingUpdate({ onCompleted: data => { - if (data.productChannelListingUpdate.errors.length === 0) { - const updatedProductChannelsChoices: ChannelData[] = createSortedChannelsDataFromProduct( - data.productChannelListingUpdate.product - ); - setCurrentChannels(updatedProductChannelsChoices); + if (!!data.productChannelListingUpdate.errors.length) { + return; } } }); + const [ updateVariantChannels, updateVariantChannelsOpts ] = useProductVariantChannelListingUpdate({}); - const channelChoices = product?.channelListings.map(listing => ({ - label: listing.channel.name, - value: listing.channel.id - })); - const [ createProductMedia, createProductMediaOpts @@ -328,6 +365,7 @@ export const ProductUpdate: React.FC = ({ id, params }) => { product, createUpdateHandler( product, + allChannels, variables => uploadFile({ variables }), variables => updateProduct({ variables }), variables => updateSimpleProduct({ variables }), @@ -409,61 +447,76 @@ export const ProductUpdate: React.FC = ({ id, params }) => { ?.errors || []) ]; - const fetchMoreCollections = { - hasMore: searchCollectionsOpts.data?.search?.pageInfo?.hasNextPage, - loading: searchCollectionsOpts.loading, - onFetchMore: loadMoreCollections - }; - const fetchMoreCategories = { - hasMore: searchCategoriesOpts.data?.search?.pageInfo?.hasNextPage, - loading: searchCategoriesOpts.loading, - onFetchMore: loadMoreCategories - }; - const fetchMoreReferencePages = { - hasMore: searchPagesOpts.data?.search?.pageInfo?.hasNextPage, - loading: searchPagesOpts.loading, - onFetchMore: loadMorePages - }; - const fetchMoreReferenceProducts = { - hasMore: searchProductsOpts.data?.search?.pageInfo?.hasNextPage, - loading: searchProductsOpts.loading, - onFetchMore: loadMoreProducts - }; + const fetchMoreCollections = getSearchFetchMoreProps( + searchCollectionsOpts, + loadMoreCollections + ); + + const fetchMoreCategories = getSearchFetchMoreProps( + searchCategoriesOpts, + loadMoreCategories + ); + + const fetchMoreReferencePages = getSearchFetchMoreProps( + searchPagesOpts, + loadMorePages + ); + + const fetchMoreReferenceProducts = getSearchFetchMoreProps( + searchProductsOpts, + loadMoreProducts + ); return ( <> - {!!allChannels?.length && ( - - )} + {!!allChannels?.length && + (isSimpleProduct ? ( + + ) : ( + + ))} = ({ id, params }) => { onDelete={() => openModal("remove")} onImageReorder={handleImageReorder} onMediaUrlUpload={handleMediaUrlUpload} - onSubmit={handleSubmit} + onSubmit={(formData: ProductUpdatePageSubmitData) => { + setHaveChannelsWithVariantsChanged(false); + return handleSubmit(formData); + }} onWarehouseConfigure={() => navigate(warehouseAddPath)} onVariantAdd={handleVariantAdd} onVariantsAdd={() => navigate(productVariantCreatorUrl(id))} @@ -510,8 +566,6 @@ export const ProductUpdate: React.FC = ({ id, params }) => { fetchMoreCategories={fetchMoreCategories} fetchMoreCollections={fetchMoreCollections} selectedChannelId={channel?.id} - openChannelsModal={handleChannelsModalOpen} - onChannelsChange={setCurrentChannels} assignReferencesAttributeId={ params.action === "assign-attribute-value" && params.id } @@ -534,18 +588,12 @@ export const ProductUpdate: React.FC = ({ id, params }) => { confirmButtonState={deleteProductOpts.status} onConfirm={() => deleteProduct({ variables: { id } })} variant="delete" - title={intl.formatMessage({ - defaultMessage: "Delete Product", - description: "dialog header" - })} + title={intl.formatMessage(messages.deleteProductDialogTitle)} > @@ -561,15 +609,11 @@ export const ProductUpdate: React.FC = ({ id, params }) => { }) } variant="delete" - title={intl.formatMessage({ - defaultMessage: "Delete Product Variants", - description: "dialog header" - })} + title={intl.formatMessage(messages.deleteVariantDialogTitle)} > {params?.ids?.length} diff --git a/src/products/views/ProductUpdate/handlers.ts b/src/products/views/ProductUpdate/handlers/index.ts similarity index 80% rename from src/products/views/ProductUpdate/handlers.ts rename to src/products/views/ProductUpdate/handlers/index.ts index 7e34b57df..a33f4efba 100644 --- a/src/products/views/ProductUpdate/handlers.ts +++ b/src/products/views/ProductUpdate/handlers/index.ts @@ -12,7 +12,7 @@ import { handleUploadMultipleFiles, prepareAttributesInput } from "@saleor/attributes/utils/handlers"; -import { createSortedChannelsDataFromProduct } from "@saleor/channels/utils"; +import { ChannelData } from "@saleor/channels/utils"; import { FileUpload, FileUploadVariables @@ -23,7 +23,6 @@ import { ProductChannelListingErrorFragment } from "@saleor/fragments/types/Prod import { ProductErrorFragment } from "@saleor/fragments/types/ProductErrorFragment"; import { StockErrorFragment } from "@saleor/fragments/types/StockErrorFragment"; import { UploadErrorFragment } from "@saleor/fragments/types/UploadErrorFragment"; -import { weight } from "@saleor/misc"; import { ProductUpdatePageSubmitData } from "@saleor/products/components/ProductUpdatePage"; import { ProductChannelListingUpdate, @@ -55,14 +54,20 @@ import { VariantCreateVariables } from "@saleor/products/types/VariantCreate"; import { mapFormsetStockToStockInput } from "@saleor/products/utils/data"; -import { getAvailabilityVariables } from "@saleor/products/utils/handlers"; import { getParsedDataForJsonStringField } from "@saleor/translations/utils"; import { ReorderEvent } from "@saleor/types"; import { move } from "@saleor/utils/lists"; -import { diff } from "fast-array-diff"; import { MutationFetchResult } from "react-apollo"; import { arrayMove } from "react-sortable-hoc"; +import { + getChannelsVariables, + getSimpleChannelsVariables, + getSimpleProductErrors, + getSimpleProductVariables, + getVariantChannelsInput +} from "./utils"; + type SubmitErrors = Array< | ProductErrorFragment | StockErrorFragment @@ -72,64 +77,9 @@ type SubmitErrors = Array< | ProductChannelListingErrorFragment >; -const getSimpleProductVariables = ( - productVariables: ProductUpdateVariables, - 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 - }, - updateStocks: data.updateStocks.map(mapFormsetStockToStockInput) -}); - -const getSimpleProductErrors = (data: SimpleProductUpdate) => [ - ...data.productUpdate.errors, - ...data.productVariantStocksCreate.errors, - ...data.productVariantStocksDelete.errors, - ...data.productVariantStocksUpdate.errors -]; - -const getChannelsVariables = ( - data: ProductUpdatePageSubmitData, - product: ProductDetails_product -) => { - const productChannels = createSortedChannelsDataFromProduct(product); - const diffChannels = diff( - productChannels, - data.channelListings, - (a, b) => a.id === b.id - ); - - return { - id: product.id, - input: { - addChannels: getAvailabilityVariables(data.channelListings), - removeChannels: diffChannels.removed?.map( - removedChannel => removedChannel.id - ) - } - }; -}; - -const getVariantChannelsInput = (data: ProductUpdatePageSubmitData) => - data.channelListings.map(listing => ({ - channelId: listing.id, - costPrice: listing.costPrice || null, - price: listing.price - })); - export function createUpdateHandler( product: ProductDetails_product, + allChannels: ChannelData[], uploadFile: ( variables: FileUploadVariables ) => Promise>, @@ -202,9 +152,7 @@ export function createUpdateHandler( const result = await updateProduct(productVariables); errors = [...errors, ...result.data.productUpdate.errors]; - await updateChannels({ - variables: getChannelsVariables(data, product) - }); + await updateChannels(getChannelsVariables(product, allChannels, data)); } else { if (!product.variants.length) { const productVariantResult = await productVariantCreate({ @@ -228,6 +176,7 @@ export function createUpdateHandler( const variantId = productVariantResult.data.productVariantCreate?.productVariant?.id; + if (variantId) { updateVariantChannels({ variables: { @@ -235,9 +184,11 @@ export function createUpdateHandler( input: getVariantChannelsInput(data) } }); - await updateChannels({ - variables: getChannelsVariables(data, product) - }); + + await updateChannels( + getChannelsVariables(product, allChannels, data) + ); + const result = await updateSimpleProduct( getSimpleProductVariables(productVariables, data, variantId) ); @@ -253,9 +204,8 @@ export function createUpdateHandler( ); errors = [...errors, ...getSimpleProductErrors(result.data)]; - await updateChannels({ - variables: getChannelsVariables(data, product) - }); + await updateChannels(getSimpleChannelsVariables(data, product)); + updateVariantChannels({ variables: { id: product.variants[0].id, diff --git a/src/products/views/ProductUpdate/handlers/utils.ts b/src/products/views/ProductUpdate/handlers/utils.ts new file mode 100644 index 000000000..d4e6d9ee5 --- /dev/null +++ b/src/products/views/ProductUpdate/handlers/utils.ts @@ -0,0 +1,193 @@ +import { + ChannelData, + createSortedChannelsDataFromProduct +} from "@saleor/channels/utils"; +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 { + ProductDetails_product, + ProductDetails_product_variants +} from "@saleor/products/types/ProductDetails"; +import { ProductUpdateVariables } from "@saleor/products/types/ProductUpdate"; +import { SimpleProductUpdate } from "@saleor/products/types/SimpleProductUpdate"; +import { mapFormsetStockToStockInput } from "@saleor/products/utils/data"; +import { getAvailabilityVariables } from "@saleor/products/utils/handlers"; +import { ProductChannelListingAddInput } from "@saleor/types/globalTypes"; +import { diff } from "fast-array-diff"; +import isEqual from "lodash/isEqual"; + +import { ChannelsWithVariantsData, ChannelWithVariantData } from "../types"; +import { getParsedChannelsWithVariantsDataFromChannels } from "../utils"; + +export const getSimpleProductVariables = ( + productVariables: ProductUpdateVariables, + 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 + }, + updateStocks: data.updateStocks.map(mapFormsetStockToStockInput) +}); + +export const getSimpleProductErrors = (data: SimpleProductUpdate) => [ + ...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: diff(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: ProductDetails_product_variants[] +) => ({ removeVariants }: ProductChannelListingAddInput) => + isRemovingAllVariants(allVariants, removeVariants); + +const isRemovingAllVariants = ( + allVariants: ProductDetails_product_variants[], + removeVariants: string[] +) => !!removeVariants.length && removeVariants.length === allVariants.length; + +const shouldUpdateChannel = ( + initialChannelWithVariantData, + allVariants: ProductDetails_product_variants[], + 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 }: ProductDetails_product, + 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, + product: ProductDetails_product +) => { + const productChannels = createSortedChannelsDataFromProduct(product); + const diffChannels = diff( + productChannels, + data.channelListings, + (a, b) => a.id === b.id + ); + + return { + variables: { + id: product.id, + input: { + updateChannels: getAvailabilityVariables(data.channelListings), + removeChannels: diffChannels.removed?.map( + removedChannel => removedChannel.id + ) + } + } + }; +}; + +export const getVariantChannelsInput = ({ + channelListings +}: ProductUpdatePageSubmitData) => + channelListings.map(listing => ({ + channelId: listing.id, + costPrice: listing.costPrice || null, + price: listing.price + })); diff --git a/src/products/views/ProductUpdate/types.ts b/src/products/views/ProductUpdate/types.ts new file mode 100644 index 000000000..2e5d2674d --- /dev/null +++ b/src/products/views/ProductUpdate/types.ts @@ -0,0 +1,44 @@ +import { ChannelsAction } from "@saleor/channels/urls"; +import { ChannelData } from "@saleor/channels/utils"; +import { ProductDetails_product_variants } from "@saleor/products/types/ProductDetails"; +import { ProductUrlDialog } from "@saleor/products/urls"; + +export interface UseChannelsWithProductVariantsProps { + channels: ChannelData[]; + variants: ProductDetails_product_variants[]; + action: ProductUrlDialog; + openModal: (action: ChannelsAction) => void; + closeModal: () => void; +} + +export interface ChannelWithVariantData { + selectedVariantsIds: string[]; + variantsIdsToRemove: string[]; + variantsIdsToAdd: string[]; +} + +export type ChannelsWithVariantsData = Record; + +export const initialChannelWithVariantData: ChannelWithVariantData = { + variantsIdsToRemove: [], + variantsIdsToAdd: [], + selectedVariantsIds: [] +}; + +export const CHANNELS_AVAILIABILITY_MODAL_SELECTOR = "open-channels-picker"; + +export interface UseChannelsWithProductVariants { + channelsData: ChannelData[]; + setChannelsData: (data: ChannelData[]) => void; + onChannelsWithVariantsConfirm: () => void; + addVariantToChannel: (channelId: string, variantId: string) => void; + removeVariantFromChannel: (channelId: string, variantId: string) => void; + channelsWithVariantsData: ChannelsWithVariantsData; + onChannelsAvailiabilityModalOpen: () => void; + onChannelsAvailiabilityModalClose: () => void; + isChannelsAvailabilityModalOpen: boolean; + toggleAllChannels: () => void; + toggleAllChannelVariants: (channelId: string) => () => void; + haveChannelsWithVariantsDataChanged: boolean; + setHaveChannelsWithVariantsChanged: (hasChanged: boolean) => void; +} diff --git a/src/products/views/ProductUpdate/useChannelsWithProductVariants.ts b/src/products/views/ProductUpdate/useChannelsWithProductVariants.ts new file mode 100644 index 000000000..2eae15ade --- /dev/null +++ b/src/products/views/ProductUpdate/useChannelsWithProductVariants.ts @@ -0,0 +1,140 @@ +import useStateFromProps from "@saleor/hooks/useStateFromProps"; +import isEmpty from "lodash-es/isEmpty"; +import reduce from "lodash-es/reduce"; +import { useEffect, useRef, useState } from "react"; + +import { + CHANNELS_AVAILIABILITY_MODAL_SELECTOR, + ChannelsWithVariantsData, + UseChannelsWithProductVariants, + UseChannelsWithProductVariantsProps +} from "./types"; +import { + areAllVariantsAtAllChannelsSelected, + areAnyChannelVariantsSelected, + getChannelVariantToggleData, + getChannelWithAddedVariantData, + getChannelWithRemovedVariantData, + getParsedChannelsWithVariantsDataFromChannels +} from "./utils"; + +const useChannelsWithProductVariants = ({ + channels, + variants, + openModal, + closeModal, + action +}: UseChannelsWithProductVariantsProps): UseChannelsWithProductVariants => { + const [channelsData, setChannelsData] = useStateFromProps(channels); + + const initialChannelsWithVariantsData = getParsedChannelsWithVariantsDataFromChannels( + channels + ); + + const [ + channelsWithVariantsData, + setChannelsWithVariantsData + ] = useStateFromProps( + initialChannelsWithVariantsData + ); + + const channelsWithVariantsDataRef = useRef(channelsWithVariantsData); + + const [hasChanged, setHasChanged] = useState(false); + + const handleSetHasChanged = () => { + const isDataRefEmpty = isEmpty(channelsWithVariantsDataRef.current); + const isDataEmpty = isEmpty(channelsWithVariantsData); + + const hasFilledInitialData = isDataRefEmpty && !isDataEmpty; + + const hasNoDataFilled = isDataRefEmpty && isDataEmpty; + + channelsWithVariantsDataRef.current = channelsWithVariantsData; + + if (hasNoDataFilled || hasFilledInitialData) { + return; + } + + setHasChanged(true); + }; + + useEffect(handleSetHasChanged, [channelsWithVariantsData]); + + const handleAddVariant = (channelId: string, variantId: string) => + setChannelsWithVariantsData({ + ...channelsWithVariantsData, + ...getChannelWithAddedVariantData({ + channelWithVariantsData: channelsWithVariantsData[channelId], + channelId, + variantId + }) + }); + + const handleRemoveVariant = (channelId: string, variantId: string) => + setChannelsWithVariantsData({ + ...channelsWithVariantsData, + ...getChannelWithRemovedVariantData({ + channelWithVariantsData: channelsWithVariantsData[channelId], + channelId, + variantId + }) + }); + + const toggleAllChannelVariants = (channelId: string) => () => { + const isChannelSelected = areAnyChannelVariantsSelected( + channelsWithVariantsData[channelId] + ); + + setChannelsWithVariantsData({ + ...channelsWithVariantsData, + [channelId]: getChannelVariantToggleData(variants, isChannelSelected) + }); + }; + + const toggleAllChannels = () => { + const areAllChannelsSelected = areAllVariantsAtAllChannelsSelected( + variants, + channelsWithVariantsData + ); + + const updatedData: ChannelsWithVariantsData = reduce( + channelsWithVariantsData, + (result, _, channelId) => ({ + ...result, + [channelId]: getChannelVariantToggleData( + variants, + areAllChannelsSelected + ) + }), + {} + ); + + setChannelsWithVariantsData(updatedData); + }; + + const onChannelsWithVariantsConfirm = () => closeModal(); + + const handleModalOpen = () => + openModal(CHANNELS_AVAILIABILITY_MODAL_SELECTOR); + + const isModalOpen = action === CHANNELS_AVAILIABILITY_MODAL_SELECTOR; + + return { + channelsWithVariantsData, + setChannelsData, + channelsData, + addVariantToChannel: handleAddVariant, + removeVariantFromChannel: handleRemoveVariant, + onChannelsAvailiabilityModalOpen: handleModalOpen, + onChannelsAvailiabilityModalClose: closeModal, + isChannelsAvailabilityModalOpen: isModalOpen, + haveChannelsWithVariantsDataChanged: hasChanged, + toggleAllChannelVariants, + toggleAllChannels, + onChannelsWithVariantsConfirm, + setHaveChannelsWithVariantsChanged: setHasChanged + }; +}; + +export default useChannelsWithProductVariants; diff --git a/src/products/views/ProductUpdate/utils.ts b/src/products/views/ProductUpdate/utils.ts new file mode 100644 index 000000000..db4dccc5f --- /dev/null +++ b/src/products/views/ProductUpdate/utils.ts @@ -0,0 +1,122 @@ +import { + getUpdatedIdsWithNewId, + getUpdatedIdsWithoutNewId +} from "@saleor/channels/pages/ChannelDetailsPage/utils"; +import { ChannelData } from "@saleor/channels/utils"; +import { ProductDetails_product_variants } from "@saleor/products/types/ProductDetails"; +import { reduce } from "lodash"; +import every from "lodash-es/every"; + +import { + ChannelsWithVariantsData, + ChannelWithVariantData, + initialChannelWithVariantData +} from "./types"; + +export const getParsedChannelsWithVariantsDataFromChannels = ( + channels: ChannelData[] +): ChannelsWithVariantsData => + channels?.reduce( + (result: ChannelsWithVariantsData, { id, variantsIds }) => ({ + ...result, + [id]: { + ...initialChannelWithVariantData, + selectedVariantsIds: variantsIds + } as ChannelWithVariantData + }), + {} + ); + +interface ChannelAddRemoveVariantCommonProps { + channelWithVariantsData: ChannelWithVariantData; + channelId: string; + variantId: string; +} + +export const getChannelWithAddedVariantData = ({ + channelWithVariantsData: { + selectedVariantsIds, + variantsIdsToAdd, + variantsIdsToRemove + }, + channelId, + variantId +}: ChannelAddRemoveVariantCommonProps): ChannelsWithVariantsData => ({ + [channelId]: { + selectedVariantsIds: getUpdatedIdsWithNewId(selectedVariantsIds, variantId), + variantsIdsToAdd: getUpdatedIdsWithNewId(variantsIdsToAdd, variantId), + variantsIdsToRemove: getUpdatedIdsWithoutNewId( + variantsIdsToRemove, + variantId + ) + } +}); + +export const getChannelWithRemovedVariantData = ({ + channelWithVariantsData: { + selectedVariantsIds, + variantsIdsToAdd, + variantsIdsToRemove + }, + channelId, + variantId +}: ChannelAddRemoveVariantCommonProps): ChannelsWithVariantsData => ({ + [channelId]: { + selectedVariantsIds: getUpdatedIdsWithoutNewId( + selectedVariantsIds, + variantId + ), + variantsIdsToRemove: getUpdatedIdsWithNewId(variantsIdsToRemove, variantId), + variantsIdsToAdd: getUpdatedIdsWithoutNewId(variantsIdsToAdd, variantId) + } +}); + +export const getChannelVariantToggleData = ( + variants: ProductDetails_product_variants[], + isSelected: boolean +): ChannelWithVariantData => { + const allProductVariantsIds = extractAllProductVariantsIds(variants); + + return isSelected + ? { + selectedVariantsIds: [], + variantsIdsToAdd: [], + variantsIdsToRemove: allProductVariantsIds + } + : { + selectedVariantsIds: allProductVariantsIds, + variantsIdsToAdd: allProductVariantsIds, + variantsIdsToRemove: [] + }; +}; + +export const extractAllProductVariantsIds = ( + productVariants: ProductDetails_product_variants[] = [] +) => productVariants.map(({ id }) => id); + +export const areAllVariantsAtAllChannelsSelected = ( + variants: ProductDetails_product_variants[] = [], + channelsWithVariants: ChannelsWithVariantsData = {} +) => + every(channelsWithVariants, channelWithVariantsData => + areAllChannelVariantsSelected(variants, channelWithVariantsData) + ); + +export const areAllChannelVariantsSelected = ( + variants: ProductDetails_product_variants[] = [], + { selectedVariantsIds }: Pick +) => selectedVariantsIds.length === variants.length; + +export const areAnyChannelVariantsSelected = ( + channelsWithVariantsData: ChannelWithVariantData +) => channelsWithVariantsData?.selectedVariantsIds.length > 0; + +export const getTotalSelectedChannelsCount = ( + channelsWithVariantsData: ChannelsWithVariantsData +) => + reduce( + channelsWithVariantsData, + (result, { selectedVariantsIds }) => + selectedVariantsIds.length ? result + 1 : result, + 0 + ); diff --git a/src/searches/types/SearchShippingZones.ts b/src/searches/types/SearchShippingZones.ts new file mode 100644 index 000000000..ccef17280 --- /dev/null +++ b/src/searches/types/SearchShippingZones.ts @@ -0,0 +1,46 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL query operation: SearchShippingZones +// ==================================================== + +export interface SearchShippingZones_search_edges_node { + __typename: "ShippingZone"; + id: string; + name: string; +} + +export interface SearchShippingZones_search_edges { + __typename: "ShippingZoneCountableEdge"; + node: SearchShippingZones_search_edges_node; +} + +export interface SearchShippingZones_search_pageInfo { + __typename: "PageInfo"; + endCursor: string | null; + hasNextPage: boolean; + hasPreviousPage: boolean; + startCursor: string | null; +} + +export interface SearchShippingZones_search { + __typename: "ShippingZoneCountableConnection"; + totalCount: number | null; + edges: SearchShippingZones_search_edges[]; + pageInfo: SearchShippingZones_search_pageInfo; +} + +export interface SearchShippingZones { + search: SearchShippingZones_search | null; +} + +export interface SearchShippingZonesVariables { + query: string; + first: number; + after?: string | null; + last?: number | null; + before?: string | null; +} diff --git a/src/searches/useShippingZonesSearch.ts b/src/searches/useShippingZonesSearch.ts new file mode 100644 index 000000000..71e075c6d --- /dev/null +++ b/src/searches/useShippingZonesSearch.ts @@ -0,0 +1,43 @@ +import { pageInfoFragment } from "@saleor/fragments/pageInfo"; +import makeTopLevelSearch from "@saleor/hooks/makeTopLevelSearch"; +import gql from "graphql-tag"; + +import { + SearchShippingZones, + SearchShippingZonesVariables +} from "./types/SearchShippingZones"; + +const searchShippingZones = gql` + ${pageInfoFragment} + query SearchShippingZones( + $query: String! + $first: Int! + $after: String + $last: Int + $before: String + ) { + search: shippingZones( + filter: { search: $query } + first: $first + after: $after + last: $last + before: $before + ) { + totalCount + edges { + node { + id + name + } + } + pageInfo { + ...PageInfoFragment + } + } + } +`; + +export default makeTopLevelSearch< + SearchShippingZones, + SearchShippingZonesVariables +>(searchShippingZones); diff --git a/src/shipping/components/ShippingZoneDetailsPage/ShippingZoneDetailsPage.tsx b/src/shipping/components/ShippingZoneDetailsPage/ShippingZoneDetailsPage.tsx index 6c3cd0252..957cb5732 100644 --- a/src/shipping/components/ShippingZoneDetailsPage/ShippingZoneDetailsPage.tsx +++ b/src/shipping/components/ShippingZoneDetailsPage/ShippingZoneDetailsPage.tsx @@ -1,3 +1,4 @@ +import { Channels_channels } from "@saleor/channels/types/Channels"; import AppHeader from "@saleor/components/AppHeader"; import CardSpacer from "@saleor/components/CardSpacer"; import { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton"; @@ -6,20 +7,17 @@ import CountryList from "@saleor/components/CountryList"; import Form from "@saleor/components/Form"; import Grid from "@saleor/components/Grid"; import Metadata from "@saleor/components/Metadata/Metadata"; -import { MetadataFormData } from "@saleor/components/Metadata/types"; import { MultiAutocompleteChoiceType } from "@saleor/components/MultiAutocompleteSelectField"; import PageHeader from "@saleor/components/PageHeader"; import SaveButtonBar from "@saleor/components/SaveButtonBar"; import { SingleAutocompleteChoiceType } from "@saleor/components/SingleAutocompleteSelectField"; import { ShippingErrorFragment } from "@saleor/fragments/types/ShippingErrorFragment"; -import { - ShippingZoneDetailsFragment, - ShippingZoneDetailsFragment_warehouses -} from "@saleor/fragments/types/ShippingZoneDetailsFragment"; +import { ShippingZoneDetailsFragment_warehouses } from "@saleor/fragments/types/ShippingZoneDetailsFragment"; import { SubmitPromise } from "@saleor/hooks/useForm"; import useStateFromProps from "@saleor/hooks/useStateFromProps"; +import { ShippingZone_shippingZone } from "@saleor/shipping/types/ShippingZone"; import createMultiAutocompleteSelectHandler from "@saleor/utils/handlers/multiAutocompleteSelectChangeHandler"; -import { mapMetadataItemToInput } from "@saleor/utils/maps"; +import { mapNodeToChoice } from "@saleor/utils/maps"; import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTrigger"; import React from "react"; import { defineMessages, FormattedMessage, useIntl } from "react-intl"; @@ -27,15 +25,11 @@ import { defineMessages, FormattedMessage, useIntl } from "react-intl"; import { getStringOrPlaceholder } from "../../../misc"; import { ChannelProps, FetchMoreProps, SearchProps } from "../../../types"; import { ShippingMethodTypeEnum } from "../../../types/globalTypes"; +import { FormData } from "../../components/ShippingZoneDetailsPage/types"; import ShippingZoneInfo from "../ShippingZoneInfo"; import ShippingZoneRates from "../ShippingZoneRates"; -import ShippingZoneWarehouses from "../ShippingZoneWarehouses"; - -export interface FormData extends MetadataFormData { - name: string; - description: string; - warehouses: string[]; -} +import ShippingZoneSettingsCard from "../ShippingZoneSettingsCard"; +import { getInitialFormData } from "./utils"; const messages = defineMessages({ countries: { @@ -63,7 +57,7 @@ export interface ShippingZoneDetailsPageProps disabled: boolean; errors: ShippingErrorFragment[]; saveButtonBarState: ConfirmButtonTransitionState; - shippingZone: ShippingZoneDetailsFragment; + shippingZone: ShippingZone_shippingZone; warehouses: ShippingZoneDetailsFragment_warehouses[]; onBack: () => void; onCountryAdd: () => void; @@ -76,6 +70,7 @@ export interface ShippingZoneDetailsPageProps onWarehouseAdd: () => void; onWeightRateAdd: () => void; onWeightRateEdit: (id: string) => void; + allChannels?: Channels_channels[]; } function warehouseToChoice( @@ -108,28 +103,25 @@ const ShippingZoneDetailsPage: React.FC = ({ saveButtonBarState, selectedChannelId, shippingZone, - warehouses + warehouses, + allChannels }) => { const intl = useIntl(); - const initialForm: FormData = { - description: shippingZone?.description || "", - metadata: shippingZone?.metadata.map(mapMetadataItemToInput), - name: shippingZone?.name || "", - privateMetadata: shippingZone?.privateMetadata.map(mapMetadataItemToInput), - warehouses: shippingZone?.warehouses?.map(warehouse => warehouse.id) || [] - }; + const initialForm = getInitialFormData(shippingZone); + const [warehouseDisplayValues, setWarehouseDisplayValues] = useStateFromProps< MultiAutocompleteChoiceType[] - >( - shippingZone?.warehouses?.map(warehouse => ({ - label: warehouse.name, - value: warehouse.id - })) || [] - ); + >(mapNodeToChoice(shippingZone?.warehouses)); const warehouseChoices = warehouses.map(warehouseToChoice); + const channelChoices = mapNodeToChoice(allChannels); + + const [channelsDisplayValues, setChannelDisplayValues] = useStateFromProps< + MultiAutocompleteChoiceType[] + >(mapNodeToChoice(shippingZone?.channels)); + const { makeChangeHandler: makeMetadataChangeHandler } = useMetadataChangeTrigger(); @@ -144,6 +136,13 @@ const ShippingZoneDetailsPage: React.FC = ({ warehouseChoices ); + const handleChannelChange = createMultiAutocompleteSelectHandler( + toggleValue, + setChannelDisplayValues, + channelsDisplayValues, + channelChoices + ); + const changeMetadata = makeMetadataChangeHandler(change); return ( @@ -205,16 +204,19 @@ const ShippingZoneDetailsPage: React.FC = ({
-
diff --git a/src/shipping/components/ShippingZoneDetailsPage/types.ts b/src/shipping/components/ShippingZoneDetailsPage/types.ts new file mode 100644 index 000000000..0b0e50515 --- /dev/null +++ b/src/shipping/components/ShippingZoneDetailsPage/types.ts @@ -0,0 +1,8 @@ +import { MetadataFormData } from "@saleor/components/Metadata"; + +export interface FormData extends MetadataFormData { + name: string; + description: string; + warehouses: string[]; + channels: string[]; +} diff --git a/src/shipping/components/ShippingZoneDetailsPage/utils.ts b/src/shipping/components/ShippingZoneDetailsPage/utils.ts new file mode 100644 index 000000000..320a3a362 --- /dev/null +++ b/src/shipping/components/ShippingZoneDetailsPage/utils.ts @@ -0,0 +1,15 @@ +import { ShippingZone_shippingZone } from "@saleor/shipping/types/ShippingZone"; +import { mapMetadataItemToInput } from "@saleor/utils/maps"; + +import { FormData } from "./types"; + +export const getInitialFormData = ( + shippingZone?: ShippingZone_shippingZone +): FormData => ({ + description: shippingZone?.description || "", + metadata: shippingZone?.metadata.map(mapMetadataItemToInput), + name: shippingZone?.name || "", + privateMetadata: shippingZone?.privateMetadata.map(mapMetadataItemToInput), + warehouses: shippingZone?.warehouses?.map(warehouse => warehouse.id) || [], + channels: shippingZone?.channels.map(({ id }) => id) || [] +}); diff --git a/src/shipping/components/ShippingZoneRatesCreatePage/ShippingZoneRatesCreatePage.tsx b/src/shipping/components/ShippingZoneRatesCreatePage/ShippingZoneRatesCreatePage.tsx index 23ffab72b..428b57064 100644 --- a/src/shipping/components/ShippingZoneRatesCreatePage/ShippingZoneRatesCreatePage.tsx +++ b/src/shipping/components/ShippingZoneRatesCreatePage/ShippingZoneRatesCreatePage.tsx @@ -1,7 +1,7 @@ import { ChannelShippingData } from "@saleor/channels/utils"; import AppHeader from "@saleor/components/AppHeader"; import CardSpacer from "@saleor/components/CardSpacer"; -import ChannelsAvailability from "@saleor/components/ChannelsAvailability"; +import ChannelsAvailabilityCard from "@saleor/components/ChannelsAvailabilityCard"; import { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton"; import Container from "@saleor/components/Container"; import Form from "@saleor/components/Form"; @@ -166,13 +166,10 @@ export const ShippingZoneRatesCreatePage: React.FC
- ({ - id: channel.id, - name: channel.name - }))} + channelsList={data.channelListings} openModal={openChannelsModal} />
diff --git a/src/shipping/components/ShippingZoneRatesPage/ShippingZoneRatesPage.tsx b/src/shipping/components/ShippingZoneRatesPage/ShippingZoneRatesPage.tsx index 91993979a..09e4eb775 100644 --- a/src/shipping/components/ShippingZoneRatesPage/ShippingZoneRatesPage.tsx +++ b/src/shipping/components/ShippingZoneRatesPage/ShippingZoneRatesPage.tsx @@ -1,7 +1,7 @@ import { ChannelShippingData } from "@saleor/channels/utils"; import AppHeader from "@saleor/components/AppHeader"; import CardSpacer from "@saleor/components/CardSpacer"; -import ChannelsAvailability from "@saleor/components/ChannelsAvailability"; +import ChannelsAvailabilityCard from "@saleor/components/ChannelsAvailabilityCard"; import { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton"; import Container from "@saleor/components/Container"; import Form from "@saleor/components/Form"; @@ -196,7 +196,7 @@ export const ShippingZoneRatesPage: React.FC = ({
- ({ diff --git a/src/shipping/components/ShippingZoneSettingsCard/ChannelsSection.tsx b/src/shipping/components/ShippingZoneSettingsCard/ChannelsSection.tsx new file mode 100644 index 000000000..4263af986 --- /dev/null +++ b/src/shipping/components/ShippingZoneSettingsCard/ChannelsSection.tsx @@ -0,0 +1,67 @@ +import { useChannelsSearch } from "@saleor/channels/components/ChannelsAvailabilityDialog/utils"; +import { Channels_channels } from "@saleor/channels/types/Channels"; +import CardSpacer from "@saleor/components/CardSpacer"; +import MultiAutocompleteSelectField, { + MultiAutocompleteChoiceType +} from "@saleor/components/MultiAutocompleteSelectField"; +import { FormChange } from "@saleor/hooks/useForm"; +import { mapNodeToChoice } from "@saleor/utils/maps"; +import React from "react"; +import { useIntl } from "react-intl"; +import { defineMessages, FormattedMessage } from "react-intl"; + +const messages = defineMessages({ + subtitle: { + defaultMessage: + "Assign channels to this shipping zone so we know which orders will be supported", + description: "ChannelsSection subtitle" + }, + selectFieldLabel: { + defaultMessage: "Channel", + description: "ChannelsSection select field label", + id: "shippingZoneChannels.autocomplete.label" + }, + selectFieldPlaceholder: { + defaultMessage: "Add Channel", + description: "ChannelsSection select field placeholder" + } +}); + +interface ChannelsSectionProps { + onChange: FormChange; + selectedChannels: string[]; + allChannels?: Channels_channels[]; + channelsDisplayValues: MultiAutocompleteChoiceType[]; +} + +const ChannelsSection: React.FC = ({ + onChange, + allChannels = [], + selectedChannels, + channelsDisplayValues +}) => { + const { onQueryChange, filteredChannels } = useChannelsSearch(allChannels); + + const intl = useIntl(); + + return ( + <> + + + + + ); +}; + +export default ChannelsSection; diff --git a/src/shipping/components/ShippingZoneSettingsCard/ShippingZoneSettingsCard.stories.tsx b/src/shipping/components/ShippingZoneSettingsCard/ShippingZoneSettingsCard.stories.tsx new file mode 100644 index 000000000..b65516d0b --- /dev/null +++ b/src/shipping/components/ShippingZoneSettingsCard/ShippingZoneSettingsCard.stories.tsx @@ -0,0 +1,61 @@ +import { Channels_channels } from "@saleor/channels/types/Channels"; +import CentralPlacementDecorator from "@saleor/storybook/CentralPlacementDecorator"; +import CommonDecorator from "@saleor/storybook/Decorator"; +import { storiesOf } from "@storybook/react"; +import React from "react"; + +import { FormData } from "../../components/ShippingZoneDetailsPage/types"; +import ShippingZoneSettingsCard, { + ShippingZoneSettingsCardProps +} from "./ShippingZoneSettingsCard"; + +const props: ShippingZoneSettingsCardProps = { + formData: { + channels: ["channel1"], + warehouses: ["warehouse1", "warehouse2"] + } as FormData, + warehousesDisplayValues: [ + { + value: "warehouse1", + label: "Asia Warehouse" + }, + { + value: "warehouse2", + label: "Europe Warehouse" + } + ], + hasMoreWarehouses: false, + loading: false, + onWarehouseChange: () => undefined, + onFetchMoreWarehouses: () => undefined, + onWarehousesSearchChange: () => undefined, + onWarehouseAdd: () => undefined, + warehousesChoices: [ + { + value: "warehouse1", + label: "C our wares" + }, + { + value: "warehouse2", + label: "Be stocked" + } + ], + allChannels: [ + { __typename: "Channel", id: "channel1", name: "GBP" }, + + { __typename: "Channel", id: "channel2", name: "PLN" }, + { __typename: "Channel", id: "channel3", name: "USD" } + ] as Channels_channels[], + channelsDisplayValues: [ + { + value: "channel1", + label: "GBP" + } + ], + onChannelChange: () => undefined +}; + +storiesOf("Shipping zones details / Settings Card", module) + .addDecorator(CommonDecorator) + .addDecorator(CentralPlacementDecorator) + .add("default", () => ); diff --git a/src/shipping/components/ShippingZoneSettingsCard/ShippingZoneSettingsCard.tsx b/src/shipping/components/ShippingZoneSettingsCard/ShippingZoneSettingsCard.tsx new file mode 100644 index 000000000..7fe76d417 --- /dev/null +++ b/src/shipping/components/ShippingZoneSettingsCard/ShippingZoneSettingsCard.tsx @@ -0,0 +1,93 @@ +import { Divider } from "@material-ui/core"; +import Card from "@material-ui/core/Card"; +import CardContent from "@material-ui/core/CardContent"; +import { Channels_channels } from "@saleor/channels/types/Channels"; +import CardTitle from "@saleor/components/CardTitle"; +import { MultiAutocompleteChoiceType } from "@saleor/components/MultiAutocompleteSelectField"; +import Skeleton from "@saleor/components/Skeleton"; +import { FormChange } from "@saleor/hooks/useForm"; +import React from "react"; +import { defineMessages, useIntl } from "react-intl"; + +import { FormData } from "../../components/ShippingZoneDetailsPage/types"; +import ChannelsSection from "./ChannelsSection"; +import WarehousesSection from "./WarehousesSection"; + +const messages = defineMessages({ + title: { + defaultMessage: "Settings", + description: "ShippingZoneSettingsCard title" + } +}); + +export interface ShippingZoneSettingsCardProps { + formData: FormData; + warehousesDisplayValues: MultiAutocompleteChoiceType[]; + warehousesChoices: MultiAutocompleteChoiceType[]; + onWarehouseAdd: () => void; + onWarehouseChange: FormChange; + hasMoreWarehouses: boolean; + onFetchMoreWarehouses: () => void; + onWarehousesSearchChange: (query: string) => void; + channelsDisplayValues: MultiAutocompleteChoiceType[]; + onChannelChange: FormChange; + allChannels?: Channels_channels[]; + loading: boolean; +} + +export const ShippingZoneSettingsCard: React.FC = ({ + formData, + warehousesDisplayValues, + hasMoreWarehouses, + loading, + warehousesChoices, + onFetchMoreWarehouses, + onWarehousesSearchChange, + onWarehouseAdd, + onWarehouseChange, + allChannels, + onChannelChange, + channelsDisplayValues +}) => { + const intl = useIntl(); + + return ( + + + {loading && ( + + + + )} + + {!loading && ( + <> + + + + + + + + + )} + + ); +}; + +export default ShippingZoneSettingsCard; diff --git a/src/shipping/components/ShippingZoneSettingsCard/WarehousesSection.tsx b/src/shipping/components/ShippingZoneSettingsCard/WarehousesSection.tsx new file mode 100644 index 000000000..b3cb4d60e --- /dev/null +++ b/src/shipping/components/ShippingZoneSettingsCard/WarehousesSection.tsx @@ -0,0 +1,78 @@ +import CardSpacer from "@saleor/components/CardSpacer"; +import MultiAutocompleteSelectField, { + MultiAutocompleteChoiceType +} from "@saleor/components/MultiAutocompleteSelectField"; +import { FormChange } from "@saleor/hooks/useForm"; +import { FetchMoreProps, SearchProps } from "@saleor/types"; +import React from "react"; +import { useIntl } from "react-intl"; +import { defineMessages, FormattedMessage } from "react-intl"; + +const messages = defineMessages({ + subtitle: { + defaultMessage: + "Select warehouse from which you will ship products for this shipping zone. This warehouse address will also be used to calculate taxes.", + description: "WarehousesSection subtitle" + }, + selectFieldAddText: { + defaultMessage: "Add New Warehouse", + description: "WarehousesSection select field add text" + }, + selectFieldLabel: { + defaultMessage: "Warehouse", + description: "WarehousesSection select field label", + id: "shippingZoneWarehouses.autocomplete.label" + }, + selectFieldPlaceholder: { + defaultMessage: "Select Warehouse", + description: "WarehousesSection select field placeholder" + } +}); + +interface WarehousesSectionProps extends FetchMoreProps, SearchProps { + displayValues: MultiAutocompleteChoiceType[]; + choices: MultiAutocompleteChoiceType[]; + onChange: FormChange; + onAdd: () => void; + selectedWarehouses: string[]; +} + +const WarehousesSection: React.FC = ({ + onAdd, + onSearchChange, + onChange, + onFetchMore, + displayValues, + choices, + selectedWarehouses, + hasMore, + loading +}) => { + const intl = useIntl(); + + return ( + <> + + + + + ); +}; + +export default WarehousesSection; diff --git a/src/shipping/components/ShippingZoneSettingsCard/index.ts b/src/shipping/components/ShippingZoneSettingsCard/index.ts new file mode 100644 index 000000000..61a4f0d9e --- /dev/null +++ b/src/shipping/components/ShippingZoneSettingsCard/index.ts @@ -0,0 +1,2 @@ +export * from "./ShippingZoneSettingsCard"; +export { default } from "./ShippingZoneSettingsCard"; diff --git a/src/shipping/components/ShippingZoneWarehouses/ShippingZoneWarehouses.tsx b/src/shipping/components/ShippingZoneWarehouses/ShippingZoneWarehouses.tsx deleted file mode 100644 index 2db7806bb..000000000 --- a/src/shipping/components/ShippingZoneWarehouses/ShippingZoneWarehouses.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import Card from "@material-ui/core/Card"; -import CardContent from "@material-ui/core/CardContent"; -import CardTitle from "@saleor/components/CardTitle"; -import MultiAutocompleteSelectField, { - MultiAutocompleteChoiceType -} from "@saleor/components/MultiAutocompleteSelectField"; -import { FormChange } from "@saleor/hooks/useForm"; -import { FetchMoreProps, SearchProps } from "@saleor/types"; -import React from "react"; -import { useIntl } from "react-intl"; - -interface ShippingZoneWarehousesFormData { - warehouses: string[]; -} -interface ShippingZonewWarehousesProps extends FetchMoreProps, SearchProps { - data: ShippingZoneWarehousesFormData; - displayValues: MultiAutocompleteChoiceType[]; - warehouses: MultiAutocompleteChoiceType[]; - onChange: FormChange; - onWarehouseAdd: () => void; -} - -export const ShippingZoneWarehouses: React.FC = props => { - const { - data, - displayValues, - hasMore, - loading, - warehouses, - onChange, - onFetchMore, - onSearchChange, - onWarehouseAdd - } = props; - const intl = useIntl(); - - return ( - - - - - - - ); -}; -ShippingZoneWarehouses.displayName = "ShippingZoneWarehouses"; -export default ShippingZoneWarehouses; diff --git a/src/shipping/components/ShippingZoneWarehouses/index.ts b/src/shipping/components/ShippingZoneWarehouses/index.ts deleted file mode 100644 index 9719aa5e7..000000000 --- a/src/shipping/components/ShippingZoneWarehouses/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from "./ShippingZoneWarehouses"; -export * from "./ShippingZoneWarehouses"; diff --git a/src/shipping/fixtures.ts b/src/shipping/fixtures.ts index e213c1c00..ddda3b108 100644 --- a/src/shipping/fixtures.ts +++ b/src/shipping/fixtures.ts @@ -1304,6 +1304,11 @@ export const shippingZones: ShippingZoneFragment[] = [ export const shippingZone: ShippingZone_shippingZone = { __typename: "ShippingZone", + channels: [ + { __typename: "Channel", id: "channel1", name: "GBP", currencyCode: "GBP" }, + + { __typename: "Channel", id: "channel2", name: "PLN", currencyCode: "PLN" } + ], countries: [ { __typename: "CountryDisplay", diff --git a/src/shipping/queries.ts b/src/shipping/queries.ts index 5d288d90c..aa11c5b06 100644 --- a/src/shipping/queries.ts +++ b/src/shipping/queries.ts @@ -7,6 +7,10 @@ import makeQuery from "@saleor/hooks/makeQuery"; import gql from "graphql-tag"; import { ShippingZone, ShippingZoneVariables } from "./types/ShippingZone"; +import { + ShippingZoneChannels, + ShippingZoneChannelsVariables +} from "./types/ShippingZoneChannels"; import { ShippingZones, ShippingZonesVariables } from "./types/ShippingZones"; const shippingZones = gql` @@ -51,6 +55,11 @@ const shippingZone = gql` shippingMethods { ...ShippingMethodWithExcludedProductsFragment } + channels { + id + name + currencyCode + } warehouses { id name @@ -61,3 +70,21 @@ const shippingZone = gql` export const useShippingZone = makeQuery( shippingZone ); + +const shippingZoneChannels = gql` + query ShippingZoneChannels($id: ID!) { + shippingZone(id: $id) { + id + channels { + id + name + currencyCode + } + } + } +`; + +export const useShippingZoneChannels = makeQuery< + ShippingZoneChannels, + ShippingZoneChannelsVariables +>(shippingZoneChannels); diff --git a/src/shipping/types/CreateShippingRate.ts b/src/shipping/types/CreateShippingRate.ts index b6b5ac8b6..d30d5d860 100644 --- a/src/shipping/types/CreateShippingRate.ts +++ b/src/shipping/types/CreateShippingRate.ts @@ -130,7 +130,7 @@ export interface CreateShippingRate_shippingPriceCreate_shippingZone { description: string | null; default: boolean; shippingMethods: (CreateShippingRate_shippingPriceCreate_shippingZone_shippingMethods | null)[] | null; - warehouses: (CreateShippingRate_shippingPriceCreate_shippingZone_warehouses | null)[] | null; + warehouses: CreateShippingRate_shippingPriceCreate_shippingZone_warehouses[]; } export interface CreateShippingRate_shippingPriceCreate_shippingMethod_postalCodeRules { diff --git a/src/shipping/types/DeleteShippingRate.ts b/src/shipping/types/DeleteShippingRate.ts index ebbfeed7f..19810e3e5 100644 --- a/src/shipping/types/DeleteShippingRate.ts +++ b/src/shipping/types/DeleteShippingRate.ts @@ -130,7 +130,7 @@ export interface DeleteShippingRate_shippingPriceDelete_shippingZone { description: string | null; default: boolean; shippingMethods: (DeleteShippingRate_shippingPriceDelete_shippingZone_shippingMethods | null)[] | null; - warehouses: (DeleteShippingRate_shippingPriceDelete_shippingZone_warehouses | null)[] | null; + warehouses: DeleteShippingRate_shippingPriceDelete_shippingZone_warehouses[]; } export interface DeleteShippingRate_shippingPriceDelete { diff --git a/src/shipping/types/ShippingZone.ts b/src/shipping/types/ShippingZone.ts index 635715557..f11346d50 100644 --- a/src/shipping/types/ShippingZone.ts +++ b/src/shipping/types/ShippingZone.ts @@ -140,6 +140,13 @@ export interface ShippingZone_shippingZone_shippingMethods { excludedProducts: ShippingZone_shippingZone_shippingMethods_excludedProducts | null; } +export interface ShippingZone_shippingZone_channels { + __typename: "Channel"; + id: string; + name: string; + currencyCode: string; +} + export interface ShippingZone_shippingZone_warehouses { __typename: "Warehouse"; id: string; @@ -156,7 +163,8 @@ export interface ShippingZone_shippingZone { description: string | null; default: boolean; shippingMethods: (ShippingZone_shippingZone_shippingMethods | null)[] | null; - warehouses: (ShippingZone_shippingZone_warehouses | null)[] | null; + channels: ShippingZone_shippingZone_channels[]; + warehouses: ShippingZone_shippingZone_warehouses[]; } export interface ShippingZone { diff --git a/src/shipping/types/ShippingZoneChannels.ts b/src/shipping/types/ShippingZoneChannels.ts new file mode 100644 index 000000000..5a5dbed33 --- /dev/null +++ b/src/shipping/types/ShippingZoneChannels.ts @@ -0,0 +1,29 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL query operation: ShippingZoneChannels +// ==================================================== + +export interface ShippingZoneChannels_shippingZone_channels { + __typename: "Channel"; + id: string; + name: string; + currencyCode: string; +} + +export interface ShippingZoneChannels_shippingZone { + __typename: "ShippingZone"; + id: string; + channels: ShippingZoneChannels_shippingZone_channels[]; +} + +export interface ShippingZoneChannels { + shippingZone: ShippingZoneChannels_shippingZone | null; +} + +export interface ShippingZoneChannelsVariables { + id: string; +} diff --git a/src/shipping/types/ShippingZoneDetails.ts b/src/shipping/types/ShippingZoneDetails.ts new file mode 100644 index 000000000..0fd16d10a --- /dev/null +++ b/src/shipping/types/ShippingZoneDetails.ts @@ -0,0 +1,176 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +import { PostalCodeRuleInclusionTypeEnum, WeightUnitsEnum, ShippingMethodTypeEnum } from "./../../types/globalTypes"; + +// ==================================================== +// GraphQL query operation: ShippingZoneDetails +// ==================================================== + +export interface ShippingZoneDetails_shippingZone_metadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + +export interface ShippingZoneDetails_shippingZone_privateMetadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + +export interface ShippingZoneDetails_shippingZone_countries { + __typename: "CountryDisplay"; + code: string; + country: string; +} + +export interface ShippingZoneDetails_shippingZone_shippingMethods_postalCodeRules { + __typename: "ShippingMethodPostalCodeRule"; + id: string; + inclusionType: PostalCodeRuleInclusionTypeEnum | null; + start: string | null; + end: string | null; +} + +export interface ShippingZoneDetails_shippingZone_shippingMethods_metadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + +export interface ShippingZoneDetails_shippingZone_shippingMethods_privateMetadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + +export interface ShippingZoneDetails_shippingZone_shippingMethods_minimumOrderWeight { + __typename: "Weight"; + unit: WeightUnitsEnum; + value: number; +} + +export interface ShippingZoneDetails_shippingZone_shippingMethods_maximumOrderWeight { + __typename: "Weight"; + unit: WeightUnitsEnum; + value: number; +} + +export interface ShippingZoneDetails_shippingZone_shippingMethods_channelListings_channel { + __typename: "Channel"; + id: string; + name: string; + currencyCode: string; +} + +export interface ShippingZoneDetails_shippingZone_shippingMethods_channelListings_price { + __typename: "Money"; + amount: number; + currency: string; +} + +export interface ShippingZoneDetails_shippingZone_shippingMethods_channelListings_minimumOrderPrice { + __typename: "Money"; + amount: number; + currency: string; +} + +export interface ShippingZoneDetails_shippingZone_shippingMethods_channelListings_maximumOrderPrice { + __typename: "Money"; + amount: number; + currency: string; +} + +export interface ShippingZoneDetails_shippingZone_shippingMethods_channelListings { + __typename: "ShippingMethodChannelListing"; + id: string; + channel: ShippingZoneDetails_shippingZone_shippingMethods_channelListings_channel; + price: ShippingZoneDetails_shippingZone_shippingMethods_channelListings_price | null; + minimumOrderPrice: ShippingZoneDetails_shippingZone_shippingMethods_channelListings_minimumOrderPrice | null; + maximumOrderPrice: ShippingZoneDetails_shippingZone_shippingMethods_channelListings_maximumOrderPrice | null; +} + +export interface ShippingZoneDetails_shippingZone_shippingMethods_excludedProducts_pageInfo { + __typename: "PageInfo"; + hasNextPage: boolean; + hasPreviousPage: boolean; + endCursor: string | null; + startCursor: string | null; +} + +export interface ShippingZoneDetails_shippingZone_shippingMethods_excludedProducts_edges_node_thumbnail { + __typename: "Image"; + url: string; +} + +export interface ShippingZoneDetails_shippingZone_shippingMethods_excludedProducts_edges_node { + __typename: "Product"; + id: string; + name: string; + thumbnail: ShippingZoneDetails_shippingZone_shippingMethods_excludedProducts_edges_node_thumbnail | null; +} + +export interface ShippingZoneDetails_shippingZone_shippingMethods_excludedProducts_edges { + __typename: "ProductCountableEdge"; + node: ShippingZoneDetails_shippingZone_shippingMethods_excludedProducts_edges_node; +} + +export interface ShippingZoneDetails_shippingZone_shippingMethods_excludedProducts { + __typename: "ProductCountableConnection"; + pageInfo: ShippingZoneDetails_shippingZone_shippingMethods_excludedProducts_pageInfo; + edges: ShippingZoneDetails_shippingZone_shippingMethods_excludedProducts_edges[]; +} + +export interface ShippingZoneDetails_shippingZone_shippingMethods { + __typename: "ShippingMethod"; + id: string; + postalCodeRules: (ShippingZoneDetails_shippingZone_shippingMethods_postalCodeRules | null)[] | null; + metadata: (ShippingZoneDetails_shippingZone_shippingMethods_metadata | null)[]; + privateMetadata: (ShippingZoneDetails_shippingZone_shippingMethods_privateMetadata | null)[]; + minimumOrderWeight: ShippingZoneDetails_shippingZone_shippingMethods_minimumOrderWeight | null; + maximumOrderWeight: ShippingZoneDetails_shippingZone_shippingMethods_maximumOrderWeight | null; + minimumDeliveryDays: number | null; + maximumDeliveryDays: number | null; + name: string; + type: ShippingMethodTypeEnum | null; + channelListings: ShippingZoneDetails_shippingZone_shippingMethods_channelListings[] | null; + excludedProducts: ShippingZoneDetails_shippingZone_shippingMethods_excludedProducts | null; +} + +export interface ShippingZoneDetails_shippingZone_channels { + __typename: "Channel"; + id: string; + name: string; + currencyCode: string; +} + +export interface ShippingZoneDetails_shippingZone_warehouses { + __typename: "Warehouse"; + id: string; + name: string; +} + +export interface ShippingZoneDetails_shippingZone { + __typename: "ShippingZone"; + metadata: (ShippingZoneDetails_shippingZone_metadata | null)[]; + privateMetadata: (ShippingZoneDetails_shippingZone_privateMetadata | null)[]; + id: string; + countries: (ShippingZoneDetails_shippingZone_countries | null)[] | null; + name: string; + description: string | null; + default: boolean; + shippingMethods: (ShippingZoneDetails_shippingZone_shippingMethods | null)[] | null; + channels: ShippingZoneDetails_shippingZone_channels[]; + warehouses: ShippingZoneDetails_shippingZone_warehouses[]; +} + +export interface ShippingZoneDetails { + shippingZone: ShippingZoneDetails_shippingZone | null; +} + +export interface ShippingZoneDetailsVariables { + id: string; +} diff --git a/src/shipping/views/PriceRatesCreate/PriceRatesCreate.tsx b/src/shipping/views/PriceRatesCreate/PriceRatesCreate.tsx index 7410a393c..4f84fd0fd 100644 --- a/src/shipping/views/PriceRatesCreate/PriceRatesCreate.tsx +++ b/src/shipping/views/PriceRatesCreate/PriceRatesCreate.tsx @@ -1,4 +1,3 @@ -import { useChannelsList } from "@saleor/channels/queries"; import { createSortedShippingChannels } from "@saleor/channels/utils"; import ChannelsAvailabilityDialog from "@saleor/components/ChannelsAvailabilityDialog"; import { WindowTitle } from "@saleor/components/WindowTitle"; @@ -8,6 +7,7 @@ import { sectionNames } from "@saleor/intl"; import ShippingZonePostalCodeRangeDialog from "@saleor/shipping/components/ShippingZonePostalCodeRangeDialog"; import ShippingZoneRatesCreatePage from "@saleor/shipping/components/ShippingZoneRatesCreatePage"; import { useShippingRateCreator } from "@saleor/shipping/handlers"; +import { useShippingZoneChannels } from "@saleor/shipping/queries"; import { shippingPriceRatesUrl, ShippingRateCreateUrlDialog, @@ -41,14 +41,22 @@ export const PriceRatesCreate: React.FC = ({ const navigate = useNavigator(); const intl = useIntl(); - const { data: channelsData, loading: channelsLoading } = useChannelsList({}); - const [openModal, closeModal] = createDialogActionHandlers< ShippingRateCreateUrlDialog, ShippingRateCreateUrlQueryParams >(navigate, params => shippingPriceRatesUrl(id, params), params); - const allChannels = createSortedShippingChannels(channelsData?.channels); + const { + data: shippingZoneData, + loading: channelsLoading + } = useShippingZoneChannels({ + displayLoader: true, + variables: { id } + }); + + const allChannels = createSortedShippingChannels( + shippingZoneData?.shippingZone?.channels + ); const { channelListElements, diff --git a/src/shipping/views/PriceRatesUpdate/PriceRatesUpdate.tsx b/src/shipping/views/PriceRatesUpdate/PriceRatesUpdate.tsx index f6be75183..ec2113424 100644 --- a/src/shipping/views/PriceRatesUpdate/PriceRatesUpdate.tsx +++ b/src/shipping/views/PriceRatesUpdate/PriceRatesUpdate.tsx @@ -1,5 +1,4 @@ import Button from "@material-ui/core/Button"; -import { useChannelsList } from "@saleor/channels/queries"; import { createShippingChannelsFromRate, createSortedShippingChannels @@ -90,6 +89,8 @@ export const PriceRatesUpdate: React.FC = ({ variables: { id, ...paginationState } }); + const channelsData = data?.shippingZone?.channels; + const rate = data?.shippingZone?.shippingMethods?.find(getById(rateId)); const { @@ -113,8 +114,6 @@ export const PriceRatesUpdate: React.FC = ({ params ); - const { data: channelsData } = useChannelsList({}); - const [ updateShippingMethodChannelListing, updateShippingMethodChannelListingOpts @@ -145,7 +144,7 @@ export const PriceRatesUpdate: React.FC = ({ const shippingChannels = createShippingChannelsFromRate( rate?.channelListings ); - const allChannels = createSortedShippingChannels(channelsData?.channels); + const allChannels = createSortedShippingChannels(channelsData); const { channelListElements, diff --git a/src/shipping/views/ShippingZoneDetails/index.tsx b/src/shipping/views/ShippingZoneDetails/index.tsx index da13434f6..0fe06ce78 100644 --- a/src/shipping/views/ShippingZoneDetails/index.tsx +++ b/src/shipping/views/ShippingZoneDetails/index.tsx @@ -1,4 +1,5 @@ import DialogContentText from "@material-ui/core/DialogContentText"; +import { useChannelsList } from "@saleor/channels/queries"; import ActionDialog from "@saleor/components/ActionDialog"; import useAppChannel from "@saleor/components/AppLayout/AppChannelContext"; import NotFoundPage from "@saleor/components/NotFoundPage"; @@ -32,9 +33,8 @@ import { FormattedMessage, useIntl } from "react-intl"; import { findValueInEnum, getStringOrPlaceholder } from "../../../misc"; import { CountryCode } from "../../../types/globalTypes"; -import ShippingZoneDetailsPage, { - FormData -} from "../../components/ShippingZoneDetailsPage"; +import ShippingZoneDetailsPage from "../../components/ShippingZoneDetailsPage"; +import { FormData } from "../../components/ShippingZoneDetailsPage/types"; import { useShippingZone } from "../../queries"; import { shippingPriceRatesEditUrl, @@ -69,6 +69,8 @@ const ShippingZoneDetails: React.FC = ({ } ); + const { data: channelsList } = useChannelsList({}); + const { data, loading } = useShippingZone({ displayLoader: true, variables: { id, ...paginationState } @@ -138,11 +140,18 @@ const ShippingZoneDetails: React.FC = ({ submitData.warehouses ); + const channelsDiff = diff( + data.shippingZone.channels.map(channel => channel.id), + submitData.channels + ); + const result = await updateShippingZone({ variables: { id, input: { addWarehouses: warehouseDiff.added, + addChannels: channelsDiff.added, + removeChannels: channelsDiff.removed, description: submitData.description, name: submitData.name, removeWarehouses: warehouseDiff.removed @@ -187,6 +196,7 @@ const ShippingZoneDetails: React.FC = ({ }) } onSubmit={handleSubmit} + allChannels={channelsList?.channels} onWarehouseAdd={() => openModal("add-warehouse")} onWeightRateAdd={() => navigate(shippingWeightRatesUrl(id))} onWeightRateEdit={rateId => diff --git a/src/shipping/views/WeightRatesCreate/WeightRatesCreate.tsx b/src/shipping/views/WeightRatesCreate/WeightRatesCreate.tsx index 93bc558e6..59fb5bd80 100644 --- a/src/shipping/views/WeightRatesCreate/WeightRatesCreate.tsx +++ b/src/shipping/views/WeightRatesCreate/WeightRatesCreate.tsx @@ -1,4 +1,3 @@ -import { useChannelsList } from "@saleor/channels/queries"; import { createShippingChannels, createSortedShippingChannels @@ -11,6 +10,7 @@ import { sectionNames } from "@saleor/intl"; import ShippingZonePostalCodeRangeDialog from "@saleor/shipping/components/ShippingZonePostalCodeRangeDialog"; import ShippingZoneRatesCreatePage from "@saleor/shipping/components/ShippingZoneRatesCreatePage"; import { useShippingRateCreator } from "@saleor/shipping/handlers"; +import { useShippingZoneChannels } from "@saleor/shipping/queries"; import { ShippingRateCreateUrlDialog, ShippingRateCreateUrlQueryParams, @@ -44,15 +44,25 @@ export const WeightRatesCreate: React.FC = ({ const navigate = useNavigator(); const intl = useIntl(); - const { data: channelsData, loading: channelsLoading } = useChannelsList({}); + const { + data: shippingZoneData, + loading: channelsLoading + } = useShippingZoneChannels({ + displayLoader: true, + variables: { id } + }); const [openModal, closeModal] = createDialogActionHandlers< ShippingRateCreateUrlDialog, ShippingRateCreateUrlQueryParams >(navigate, params => shippingWeightRatesUrl(id, params), params); - const shippingChannels = createShippingChannels(channelsData?.channels); - const allChannels = createSortedShippingChannels(channelsData?.channels); + const shippingChannels = createShippingChannels( + shippingZoneData?.shippingZone?.channels + ); + const allChannels = createSortedShippingChannels( + shippingZoneData?.shippingZone?.channels + ); const { channelListElements, diff --git a/src/shipping/views/WeightRatesUpdate/WeightRatesUpdate.tsx b/src/shipping/views/WeightRatesUpdate/WeightRatesUpdate.tsx index 717cd5d0f..0f7cd5ab8 100644 --- a/src/shipping/views/WeightRatesUpdate/WeightRatesUpdate.tsx +++ b/src/shipping/views/WeightRatesUpdate/WeightRatesUpdate.tsx @@ -1,5 +1,4 @@ import Button from "@material-ui/core/Button"; -import { useChannelsList } from "@saleor/channels/queries"; import { createShippingChannelsFromRate, createSortedShippingChannels @@ -90,6 +89,8 @@ export const WeightRatesUpdate: React.FC = ({ variables: { id, ...paginationState } }); + const channelsData = data?.shippingZone?.channels; + const rate = data?.shippingZone?.shippingMethods?.find(getById(rateId)); const [openModal, closeModal] = createDialogActionHandlers< @@ -162,7 +163,6 @@ export const WeightRatesUpdate: React.FC = ({ params ); - const { data: channelsData } = useChannelsList({}); const [ updateShippingMethodChannelListing, updateShippingMethodChannelListingOpts @@ -194,7 +194,7 @@ export const WeightRatesUpdate: React.FC = ({ const shippingChannels = createShippingChannelsFromRate( rate?.channelListings ); - const allChannels = createSortedShippingChannels(channelsData?.channels); + const allChannels = createSortedShippingChannels(channelsData); const { channelListElements, diff --git a/src/storybook/CentralPlacementDecorator.tsx b/src/storybook/CentralPlacementDecorator.tsx new file mode 100644 index 000000000..7bbb6a32f --- /dev/null +++ b/src/storybook/CentralPlacementDecorator.tsx @@ -0,0 +1,18 @@ +import React from "react"; + +const CentralPlacementDecorator = storyFn => ( +
+ {storyFn()} +
+); +export default CentralPlacementDecorator; diff --git a/src/storybook/__snapshots__/Stories.test.ts.snap b/src/storybook/__snapshots__/Stories.test.ts.snap index 0e47bbde1..f65be766a 100644 --- a/src/storybook/__snapshots__/Stories.test.ts.snap +++ b/src/storybook/__snapshots__/Stories.test.ts.snap @@ -1339,6 +1339,833 @@ exports[`Storyshots Attributes / Delete multiple attributes default 1`] = ` /> `; +exports[`Storyshots Channels / Cannot define channels availability card default 1`] = ` +
+
+
+
+ + Availability + +
+
+
+
+
+ You will be able to define availability of product after creating variants. +
+
+
+
+`; + +exports[`Storyshots Channels / Channels with Variants Availability Dialog default 1`] = ` +
+`; + +exports[`Storyshots Channels / Channels with variants availability card default 1`] = ` +
+
+
+
+ + Availability + +
+
+
+
+
+
+ Available at 2 out of 3 channels +
+
+
+ +
+
+
+
+
+
+
+ + +
+
+
+
+
+ + +
+
+
+ Set availability date +
+
+ +
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+ + +
+
+
+
+
+ + +
+
+
+ Set availability date +
+
+ +
+
+
+
+
+
+
+
+
+
+`; + exports[`Storyshots Customers / Address editing dialog default 1`] = `
`; -exports[`Storyshots Generics / AvailabilityCard default 1`] = ` -
-
-
- - Availability - -
-
-
-
-
-
- Available at 3 out of 4 channels -
-
-
-
-
-
- Channel1 -
- -
-
- Visible since 07/14/2020 -
-
-
-
-
-
-
-
- Channel2 -
- -
-
- Will become available on 07/30/2020 -
-
-
-
-
-
-
-`; - exports[`Storyshots Generics / Card menu default 1`] = `
`; -exports[`Storyshots Generics / ChannelsAvailability default 1`] = ` +exports[`Storyshots Generics / Channels availability card default 1`] = `
@@ -2902,7 +3606,7 @@ exports[`Storyshots Generics / ChannelsAvailability default 1`] = `
`; -exports[`Storyshots Generics / ChannelsAvailability with onChange 1`] = ` +exports[`Storyshots Generics / Channels availability card with onChange 1`] = `
@@ -2952,83 +3656,737 @@ exports[`Storyshots Generics / ChannelsAvailability with onChange 1`] = ` class="Hr-root-id ChannelsAvailabilityCard-hr-id" />
-
-
+
+
+
+
+
+
+
+
+
+
+ + +
+
+
+ Set availability date +
+
+ +
+
+
+
+
+
+
+ -
@@ -3545,6 +4903,39 @@ exports[`Storyshots Generics / DateTime plain 1`] = `
`; +exports[`Storyshots Generics / Deletable Item default 1`] = ` +
+
+ +
+
+`; + exports[`Storyshots Generics / Delete filter tab default 1`] = `
`; +exports[`Storyshots Generics / Select with autocomplete naked 1`] = ` +
+
+
+ +
+ +
+ +
+
+
+`; + exports[`Storyshots Generics / Select with autocomplete no data 1`] = `
`; +exports[`Storyshots Shipping zones card with no options selected 1`] = ` +
+
+
+ + Shipping Zones + +
+
+
+
+
+
+ Select Shipping Zones that will be supplied via this channel. You can assign Shipping Zones to multiple channels. +
+
+
+
+ +
+
+
+
+
+
+
+
+ + Add Shipping Zones + + +
+
+
+
+
+
+
+
+`; + +exports[`Storyshots Shipping zones card with options selected 1`] = ` +
+
+
+ + Shipping Zones + +
+
+
+
+
+
+ Select Shipping Zones that will be supplied via this channel. You can assign Shipping Zones to multiple channels. +
+
+
+
+ +
+
+
+
+
+
+
+
+
+ Fancy shipping zone +
+
+ +
+
+
+
+
+ Nice shipping zone +
+
+ +
+
+
+
+ + Add Shipping Zones + + +
+
+
+
+
+
+
+
+`; + +exports[`Storyshots Shipping zones details / Settings Card default 1`] = ` +
+
+
+
+ + Settings + +
+
+
+
+
+ Assign channels to this shipping zone so we know which orders will be supported +
+
+
+ + +
+
+
+
+
+
+ GBP +
+ +
+
+
+
+
+
+ Select warehouse from which you will ship products for this shipping zone. This warehouse address will also be used to calculate taxes. +
+
+
+ + +
+
+
+
+
+
+ Asia Warehouse +
+ +
+
+
+
+
+ Europe Warehouse +
+ +
+
+
+
+
+
+
+`; + exports[`Storyshots Views / Apps / Activate app default 1`] = `
- Secrect Key + Secret Key @@ -22769,7 +24849,7 @@ exports[`Storyshots Views / Apps / Webhooks / Create webhook form errors 1`] = ` class="PrivateNotchedOutline-legendLabelled-id" > - Secrect Key + Secret Key @@ -24100,7 +26180,7 @@ exports[`Storyshots Views / Apps / Webhooks / Create webhook loading 1`] = ` class="PrivateNotchedOutline-legendLabelled-id" > - Secrect Key + Secret Key @@ -25492,7 +27572,7 @@ exports[`Storyshots Views / Apps / Webhooks / Webhook details default 1`] = ` class="PrivateNotchedOutline-legendLabelled-id PrivateNotchedOutline-legendNotched-id" > - Secrect Key + Secret Key @@ -26831,7 +28911,7 @@ exports[`Storyshots Views / Apps / Webhooks / Webhook details form errors 1`] = class="PrivateNotchedOutline-legendLabelled-id PrivateNotchedOutline-legendNotched-id" > - Secrect Key + Secret Key @@ -28168,7 +30248,7 @@ exports[`Storyshots Views / Apps / Webhooks / Webhook details loading 1`] = ` class="PrivateNotchedOutline-legendLabelled-id" > - Secrect Key + Secret Key @@ -29548,7 +31628,7 @@ exports[`Storyshots Views / Apps / Webhooks / Webhook details unnamed 1`] = ` class="PrivateNotchedOutline-legendLabelled-id PrivateNotchedOutline-legendNotched-id" > - Secrect Key + Secret Key @@ -47302,12 +49382,13 @@ exports[`Storyshots Views / Categories / Update category products 1`] = `
Gardner-Schultz
@@ -47387,12 +49468,13 @@ exports[`Storyshots Views / Categories / Update category products 1`] = `
James, Martinez and Murray
@@ -47472,12 +49554,13 @@ exports[`Storyshots Views / Categories / Update category products 1`] = `
Curtis, Joyce and Turner
@@ -47557,12 +49640,13 @@ exports[`Storyshots Views / Categories / Update category products 1`] = `
Davis, Brown and Ray
@@ -47642,12 +49726,13 @@ exports[`Storyshots Views / Categories / Update category products 1`] = `
Gallegos Ltd
@@ -47727,12 +49812,13 @@ exports[`Storyshots Views / Categories / Update category products 1`] = `
Franklin Inc
@@ -47812,12 +49898,13 @@ exports[`Storyshots Views / Categories / Update category products 1`] = `
Williams-Taylor
@@ -47897,12 +49984,13 @@ exports[`Storyshots Views / Categories / Update category products 1`] = `
Riddle, Evans and Hicks
@@ -47982,12 +50070,13 @@ exports[`Storyshots Views / Categories / Update category products 1`] = `
Hebert-Sherman
@@ -48067,12 +50156,13 @@ exports[`Storyshots Views / Categories / Update category products 1`] = `
Carter and Sons
@@ -48371,6 +50461,143 @@ exports[`Storyshots Views / Channels / Channel details default 1`] = `
+
+
+
+ + Shipping Zones + +
+
+
+
+
+
+ Select Shipping Zones that will be supplied via this channel. You can assign Shipping Zones to multiple channels. +
+
+
+
+ +
+
+
+
+
+
+
+
+ + Add Shipping Zones + + +
+
+
+
+
+
+
@@ -48627,6 +50854,143 @@ exports[`Storyshots Views / Channels / Channel details disabled 1`] = `
+
+
+
+ + Shipping Zones + +
+
+
+
+
+
+ Select Shipping Zones that will be supplied via this channel. You can assign Shipping Zones to multiple channels. +
+
+
+
+ +
+
+
+
+
+
+
+
+ + Add Shipping Zones + + +
+
+
+
+
+
+
@@ -48880,6 +51244,143 @@ exports[`Storyshots Views / Channels / Channel details loading 1`] = `
+
+
+
+ + Shipping Zones + +
+
+
+
+
+
+ Select Shipping Zones that will be supplied via this channel. You can assign Shipping Zones to multiple channels. +
+
+
+
+ +
+
+
+
+
+
+
+
+ + Add Shipping Zones + + +
+
+
+
+
+
+
@@ -49133,6 +51634,143 @@ exports[`Storyshots Views / Channels / Channel details with data 1`] = `
+
+
+
+ + Shipping Zones + +
+
+
+
+
+
+ Select Shipping Zones that will be supplied via this channel. You can assign Shipping Zones to multiple channels. +
+
+
+
+ +
+
+
+
+
+
+
+
+ + Add Shipping Zones + + +
+
+
+
+
+
+
@@ -49392,6 +52030,143 @@ exports[`Storyshots Views / Channels / Channel details with errors 1`] = `
+
+
+
+ + Shipping Zones + +
+
+
+
+
+
+ Select Shipping Zones that will be supplied via this channel. You can assign Shipping Zones to multiple channels. +
+
+
+
+ +
+
+
+
+
+
+
+
+ + Add Shipping Zones + + +
+
+
+
+
+
+
@@ -49598,6 +52373,143 @@ exports[`Storyshots Views / Channels / Channel details without editable currency
+
+
+
+ + Shipping Zones + +
+
+
+
+
+
+ Select Shipping Zones that will be supplied via this channel. You can assign Shipping Zones to multiple channels. +
+
+
+
+ +
+
+
+
+
+
+
+
+ + Add Shipping Zones + + +
+
+
+
+
+
+
@@ -52117,12 +55029,13 @@ exports[`Storyshots Views / Collections / Collection detailsCollection details d
Murray Inc
@@ -52221,12 +55134,13 @@ exports[`Storyshots Views / Collections / Collection detailsCollection details d
Williams-Taylor
@@ -52325,12 +55239,13 @@ exports[`Storyshots Views / Collections / Collection detailsCollection details d
Hebert-Sherman
@@ -52429,12 +55344,13 @@ exports[`Storyshots Views / Collections / Collection detailsCollection details d
Estes, Johnson and Graham
@@ -52582,46 +55498,228 @@ exports[`Storyshots Views / Collections / Collection detailsCollection details d class="Hr-root-id ChannelsAvailabilityCard-hr-id" />
-
@@ -53383,12 +56481,13 @@ exports[`Storyshots Views / Collections / Collection detailsCollection details f
Murray Inc
@@ -53487,12 +56586,13 @@ exports[`Storyshots Views / Collections / Collection detailsCollection details f
Williams-Taylor
@@ -53591,12 +56691,13 @@ exports[`Storyshots Views / Collections / Collection detailsCollection details f
Hebert-Sherman
@@ -53695,12 +56796,13 @@ exports[`Storyshots Views / Collections / Collection detailsCollection details f
Estes, Johnson and Graham
@@ -53848,46 +56950,228 @@ exports[`Storyshots Views / Collections / Collection detailsCollection details f class="Hr-root-id ChannelsAvailabilityCard-hr-id" />
-
@@ -54375,10 +57659,10 @@ exports[`Storyshots Views / Collections / Collection detailsCollection details l class="MuiTableCell-root-id MuiTableCell-body-id TableCellAvatar-root-id CollectionProducts-colName-id" >
-
@@ -55357,46 +58823,228 @@ exports[`Storyshots Views / Collections / Collection detailsCollection details n class="Hr-root-id ChannelsAvailabilityCard-hr-id" />
-
@@ -57208,292 +60856,1566 @@ exports[`Storyshots Views / Collections / Create collection default 1`] = ` class="Hr-root-id ChannelsAvailabilityCard-hr-id" />
-
-
-
-
-
-
-
@@ -57918,292 +62840,1566 @@ exports[`Storyshots Views / Collections / Create collection form errors 1`] = ` class="Hr-root-id ChannelsAvailabilityCard-hr-id" />
-
-
-
-
-
-
-
@@ -58622,292 +64818,1566 @@ exports[`Storyshots Views / Collections / Create collection loading 1`] = ` class="Hr-root-id ChannelsAvailabilityCard-hr-id" />
-
-
-
-
-
-
-
@@ -79754,12 +87224,13 @@ exports[`Storyshots Views / Discounts / Sale details products 1`] = `
Orange Juice
@@ -79858,12 +87329,13 @@ exports[`Storyshots Views / Discounts / Sale details products 1`] = `
Carrot Juice
@@ -79962,12 +87434,13 @@ exports[`Storyshots Views / Discounts / Sale details products 1`] = `
Bean Juice
@@ -80066,12 +87539,13 @@ exports[`Storyshots Views / Discounts / Sale details products 1`] = `
Black Hoodie
@@ -93939,13 +101413,15 @@ exports[`Storyshots Views / HomePage default 1`] = ` class="MuiTableRow-root-id HomeProductListCard-tableRow-id MuiTableRow-hover-id" >
@@ -101000,12 +108476,13 @@ exports[`Storyshots Views / Orders / Fulfill order default 1`] = ` >
T-Shirt
Lemon Juice
Orange Juice
T-Shirt
Lemon Juice
Orange Juice
T-Shirt
Lemon Juice
Orange Juice
Watkins-Gonzalez (Soft)
@@ -103044,12 +110530,13 @@ exports[`Storyshots Views / Orders / Order details cancelled 1`] = ` >
Williams, Garcia and Walker (XS)
@@ -103173,12 +110660,13 @@ exports[`Storyshots Views / Orders / Order details cancelled 1`] = ` >
Williams, Garcia and Walker (XS)
@@ -104552,12 +112040,13 @@ exports[`Storyshots Views / Orders / Order details default 1`] = ` >
Watkins-Gonzalez (Soft)
@@ -104721,12 +112210,13 @@ exports[`Storyshots Views / Orders / Order details default 1`] = ` >
Williams, Garcia and Walker (XS)
@@ -104890,12 +112380,13 @@ exports[`Storyshots Views / Orders / Order details default 1`] = ` >
Williams, Garcia and Walker (XS)
@@ -106365,12 +113856,13 @@ exports[`Storyshots Views / Orders / Order details fulfilled 1`] = ` >
Watkins-Gonzalez (Soft)
@@ -106534,12 +114026,13 @@ exports[`Storyshots Views / Orders / Order details fulfilled 1`] = ` >
Williams, Garcia and Walker (XS)
@@ -106703,12 +114196,13 @@ exports[`Storyshots Views / Orders / Order details fulfilled 1`] = ` >
Williams, Garcia and Walker (XS)
@@ -108747,12 +116241,13 @@ exports[`Storyshots Views / Orders / Order details no customer note 1`] = ` >
Watkins-Gonzalez (Soft)
@@ -108916,12 +116411,13 @@ exports[`Storyshots Views / Orders / Order details no customer note 1`] = ` >
Williams, Garcia and Walker (XS)
@@ -109085,12 +116581,13 @@ exports[`Storyshots Views / Orders / Order details no customer note 1`] = ` >
Williams, Garcia and Walker (XS)
@@ -110560,12 +118057,13 @@ exports[`Storyshots Views / Orders / Order details no payment 1`] = ` >
Watkins-Gonzalez (Soft)
@@ -110729,12 +118227,13 @@ exports[`Storyshots Views / Orders / Order details no payment 1`] = ` >
Williams, Garcia and Walker (XS)
@@ -110898,12 +118397,13 @@ exports[`Storyshots Views / Orders / Order details no payment 1`] = ` >
Williams, Garcia and Walker (XS)
@@ -112373,12 +119873,13 @@ exports[`Storyshots Views / Orders / Order details no shipping address 1`] = ` >
Watkins-Gonzalez (Soft)
@@ -112542,12 +120043,13 @@ exports[`Storyshots Views / Orders / Order details no shipping address 1`] = ` >
Williams, Garcia and Walker (XS)
@@ -112711,12 +120213,13 @@ exports[`Storyshots Views / Orders / Order details no shipping address 1`] = ` >
Williams, Garcia and Walker (XS)
@@ -114186,12 +121689,13 @@ exports[`Storyshots Views / Orders / Order details partially fulfilled 1`] = ` >
Watkins-Gonzalez (Soft)
@@ -114355,12 +121859,13 @@ exports[`Storyshots Views / Orders / Order details partially fulfilled 1`] = ` >
Williams, Garcia and Walker (XS)
@@ -114524,12 +122029,13 @@ exports[`Storyshots Views / Orders / Order details partially fulfilled 1`] = ` >
Williams, Garcia and Walker (XS)
@@ -115999,12 +123505,13 @@ exports[`Storyshots Views / Orders / Order details payment confirmed 1`] = ` >
Watkins-Gonzalez (Soft)
@@ -116168,12 +123675,13 @@ exports[`Storyshots Views / Orders / Order details payment confirmed 1`] = ` >
Williams, Garcia and Walker (XS)
@@ -116337,12 +123845,13 @@ exports[`Storyshots Views / Orders / Order details payment confirmed 1`] = ` >
Williams, Garcia and Walker (XS)
@@ -117812,12 +125321,13 @@ exports[`Storyshots Views / Orders / Order details payment error 1`] = ` >
Watkins-Gonzalez (Soft)
@@ -117981,12 +125491,13 @@ exports[`Storyshots Views / Orders / Order details payment error 1`] = ` >
Williams, Garcia and Walker (XS)
@@ -118150,12 +125661,13 @@ exports[`Storyshots Views / Orders / Order details payment error 1`] = ` >
Williams, Garcia and Walker (XS)
@@ -119625,12 +127137,13 @@ exports[`Storyshots Views / Orders / Order details pending payment 1`] = ` >
Watkins-Gonzalez (Soft)
@@ -119794,12 +127307,13 @@ exports[`Storyshots Views / Orders / Order details pending payment 1`] = ` >
Williams, Garcia and Walker (XS)
@@ -119963,12 +127477,13 @@ exports[`Storyshots Views / Orders / Order details pending payment 1`] = ` >
Williams, Garcia and Walker (XS)
@@ -121438,12 +128953,13 @@ exports[`Storyshots Views / Orders / Order details refunded payment 1`] = ` >
Watkins-Gonzalez (Soft)
@@ -121607,12 +129123,13 @@ exports[`Storyshots Views / Orders / Order details refunded payment 1`] = ` >
Williams, Garcia and Walker (XS)
@@ -121776,12 +129293,13 @@ exports[`Storyshots Views / Orders / Order details refunded payment 1`] = ` >
Williams, Garcia and Walker (XS)
@@ -123251,12 +130769,13 @@ exports[`Storyshots Views / Orders / Order details rejected payment 1`] = ` >
Watkins-Gonzalez (Soft)
@@ -123420,12 +130939,13 @@ exports[`Storyshots Views / Orders / Order details rejected payment 1`] = ` >
Williams, Garcia and Walker (XS)
@@ -123589,12 +131109,13 @@ exports[`Storyshots Views / Orders / Order details rejected payment 1`] = ` >
Williams, Garcia and Walker (XS)
@@ -125064,12 +132585,13 @@ exports[`Storyshots Views / Orders / Order details unfulfilled 1`] = ` >
Watkins-Gonzalez (Soft)
@@ -125233,12 +132755,13 @@ exports[`Storyshots Views / Orders / Order details unfulfilled 1`] = ` >
Williams, Garcia and Walker (XS)
@@ -125402,12 +132925,13 @@ exports[`Storyshots Views / Orders / Order details unfulfilled 1`] = ` >
Williams, Garcia and Walker (XS)
@@ -126867,12 +134391,13 @@ exports[`Storyshots Views / Orders / Order draft default 1`] = ` >
Milk
@@ -134922,12 +142451,13 @@ exports[`Storyshots Views / Orders / Refund order products 1`] = ` >
Coffee
@@ -135086,12 +142616,13 @@ exports[`Storyshots Views / Orders / Refund order products 1`] = ` >
Milk
@@ -135163,12 +142694,13 @@ exports[`Storyshots Views / Orders / Refund order products 1`] = ` >
Coffee
@@ -135327,12 +142859,13 @@ exports[`Storyshots Views / Orders / Refund order products 1`] = ` >
Milk
@@ -135404,12 +142937,13 @@ exports[`Storyshots Views / Orders / Refund order products 1`] = ` >
Coffee
@@ -166795,303 +174329,9 @@ exports[`Storyshots Views / Products / Create product When loading 1`] = ` class="CardTitle-hr-id" />
-
- Available at 7 out of 5 channels -
-
-
-
-
-
- Test -
- -
-
- Hidden -
-
-
-
-
-
-
-
- Channel -
- -
-
- Hidden -
-
-
-
-
-
-
-
- Channel test -
- -
-
- Hidden -
-
-
-
-
-
-
-
- Channel USD -
- -
-
- Hidden -
-
-
-
-
-
-
-
- Channel -
- -
-
- Hidden -
-
-
-
-
-
-
-
- Channel test -
- -
-
- Hidden -
-
-
-
-
-
-
-
- Channel USD -
- -
-
- Hidden -
-
-
-
+ You will be able to define availability of product after creating variants.
-
- Available at 7 out of 5 channels -
-
-
-
-
-
- Test -
- -
-
- Hidden -
-
-
-
-
-
-
-
- Channel -
- -
-
- Hidden -
-
-
-
-
-
-
-
- Channel test -
- -
-
- Hidden -
-
-
-
-
-
-
-
- Channel USD -
- -
-
- Hidden -
-
-
-
-
-
-
-
- Channel -
- -
-
- Hidden -
-
-
-
-
-
-
-
- Channel test -
- -
-
- Hidden -
-
-
-
-
-
-
-
- Channel USD -
- -
-
- Hidden -
-
-
-
+ You will be able to define availability of product after creating variants.
-
- Available at 7 out of 5 channels -
-
-
-
-
-
- Test -
- -
-
- Hidden -
-
-
-
-
-
-
-
- Channel -
- -
-
- Hidden -
-
-
-
-
-
-
-
- Channel test -
- -
-
- Hidden -
-
-
-
-
-
-
-
- Channel USD -
- -
-
- Hidden -
-
-
-
-
-
-
-
- Channel -
- -
-
- Hidden -
-
-
-
-
-
-
-
- Channel test -
- -
-
- Hidden -
-
-
-
-
-
-
-
- Channel USD -
- -
-
- Hidden -
-
-
-
+ You will be able to define availability of product after creating variants.
@@ -170615,12 +177268,13 @@ exports[`Storyshots Views / Products / Create product variant default 1`] = `
@@ -170646,10 +177300,10 @@ exports[`Storyshots Views / Products / Create product variant default 1`] = ` colspan="2" >
@@ -171836,12 +178491,13 @@ exports[`Storyshots Views / Products / Create product variant no warehouses 1`]
@@ -171867,10 +178523,10 @@ exports[`Storyshots Views / Products / Create product variant no warehouses 1`] colspan="2" >
@@ -172954,10 +179610,10 @@ exports[`Storyshots Views / Products / Create product variant when loading data colspan="2" >
@@ -174029,12 +180686,13 @@ exports[`Storyshots Views / Products / Create product variant with errors 1`] =
@@ -174060,10 +180718,10 @@ exports[`Storyshots Views / Products / Create product variant with errors 1`] = colspan="2" >

- Available at 0 out of 5 channels + Available at 1 out of 7 channels

- Available at 0 out of 5 channels + Available at 1 out of 7 channels

- Available at 0 out of 5 channels + Available at 1 out of 7 channels

-
-
- - Pricing - -
-
-
-
-
-
- Channels that don’t have assigned prices will use their parent channel to define the price. Price will be converted to channel’s currency -
-
- - - - - - - - - - - - - -
- Channel Name - - Selling Price - - Cost Price -
- No channels found -
-
-
-
-
@@ -183332,7 +189908,7 @@ exports[`Storyshots Views / Products / Product edit no stock and no variants 1`]
- Available at 0 out of 5 channels + Available at 1 out of 7 channels

-
-
- - Pricing - -
-
-
-
-
-
- Channels that don’t have assigned prices will use their parent channel to define the price. Price will be converted to channel’s currency -
-
- - - - - - - - - - - - - -
- Channel Name - - Selling Price - - Cost Price -
- No channels found -
-
-
-
-
@@ -184971,7 +191465,7 @@ exports[`Storyshots Views / Products / Product edit no stock, no variants and no
- Available at 0 out of 5 channels + Available at 1 out of 7 channels

-
-
- - Pricing - -
-
-
-
-
-
- Channels that don’t have assigned prices will use their parent channel to define the price. Price will be converted to channel’s currency -
-
- - - - - - - - - - - - - -
- Channel Name - - Selling Price - - Cost Price -
- No channels found -
-
-
-
-
@@ -186832,7 +193244,7 @@ exports[`Storyshots Views / Products / Product edit no variants 1`] = `
- Available at 0 out of 5 channels + Available at 1 out of 7 channels

- Available at 0 out of 5 channels + Available at 1 out of 7 channels

-
- Available at 0 out of 5 channels -
-
+ You will be able to define availability of product after creating variants.
- Available at 0 out of 5 channels + Available at 1 out of 7 channels

-
-
- - Pricing - -
-
-
-
-
-
- Channels that don’t have assigned prices will use their parent channel to define the price. Price will be converted to channel’s currency -
-
- - - - - - - - - - - - - -
- Channel Name - - Selling Price - - Cost Price -
- No channels found -
-
-
-
-
@@ -193098,7 +199421,7 @@ exports[`Storyshots Views / Products / Product edit when product has no variants
- Available at 0 out of 5 channels + Available at 1 out of 7 channels

- Available at 7 out of 5 channels -
-
-
-
-
-
- Test -
- -
-
- Hidden -
-
-
-
-
-
-
-
- Channel -
- -
-
- Hidden -
-
-
-
-
-
-
-
- Channel test -
- -
-
- Hidden -
-
-
-
-
-
-
-
- Channel USD -
- -
-
- Hidden -
-
-
-
-
-
-
-
- Channel -
- -
-
- Hidden -
-
-
-
-
-
-
-
- Channel test -
- -
-
- Hidden -
-
-
-
-
-
-
-
- Channel USD -
- -
-
- Hidden -
-
+ Available at 1 out of 7 channels

@@ -209056,12 +215193,13 @@ exports[`Storyshots Views / Products / Product variant details attribute errors
@@ -209120,12 +215258,13 @@ exports[`Storyshots Views / Products / Product variant details attribute errors
@@ -209184,12 +215323,13 @@ exports[`Storyshots Views / Products / Product variant details attribute errors
@@ -210782,12 +216922,13 @@ exports[`Storyshots Views / Products / Product variant details no warehouses 1`]
@@ -210846,12 +216987,13 @@ exports[`Storyshots Views / Products / Product variant details no warehouses 1`]
@@ -210910,12 +217052,13 @@ exports[`Storyshots Views / Products / Product variant details no warehouses 1`]
@@ -210974,12 +217117,13 @@ exports[`Storyshots Views / Products / Product variant details no warehouses 1`]
@@ -212340,12 +218484,13 @@ exports[`Storyshots Views / Products / Product variant details when loaded data
@@ -212404,12 +218549,13 @@ exports[`Storyshots Views / Products / Product variant details when loaded data
@@ -212468,12 +218614,13 @@ exports[`Storyshots Views / Products / Product variant details when loaded data
@@ -212532,12 +218679,13 @@ exports[`Storyshots Views / Products / Product variant details when loaded data
@@ -214127,10 +220275,10 @@ exports[`Storyshots Views / Products / Product variant details when loading data class="MuiTableCell-root-id MuiTableCell-body-id TableCellAvatar-root-id ProductVariantNavigation-colAvatar-id" >
@@ -216751,12 +222899,13 @@ exports[`Storyshots Views / Shipping / Shipping rate create price rate 1`] = `
- Warehouse + Settings
+ Assign channels to this shipping zone so we know which orders will be supported +
+
+
+ + +
+
+
+
+
+
+ GBP +
+ +
+
+
+
+
+ PLN +
+ +
+
+
+
+
+
+ Select warehouse from which you will ship products for this shipping zone. This warehouse address will also be used to calculate taxes. +
@@ -222736,11 +229030,6 @@ exports[`Storyshots Views / Shipping / Shipping zone details default 1`] = `
-

- Select warehouse from which you will ship products for this shipping zone. This warehouse address will also be used to calculate taxes. -

- Warehouse + Settings
+ Assign channels to this shipping zone so we know which orders will be supported +
+
+
+ + +
+
+
+
+
+
+ GBP +
+ +
+
+
+
+
+ PLN +
+ +
+
+
+
+
+
+ Select warehouse from which you will ship products for this shipping zone. This warehouse address will also be used to calculate taxes. +
@@ -223707,11 +230138,6 @@ exports[`Storyshots Views / Shipping / Shipping zone details form errors 1`] = `
-

- Select warehouse from which you will ship products for this shipping zone. This warehouse address will also be used to calculate taxes. -

- Warehouse + Settings
+ Assign channels to this shipping zone so we know which orders will be supported +
+
+
+ + +
+
+
+
+
+
+ Select warehouse from which you will ship products for this shipping zone. This warehouse address will also be used to calculate taxes. +
@@ -224519,11 +231020,6 @@ exports[`Storyshots Views / Shipping / Shipping zone details loading 1`] = `
-

- Select warehouse from which you will ship products for this shipping zone. This warehouse address will also be used to calculate taxes. -

({ - label: listing.channel.name, - value: listing.channel.id -})); - const props: ProductUpdatePageProps = { ...listActionsProps, allChannelsCount: 5, + onChannelsChange: () => undefined, + currentChannels: [], + isSimpleProduct: false, categories: [product.category], - channelChoices, + channelsWithVariantsData: { + channel1: { + selectedVariantsIds: ["variantA"], + variantsIdsToRemove: ["variantB"], + variantsIdsToAdd: [] + } + }, + setChannelsData: () => undefined, + channelsData: channels, channelsErrors: [], collections, - currentChannels: [], defaultWeightUnit: "kg", disabled: false, errors: [], @@ -50,7 +55,6 @@ const props: ProductUpdatePageProps = { limits, onAssignReferencesClick: () => undefined, onBack: () => undefined, - onChannelsChange: () => undefined, onCloseDialog: () => undefined, onDelete: () => undefined, onImageDelete: () => undefined, diff --git a/src/types.ts b/src/types.ts index 8a387d964..6d2ad5939 100644 --- a/src/types.ts +++ b/src/types.ts @@ -176,6 +176,7 @@ export type ReorderAction = (event: ReorderEvent) => void; export interface FetchMoreProps { loading: boolean; hasMore: boolean; + totalCount?: number; onFetchMore: () => void; } diff --git a/src/types/globalTypes.ts b/src/types/globalTypes.ts index a781480ec..0372fb42b 100644 --- a/src/types/globalTypes.ts +++ b/src/types/globalTypes.ts @@ -121,6 +121,7 @@ export enum ChannelErrorCode { CHANNELS_CURRENCY_MUST_BE_THE_SAME = "CHANNELS_CURRENCY_MUST_BE_THE_SAME", CHANNEL_TARGET_ID_MUST_BE_DIFFERENT = "CHANNEL_TARGET_ID_MUST_BE_DIFFERENT", CHANNEL_WITH_ORDERS = "CHANNEL_WITH_ORDERS", + DUPLICATED_INPUT_ITEM = "DUPLICATED_INPUT_ITEM", GRAPHQL_ERROR = "GRAPHQL_ERROR", INVALID = "INVALID", NOT_FOUND = "NOT_FOUND", @@ -1092,7 +1093,6 @@ export interface AttributeFilterInput { filterableInStorefront?: boolean | null; filterableInDashboard?: boolean | null; availableInGrid?: boolean | null; - metadata?: (MetadataInput | null)[] | null; search?: string | null; ids?: (string | null)[] | null; type?: AttributeTypeEnum | null; @@ -1159,7 +1159,6 @@ export interface CatalogueInput { export interface CategoryFilterInput { search?: string | null; - metadata?: (MetadataInput | null)[] | null; ids?: (string | null)[] | null; } @@ -1183,6 +1182,7 @@ export interface ChannelCreateInput { name: string; slug: string; currencyCode: string; + addShippingZones?: string[] | null; } export interface ChannelDeleteInput { @@ -1193,6 +1193,8 @@ export interface ChannelUpdateInput { isActive?: boolean | null; name?: string | null; slug?: string | null; + addShippingZones?: string[] | null; + removeShippingZones?: string[] | null; } export interface CollectionChannelListingUpdateInput { @@ -1215,7 +1217,6 @@ export interface CollectionCreateInput { export interface CollectionFilterInput { published?: CollectionPublished | null; search?: string | null; - metadata?: (MetadataInput | null)[] | null; ids?: (string | null)[] | null; channel?: string | null; } @@ -1257,7 +1258,6 @@ export interface CustomerInput { email?: string | null; isActive?: boolean | null; note?: string | null; - languageCode?: LanguageCodeEnum | null; } export interface DateRangeInput { @@ -1384,7 +1384,6 @@ export interface OrderDraftFilterInput { customer?: string | null; created?: DateRangeInput | null; search?: string | null; - metadata?: (MetadataInput | null)[] | null; channels?: (string | null)[] | null; } @@ -1394,7 +1393,6 @@ export interface OrderFilterInput { customer?: string | null; created?: DateRangeInput | null; search?: string | null; - metadata?: (MetadataInput | null)[] | null; channels?: (string | null)[] | null; } @@ -1588,10 +1586,12 @@ export interface ProductChannelListingAddInput { visibleInListings?: boolean | null; isAvailableForPurchase?: boolean | null; availableForPurchaseDate?: any | null; + addVariants?: string[] | null; + removeVariants?: string[] | null; } export interface ProductChannelListingUpdateInput { - addChannels?: ProductChannelListingAddInput[] | null; + updateChannels?: ProductChannelListingAddInput[] | null; removeChannels?: string[] | null; } @@ -1620,7 +1620,6 @@ export interface ProductFilterInput { productType?: string | null; stocks?: ProductStockFilterInput | null; search?: string | null; - metadata?: (MetadataInput | null)[] | null; price?: PriceRangeInput | null; minimalPrice?: PriceRangeInput | null; productTypes?: (string | null)[] | null; @@ -1658,7 +1657,6 @@ export interface ProductTypeFilterInput { search?: string | null; configurable?: ProductTypeConfigurable | null; productType?: ProductTypeEnum | null; - metadata?: (MetadataInput | null)[] | null; ids?: (string | null)[] | null; } @@ -1800,6 +1798,7 @@ export interface ShippingZoneCreateInput { countries?: (string | null)[] | null; default?: boolean | null; addWarehouses?: (string | null)[] | null; + addChannels?: string[] | null; } export interface ShippingZoneUpdateInput { @@ -1808,7 +1807,9 @@ export interface ShippingZoneUpdateInput { countries?: (string | null)[] | null; default?: boolean | null; addWarehouses?: (string | null)[] | null; + addChannels?: string[] | null; removeWarehouses?: (string | null)[] | null; + removeChannels?: string[] | null; } export interface ShopSettingsInput { @@ -1877,7 +1878,6 @@ export interface UserCreateInput { email?: string | null; isActive?: boolean | null; note?: string | null; - languageCode?: LanguageCodeEnum | null; redirectUrl?: string | null; } diff --git a/src/utils/maps.ts b/src/utils/maps.ts index 0367e8599..4bfe0ece1 100644 --- a/src/utils/maps.ts +++ b/src/utils/maps.ts @@ -27,6 +27,10 @@ export function mapPagesToChoices( export function mapNodeToChoice( nodes: Array> ): Array { + if (!nodes) { + return []; + } + return nodes.map(node => ({ label: node.name, value: node.id