Add channel shipping zones (#1015)

* Add naked input option to SingleAutocompleteSelectField and update it's stories

* Add new icons - chevron up, down & trash

* Add deletable item component and stories

* Add card add items footer component to be used in warehouses and product stocks assign

* Update schema and types

* Add shipping zones card components

* Update channel details page form to also include shipping zones

* Update makeTopLevelSearch hook files directory and add getSearchFetchMoreProps function to avoid extracting it manually every time

* Update channels types & fragments

* Move getDefaultNotifierSuccessErrorData function to useNotifier utils, update dir etc., also make order discount provider use it from the new dir

* Add shippinh zone to channel update and create and add shipping zone search

* Update messages

* Fix types

* Fix lint, types etc

* Small refactor from review and quick fix styles of shipping zones card

* Refactor a bit and update snapshots

* Refactor a bit and update snapshots

* Addd / refactor channels availability components

* Add useChannelsWithProductVariants hook with utils and types

* Add / refactor more channels availability components

* Move avatar from table cell avatar to separate component for it to be usable outside of tables

* Add channels with variants logic to product create and update pages & views

* Refactor components to use updated channels availability components

* Remove unnecessary comments

* Update storybook

* Update types

* Update messages

* Fix prices for variants / simple product not uodating properly

* Post merge cleanup, update schema, types, etc.

* Change shipping zone details warehouses card into settings card and add ability to assign channels to shipping zone

* Update types

* Update snapshots

* Fix selecting / deselecting all channels in channels with variants modal

* Fixes after review, some types changes etc.

* Update snapshots

* Small types fixes

* Make price rates views use parent shipping method channels instead of all

* Make price rates views use parent shipping method channels instead of all

* Update types

* Fix bugs

* Fixes after review

* Fix channels availability data submission

* Fix lint

* Fix variant pricing card showing not related channels

* Fixes after review

* Fix types

* Hide unaviable variants in add products to draft order dialog

* Fix channels with variants availability modal showing confirm button as enabled when it shouldn't

* Fix types

* Update semi checked icon to match old designs

* Update types

* Update channels icon in channels with variants availability

* Fix product cypress test after product channels mutation changed

* Fix trash and chevron down colors in dark mode

* Fix shipping zones card footer not updating query after click away

* Fix types in schema, add condition not to display shipping zones select in channel details if all zones have already been selected

* Fix products adding in order draft dialog

* Fix simple productupdate

* Update snapshots after merge with master

* Update messages

* Fix product api request for cypress

* Add missing test id

* Fix selecting if product is simple -> form being submitted with empty data sometimes

* Update snapshots, messages and add fix for invalid date at product update

* Remove unnecessary imports

* Fix failing test in saleor 2552 (#1061)

* fix

* fix

* fix

Co-authored-by: Jakub Majorek <majorek.jakub@gmail.com>
Co-authored-by: Karolina <rakoczy.karolina@gmail.com>
This commit is contained in:
mmarkusik 2021-04-14 15:44:25 +02:00 committed by GitHub
parent 07bc522af1
commit 8fe66a3bde
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
170 changed files with 14601 additions and 4469 deletions

View file

@ -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{

View file

@ -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:{

View file

@ -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']",

View file

@ -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']"
};

View file

@ -1,7 +1,10 @@
// <reference types="cypress" />
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,12 +107,16 @@ describe("Shipping methods", () => {
}) => {
shippingZone = shippingZoneResp;
shippingMethod = shippingMethodResp;
addChannelToShippingZone(shippingZone.id, createdChannel.id).then(
() => {
addChannelToShippingMethod(
shippingMethod.id,
createdChannel.id,
createdChannelPrice
);
}
);
}
)
.then(() => {
cy.addAliasToGraphRequest("ShippingZone");
@ -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,

View file

@ -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,

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

View file

@ -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");

View file

@ -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();

View file

@ -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 }) {

Binary file not shown.

Binary file not shown.

View file

@ -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 <a>configure a warehouse</a>"
},
"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}"
}
}

View file

@ -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

View file

@ -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";

View file

@ -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<ChannelAvailabilityItemWrapperProps> = ({
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 (
<ExpansionPanel classes={expanderClasses}>
<ExpansionPanelSummary
expandIcon={<IconChevronDown />}
classes={summaryClasses}
>
<div className={classes.container}>
<Typography>{name}</Typography>
<Label text={intl.formatMessage(variantsLabel, { variantsCount })} />
<Label text={commonChannelMessages.availableDateText} />
</div>
</ExpansionPanelSummary>
{children}
</ExpansionPanel>
);
};
export default ChannelWithVariantsAvailabilityItemWrapper;

View file

@ -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", () => <ChannelsWithVariantsAvailabilityCard {...props} />);

View file

@ -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<ChannelsWithVariantsAvailabilityCardProps> = ({
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 <CannotDefineChannelsAvailabilityCard />;
}
return (
<ChannelsAvailabilityCardWrapper
selectedChannelsCount={selectedChannelsCount}
allChannelsCount={allChannelsCount}
openModal={openModal}
>
{channels
.filter(({ id }) =>
areAnyChannelVariantsSelected(channelsWithVariantsData[id])
)
.map(({ id }) => (
<ChannelWithVariantsAvailabilityItemWrapper
messages={channelsMessages[id]}
key={id}
channelsWithVariantsData={channelsWithVariantsData}
variants={variants}
channels={channels}
channelId={id}
>
<ChannelAvailabilityItemContent
onChange={onChange}
data={channels.find(getById(id))}
errors={errors}
messages={channelsMessages[id]}
/>
</ChannelWithVariantsAvailabilityItemWrapper>
))}
</ChannelsAvailabilityCardWrapper>
);
};
export default ChannelsWithVariantsAvailabilityCard;

View file

@ -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", () => <CannotDefineChannelsAvailabilityCard />);

View file

@ -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 = () => (
<Card>
<CardTitle title={<FormattedMessage {...messages.title} />} />
<CardContent>
<FormattedMessage {...messages.subtitle} />
</CardContent>
</Card>
);
export default CannotDefineChannelsAvailabilityCard;

View file

@ -8,6 +8,8 @@ import ChannelForm, { ChannelFormProps } from "./ChannelForm";
const props: ChannelFormProps = {
data: {
currencyCode: "euro",
shippingZonesIdsToAdd: [],
shippingZonesIdsToRemove: [],
name: "Test",
slug: "test"
},

View file

@ -26,6 +26,8 @@ export interface FormData {
name: string;
currencyCode: string;
slug: string;
shippingZonesIdsToAdd: string[];
shippingZonesIdsToRemove: string[];
}
export interface ChannelFormProps {

View file

@ -0,0 +1,11 @@
import { filter } from "fuzzaldrin";
import React from "react";
export const useChannelsSearch = function<T extends { name: string }>(
channels: T[]
) {
const [query, onQueryChange] = React.useState("");
const filteredChannels = filter<T, "name">(channels, query, { key: "name" });
return { query, onQueryChange, filteredChannels };
};

View file

@ -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", () => <ChannelsWithVariantsAvailabilityDialog {...props} />);

View file

@ -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<ChannelsAvailabilityDialogProps> = ({
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 (
<ActionDialog
confirmButtonState="default"
open={isChannelsAvailabilityModalOpen}
onClose={handleClose}
onConfirm={handleConfirm}
title={intl.formatMessage(messages.title)}
disabled={!canConfirm}
>
<ChannelsAvailabilityContentWrapper
hasAllSelected={hasAllChannelsSelected}
hasAnyChannelsToDisplay={!!filteredChannels.length}
query={query}
onQueryChange={onQueryChange}
toggleAll={toggleAllChannels}
contentType={contentType}
>
<ChannelsWithVariantsAvailabilityDialogContent
allVariants={variants}
channels={filteredChannels}
isChannelSelected={isChannelSelected}
channelsWithVariants={channelsWithVariantsData}
{...rest}
/>
</ChannelsAvailabilityContentWrapper>
</ActionDialog>
);
};
export default ChannelsWithVariantsAvailabilityDialog;

View file

@ -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<ChannelsWithVariantsAvailabilityDialogContentProps> = ({
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<any>
) =>
event.target.value
? addVariantToChannel(channelId, variantId)
: removeVariantFromChannel(channelId, variantId);
const selectChannelIcon = (channelId: string) =>
areAllChannelVariantsSelected(
allVariants,
channelsWithVariants[channelId]
) ? (
<IconCheckboxChecked />
) : (
<IconCheckboxSemiChecked />
);
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 (
<ExpansionPanel classes={expanderClasses}>
<ExpansionPanelSummary
expandIcon={<IconChevronDown />}
classes={summaryClasses}
>
<div
className={classes.channelContainer}
// stop expander when selecting & deselecting channel
onClick={event => event.stopPropagation()}
>
<div className={classes.channelCheckboxContainer}>
<ControlledCheckbox
checked={isChannelSelected(channelId)}
checkedIcon={selectChannelIcon(channelId)}
name={name}
label={
<div className={classes.channelTitleContainer}>
<Typography>{name}</Typography>
<Label
text={intl.formatMessage(
messages.variantsSelectedLabel,
{
variantsAmount: selectedVariantsIds.length
}
)}
/>
</div>
}
onChange={toggleAllChannelVariants(channelId)}
/>
</div>
<Divider />
</div>
</ExpansionPanelSummary>
{allVariants.map(({ id: variantId, name }) => (
<>
<div
key={variantId}
className={classes.variantContainer}
data-test-id="channel-row"
>
<ControlledCheckbox
checked={isVariantSelected(variantId)}
name={name}
label={
<div className={classes.variantTitleContainer}>
<Avatar thumbnail={getVariantThumbnailSrc(variantId)} />
<Typography>{name}</Typography>
</div>
}
onChange={handleVariantChange(channelId, variantId)}
/>
</div>
<Divider />
</>
))}
</ExpansionPanel>
);
})}
</>
);
};
export default ChannelsWithVariantsAvailabilityDialogContent;

View file

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

View file

@ -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<ShippingZoneItemProps> = ({
zone,
onDelete
}) => {
const { id, name } = zone;
const classes = useStyles({});
return (
<>
<div className={classes.container}>
<Typography>{name}</Typography>
<DeletableItem id={id} onDelete={onDelete} />
</div>
<Divider />
</>
);
};
export default ShippingZoneItem;

View file

@ -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", () => <ShippingZonesCard {...baseProps} />)
.add("with options selected", () => (
<ShippingZonesCard
{...baseProps}
shippingZones={shippingZones as Channel_channel_shippingZones[]}
/>
));

View file

@ -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<ShippingZonesCardProps> = props => {
const {
shippingZones,
removeShippingZone,
fetchMoreShippingZones: { totalCount }
} = props;
const expanderClasses = useExpanderStyles({});
const intl = useIntl();
const hasMoreZonesToBeSelected = totalCount !== shippingZones.length;
return (
<Card>
<CardTitle title={intl.formatMessage(messages.title)} />
<CardContent>
<Typography>{intl.formatMessage(messages.subtitle)}</Typography>
</CardContent>
<ExpansionPanel classes={expanderClasses}>
<ShippingZonesListHeader shippingZones={shippingZones} />
<Divider />
{shippingZones.map(zone => (
<ShippingZoneItem zone={zone} onDelete={removeShippingZone} />
))}
{hasMoreZonesToBeSelected ? (
<ShippingZonesCardListFooter {...props} />
) : null}
</ExpansionPanel>
</Card>
);
};
export default ShippingZonesCard;

View file

@ -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<ShippingZonesCardListFooterProps> = ({
shippingZonesChoices,
searchShippingZones,
fetchMoreShippingZones,
addShippingZone,
shippingZones
}) => {
const [isChoicesSelectShown, setIsChoicesSelectShown] = useState(false);
const shippingZonesRef = useRef<Channel_channel_shippingZones[]>(
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 ? (
<ClickAwayListener onClickAway={handleFooterClickAway}>
<div>
<SingleAutocompleteSelectField
value=""
displayValue=""
nakedInput
name="shippingZone"
choices={mapNodeToChoice(shippingZonesChoices)}
fetchChoices={searchShippingZones}
onChange={handleChoice}
{...fetchMoreShippingZones}
/>
</div>
</ClickAwayListener>
) : (
<CardAddItemsFooter
onAdd={() => setIsChoicesSelectShown(true)}
title={messages.addZoneTitle}
testIds={{
link: "add-shipping-zone-link",
button: "add-shipping-zone-button"
}}
/>
);
};
export default ShippingZonesCardListFooter;

View file

@ -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<ShippingZonesListHeaderProps> = ({
shippingZones
}) => {
const classes = useStyles({});
const intl = useIntl();
return (
<div className={classes.container}>
<ExpansionPanelSummary expandIcon={<IconChevronDown />} classes={classes}>
<Typography variant="subtitle2" color="textSecondary">
{intl.formatMessage(messages.title, {
zonesCount: shippingZones.length
})}
</Typography>
</ExpansionPanelSummary>
<HorizontalSpacer spacing={1.5} />
</div>
);
};
export default ShippingZonesListHeader;

View file

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

View file

@ -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[];
}

View file

@ -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,

View file

@ -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)

View file

@ -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<ChannelDetailsPageProps> = ({
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 (
<Form onSubmit={onSubmit} initial={channel || initialData}>
{({ change, data, hasChanged, submit }) => {
<Form onSubmit={onSubmit} initial={initialData}>
{({ 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<ChannelDetailsPageProps> = ({
errors={errors}
/>
</div>
{!!updateChannelStatus && (
<div>
{!!updateChannelStatus && (
<>
<ChannelStatus
isActive={channel?.isActive}
disabled={disabledStatus}
updateChannelStatus={updateChannelStatus}
/>
</div>
<CardSpacer />
</>
)}
<ShippingZonesCard
shippingZonesChoices={getFilteredShippingZonesChoices()}
shippingZones={shippingZonesToDisplay}
addShippingZone={addShippingZone}
removeShippingZone={removeShippingZone}
searchShippingZones={searchShippingZones}
fetchMoreShippingZones={fetchMoreShippingZones}
/>
</div>
</Grid>
<SaveButtonBar
onCancel={onBack}

View file

@ -0,0 +1,7 @@
import uniq from "lodash-es/uniq";
export const getUpdatedIdsWithNewId = (ids: string[], newId: string) =>
uniq([...ids, newId]);
export const getUpdatedIdsWithoutNewId = (ids: string[], newId: string) =>
ids.filter(id => id !== newId);

View file

@ -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 {

View file

@ -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 {

View file

@ -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 {

View file

@ -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 {

View file

@ -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 {

View file

@ -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 {

View file

@ -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 => {
productData?.channelListings?.map(
({
channel,
availableForPurchase,
isAvailableForPurchase,
visibleInListings,
publicationDate,
isPublished
}) => {
const variantChannel = productData.variants[0]?.channelListings.find(
listing => listing.channel.id === option.channel.id
listing => listing.channel.id === channel.id
);
const price = variantChannel?.price;
const costPrice = variantChannel?.costPrice;
const variantsIds = extractVariantsIdsForChannel(
productData.variants,
channel.id
);
return {
availableForPurchase: option?.availableForPurchase,
costPrice: costPrice ? costPrice.amount.toString() : "",
availableForPurchase,
isPublished,
publicationDate,
variantsIds,
costPrice: costPrice?.amount.toString() ?? "",
currency: price ? price.currency : "",
id: option.channel.id,
isAvailableForPurchase: !!option?.isAvailableForPurchase,
isPublished: option.isPublished,
name: option.channel.name,
id: channel.id,
isAvailableForPurchase: !!isAvailableForPurchase,
name: channel.name,
price: price ? price.amount.toString() : "",
publicationDate: option.publicationDate,
visibleInListings: !!option.visibleInListings
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)
);

View file

@ -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,25 +26,38 @@ 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 => ({
@ -77,6 +93,12 @@ export const ChannelCreateView = ({}) => {
})}
/>
<ChannelDetailsPage
searchShippingZones={searchShippingZones}
searchShippingZonesData={searchShippingZonesResult.data}
fetchMoreShippingZones={getSearchFetchMoreProps(
searchShippingZonesResult,
fetchMoreShippingZones
)}
disabled={createChannelOpts.loading}
errors={createChannelOpts?.data?.channelCreate?.errors || []}
currencyCodes={currencyCodeChoices}

View file

@ -1,21 +1,24 @@
import ChannelDeleteDialog from "@saleor/channels/components/ChannelDeleteDialog";
import { FormData } from "@saleor/channels/components/ChannelForm/ChannelForm";
import { ChannelDelete } from "@saleor/channels/types/ChannelDelete";
import { getChannelsCurrencyChoices } from "@saleor/channels/utils";
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 { ChannelErrorFragment } from "@saleor/fragments/types/ChannelErrorFragment";
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 getChannelsErrorMessage from "@saleor/utils/errors/channels";
import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers";
import React from "react";
import { useIntl } from "react-intl";
import { ChannelUpdateInput } from "../../../types/globalTypes";
import {
useChannelActivateMutation,
useChannelDeactivateMutation,
@ -54,14 +57,15 @@ export const ChannelDetails: React.FC<ChannelDetailsProps> = ({
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<ChannelDetailsProps> = ({
});
};
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<ChannelDetailsProps> = ({
}
});
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<ChannelDetailsProps> = ({
};
const [deleteChannel, deleteChannelOpts] = useChannelDeleteMutation({
onCompleted
onCompleted: onDeleteCompleted
});
const channelsChoices = getChannelsCurrencyChoices(
@ -146,6 +151,14 @@ export const ChannelDetails: React.FC<ChannelDetailsProps> = ({
deleteChannel({ variables: data });
};
const {
loadMore: fetchMoreShippingZones,
search: searchShippingZones,
result: searchShippingZonesResult
} = useShippingZonesSearch({
variables: DEFAULT_INITIAL_SEARCH_DATA
});
return (
<>
<WindowTitle
@ -160,15 +173,21 @@ export const ChannelDetails: React.FC<ChannelDetailsProps> = ({
</AppHeader>
<PageHeader title={data?.channel?.name} />
<ChannelDetailsPage
searchShippingZones={searchShippingZones}
searchShippingZonesData={searchShippingZonesResult.data}
fetchMoreShippingZones={getSearchFetchMoreProps(
searchShippingZonesResult,
fetchMoreShippingZones
)}
channel={data?.channel}
disabled={updateChannelOpts.loading || loading}
disabledStatus={
activateChannelOpts.loading || deactivateChannelOpts.loading
}
errors={updateChannelOpts?.data?.channelUpdate?.errors || []}
onSubmit={handleSubmit}
onBack={handleBack}
onDelete={() => openModal("remove")}
onSubmit={handleSubmit}
updateChannelStatus={() =>
data?.channel?.isActive
? deactivateChannel({ variables: { id } })

View file

@ -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<CollectionCreatePageProps> = ({
<Metadata data={data} onChange={handlers.changeMetadata} />
</div>
<div>
<AvailabilityCard
<ChannelsAvailabilityCard
messages={{
hiddenLabel: intl.formatMessage({
defaultMessage: "Hidden",

View file

@ -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";
@ -124,7 +124,7 @@ const CollectionDetailsPage: React.FC<CollectionDetailsPageProps> = ({
</div>
<div>
<div>
<AvailabilityCard
<ChannelsAvailabilityCard
messages={{
hiddenLabel: intl.formatMessage({
defaultMessage: "Hidden",

View file

@ -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";

View file

@ -1,26 +0,0 @@
import { createChannelsDataFromProduct } from "@saleor/channels/utils";
import { product } from "@saleor/products/fixtures";
import Decorator from "@saleor/storybook/Decorator";
import { storiesOf } from "@storybook/react";
import React from "react";
const productChannels = createChannelsDataFromProduct(product(""));
import AvailabilityCard from "./AvailabilityCard";
const props = {
allChannelsCount: 4,
channels: productChannels,
errors: [],
messages: {
hiddenLabel: "Not published",
hiddenSecondLabel: "hidden label",
visibleLabel: "Published"
},
onChange: () => undefined,
openModal: () => undefined,
selectedChannelsCount: 3
};
storiesOf("Generics / AvailabilityCard", module)
.addDecorator(Decorator)
.add("default", () => <AvailabilityCard {...props} />);

View file

@ -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<AvailabilityCardProps &
Omit<ChannelsAvailabilityProps, "channelsMessages">> = props => {
const intl = useIntl();
const localizeDate = useDateLocalize();
return (
<ChannelsAvailability
{...props}
channelsMessages={props.channels.reduce(
(prevVal, currVal) => ({
...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;

View file

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

View file

@ -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<ChannelProps> = ({
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 (
<>
<div className={classes.channelItem}>
<div
data-test="channel-availability-item"
role="button"
className={classes.channelBtn}
onClick={() => setOpen(open => !open)}
>
<div className={classes.channelName}>
<Typography>{name}</Typography>
<ArrowDropdown
className={classNames(classes.arrow, {
[classes.rotate]: isOpen
})}
color="primary"
/>
</div>
<Typography variant="caption">
{messages.availableDateText}
</Typography>
</div>
{isOpen && (
<>
<RadioSwitchField
disabled={disabled}
firstOptionLabel={
<>
<p className={classes.label}>{messages.visibleLabel}</p>
{isPublished &&
publicationDate &&
Date.parse(publicationDate) < dateNow && (
<span className={classes.secondLabel}>
{messages.visibleSecondLabel ||
visibleMessage(publicationDate)}
</span>
)}
</>
}
name="isPublished"
secondOptionLabel={
<>
<p className={classes.label}>{messages.hiddenLabel}</p>
{publicationDate &&
!isPublished &&
Date.parse(publicationDate) >= dateNow && (
<span className={classes.secondLabel}>
{messages.hiddenSecondLabel}
</span>
)}
</>
}
value={isPublished}
onChange={() => {
onChange(id, {
...formData,
isPublished: !isPublished,
publicationDate:
!isPublished && !publicationDate
? todayDate
: publicationDate
});
}}
/>
{!isPublished && (
<>
<Typography
className={classes.setPublicationDate}
onClick={() => setPublicationDate(!isPublicationDate)}
>
{intl.formatMessage({
defaultMessage: "Set publication date"
})}
</Typography>
{isPublicationDate && (
<TextField
error={!!formErrors.publicationDate}
disabled={disabled}
label={intl.formatMessage({
defaultMessage: "Publish on",
description: "publish on date"
})}
name={`channel:publicationDate:${id}`}
type="date"
fullWidth={true}
helperText={
formErrors.publicationDate
? getProductErrorMessage(
formErrors.publicationDate,
intl
)
: ""
}
value={publicationDate || ""}
onChange={e =>
onChange(id, {
...formData,
publicationDate: e.target.value || null
})
}
className={classes.date}
InputLabelProps={{
shrink: true
}}
/>
)}
</>
)}
{hasAvailableProps && (
<>
<Hr />
<RadioSwitchField
disabled={disabled}
firstOptionLabel={
<>
<p className={classes.label}>{messages.availableLabel}</p>
{isAvailable &&
availableForPurchase &&
Date.parse(availableForPurchase) < dateNow && (
<span className={classes.secondLabel}>
{visibleMessage(availableForPurchase)}
</span>
)}
</>
}
name={`channel:isAvailableForPurchase:${id}`}
secondOptionLabel={
<>
<p className={classes.label}>
{messages.unavailableLabel}
</p>
{availableForPurchase && !isAvailable && (
<span className={classes.secondLabel}>
{messages.availableSecondLabel}
</span>
)}
</>
}
value={isAvailable}
onChange={e => {
const { value } = e.target;
return onChange(id, {
...formData,
availableForPurchase: !value
? null
: availableForPurchase,
isAvailableForPurchase: value
});
}}
/>
{!isAvailable && (
<>
<Typography
className={classes.setPublicationDate}
onClick={() => setAvailableDate(!isAvailableDate)}
>
{messages.setAvailabilityDateLabel}
</Typography>
{isAvailableDate && (
<TextField
error={!!formErrors.availableForPurchaseDate}
disabled={disabled}
label={intl.formatMessage({
defaultMessage: "Set available on",
description: "available on date"
})}
name={`channel:availableForPurchase:${id}`}
type="date"
fullWidth={true}
helperText={
formErrors.availableForPurchaseDate
? getProductErrorMessage(
formErrors.availableForPurchaseDate,
intl
)
: ""
}
value={availableForPurchase ? availableForPurchase : ""}
onChange={e =>
onChange(id, {
...formData,
availableForPurchase: e.target.value
})
}
className={classes.date}
InputLabelProps={{
shrink: true
}}
/>
)}
</>
)}
</>
)}
{visibleInListings !== undefined && (
<>
<Hr />
<ControlledCheckbox
className={classes.checkbox}
name={`channel:visibleInListings:${id}`}
checked={visibleInListings}
disabled={disabled}
label={
<>
<p
className={classNames(
classes.label,
classes.listingLabel
)}
>
{intl.formatMessage({
defaultMessage: "Show in product listings"
})}
</p>
<span className={classes.secondLabel}>
{intl.formatMessage({
defaultMessage:
"Disabling this checkbox will remove product from search and category pages. It will be available on collection pages."
})}
</span>
</>
}
onChange={e =>
onChange(id, {
...formData,
visibleInListings: e.target.value
})
}
/>
</>
)}
</>
)}
</div>
<Hr className={classes.hr} />
</>
);
};
export const ChannelsAvailability: React.FC<ChannelsAvailabilityProps> = 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 (
<>
<Card>
<CardTitle
title={intl.formatMessage({
defaultMessage: "Availability",
description: "section header"
})}
toolbar={
<RequirePermissions
userPermissions={user?.userPermissions || []}
requiredPermissions={[PermissionEnum.MANAGE_CHANNELS]}
>
<Button
color="primary"
onClick={openModal}
data-test-id="channels-availiability-manage-button"
>
{intl.formatMessage({
defaultMessage: "Manage",
description: "section header button"
})}
</Button>
</RequirePermissions>
}
/>
<CardContent className={classes.card}>
{!!channelsAvailabilityText && (
<>
<Typography className={classes.channelInfo}>
{channelsAvailabilityText}
</Typography>
<Hr className={classes.hr} />
</>
)}
{channels
? channels.map(data => {
const channelErrors =
errors?.filter(error => error.channels.includes(data.id)) ||
[];
return (
<Channel
key={data.id}
data={data}
errors={channelErrors}
onChange={onChange}
messages={channelsMessages[data.id]}
/>
);
})
: channelsList
? channelsList.map(data => (
<React.Fragment key={data.id}>
<div className={classes.channelItem}>
<div className={classes.channelName}>
<Typography>{data.name}</Typography>
</div>
</div>
<Hr className={classes.hr} />
</React.Fragment>
))
: null}
</CardContent>
</Card>
</>
);
};
ChannelsAvailability.displayName = "ChannelsAvailability";
export default ChannelsAvailability;

View file

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

View file

@ -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<ChannelContentProps> = ({
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 (
<div className={classes.container}>
<RadioSwitchField
className={classes.radioField}
disabled={disabled}
firstOptionLabel={
<>
<p className={classes.label}>{messages.visibleLabel}</p>
{isPublished &&
publicationDate &&
Date.parse(publicationDate) < dateNow && (
<span className={classes.secondLabel}>
{messages.visibleSecondLabel ||
visibleMessage(publicationDate)}
</span>
)}
</>
}
name="isPublished"
secondOptionLabel={
<>
<p className={classes.label}>{messages.hiddenLabel}</p>
{publicationDate &&
!isPublished &&
Date.parse(publicationDate) >= dateNow && (
<span className={classes.secondLabel}>
{messages.hiddenSecondLabel}
</span>
)}
</>
}
value={isPublished}
onChange={() => {
onChange(id, {
...formData,
isPublished: !isPublished,
publicationDate:
!isPublished && !publicationDate ? todayDate : publicationDate
});
}}
/>
{!isPublished && (
<>
<Typography
className={classes.setPublicationDate}
onClick={() => setPublicationDate(!isPublicationDate)}
>
{intl.formatMessage({
defaultMessage: "Set publication date"
})}
</Typography>
{isPublicationDate && (
<TextField
error={!!formErrors.publicationDate}
disabled={disabled}
label={intl.formatMessage({
defaultMessage: "Publish on",
description: "publish on date"
})}
name={`channel:publicationDate:${id}`}
type="date"
fullWidth={true}
helperText={
formErrors.publicationDate
? getProductErrorMessage(formErrors.publicationDate, intl)
: ""
}
value={publicationDate || ""}
onChange={e =>
onChange(id, {
...formData,
publicationDate: e.target.value || null
})
}
className={classes.date}
InputLabelProps={{
shrink: true
}}
/>
)}
</>
)}
{hasAvailableProps && (
<>
<Hr />
<RadioSwitchField
className={classes.radioField}
disabled={disabled}
firstOptionLabel={
<>
<p className={classes.label}>{messages.availableLabel}</p>
{isAvailable &&
availableForPurchase &&
Date.parse(availableForPurchase) < dateNow && (
<span className={classes.secondLabel}>
{visibleMessage(availableForPurchase)}
</span>
)}
</>
}
name={`channel:isAvailableForPurchase:${id}`}
secondOptionLabel={
<>
<p className={classes.label}>{messages.unavailableLabel}</p>
{availableForPurchase && !isAvailable && (
<span className={classes.secondLabel}>
{messages.availableSecondLabel}
</span>
)}
</>
}
value={isAvailable}
onChange={e => {
const { value } = e.target;
return onChange(id, {
...formData,
availableForPurchase: !value ? null : availableForPurchase,
isAvailableForPurchase: value
});
}}
/>
{!isAvailable && (
<>
<Typography
className={classes.setPublicationDate}
onClick={() => setAvailableDate(!isAvailableDate)}
>
{messages.setAvailabilityDateLabel}
</Typography>
{isAvailableDate && (
<TextField
error={!!formErrors.availableForPurchaseDate}
disabled={disabled}
label={intl.formatMessage({
defaultMessage: "Set available on",
description: "available on date"
})}
name={`channel:availableForPurchase:${id}`}
type="date"
fullWidth={true}
helperText={
formErrors.availableForPurchaseDate
? getProductErrorMessage(
formErrors.availableForPurchaseDate,
intl
)
: ""
}
value={availableForPurchase ? availableForPurchase : ""}
onChange={e =>
onChange(id, {
...formData,
availableForPurchase: e.target.value
})
}
className={classes.date}
InputLabelProps={{
shrink: true
}}
/>
)}
</>
)}
</>
)}
{visibleInListings !== undefined && (
<>
<Hr />
<ControlledCheckbox
className={classes.checkbox}
name={`channel:visibleInListings:${id}`}
checked={visibleInListings}
disabled={disabled}
label={
<>
<p className={classNames(classes.label, classes.listingLabel)}>
{intl.formatMessage({
defaultMessage: "Show in product listings"
})}
</p>
<span className={classes.secondLabel}>
{intl.formatMessage({
defaultMessage:
"Disabling this checkbox will remove product from search and category pages. It will be available on collection pages."
})}
</span>
</>
}
onChange={e =>
onChange(id, {
...formData,
visibleInListings: e.target.value
})
}
/>
</>
)}
</div>
);
};
export default ChannelContent;

View file

@ -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<ChannelContentWrapperProps> = ({
data,
messages,
children
}) => {
const expanderClasses = useExpanderStyles({});
const summaryClasses = useSummaryStyles({});
const { name } = data;
return (
<ExpansionPanel
classes={expanderClasses}
data-test="channel-availability-item"
>
<ExpansionPanelSummary
expandIcon={<IconChevronDown />}
classes={summaryClasses}
>
<Typography>{name}</Typography>
<Typography variant="caption">{messages.availableDateText}</Typography>
</ExpansionPanelSummary>
{children}
</ExpansionPanel>
);
};
export default ChannelContentWrapper;

View file

@ -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", () => <ChannelsAvailability {...props} />)
.add("default", () => <ChannelsAvailabilityCard {...props} />)
.add("with onChange", () => (
<ChannelsAvailability
<ChannelsAvailabilityCard
{...props}
channelsList={undefined}
channels={productChannels}
channelsMessages={productChannels.reduce(
messages={productChannels.reduce(
(prevVal, currVal) => ({
...prevVal,
[currVal.id]: {
@ -60,7 +61,7 @@ storiesOf("Generics / ChannelsAvailability", module)
hiddenSecondLabel: "Will become published"
}
}),
{}
{} as Messages
)}
/>
));

View file

@ -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<ChannelsAvailabilityWrapperProps, "children"> {
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<ChannelsAvailabilityCardProps> = 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 (
<ChannelsAvailabilityCardWrapper
selectedChannelsCount={selectedChannelsCount}
allChannelsCount={allChannelsCount}
openModal={openModal}
>
{channels
? channels.map(data => {
const channelErrors =
errors?.filter(error => error.channels.includes(data.id)) || [];
return (
<ChannelAvailabilityItemWrapper messages={messages} data={data}>
<ChannelAvailabilityItemContent
data={data}
onChange={onChange}
messages={channelsMessages[data.id]}
errors={channelErrors}
/>
</ChannelAvailabilityItemWrapper>
);
})
: channelsList
? channelsList.map(data => (
<React.Fragment key={data.id}>
<div className={classes.channelItem}>
<div className={classes.channelName}>
<Typography>{data.name}</Typography>
</div>
</div>
<Hr className={classes.hr} />
</React.Fragment>
))
: null}
</ChannelsAvailabilityCardWrapper>
);
};
export default ChannelsAvailability;

View file

@ -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<ChannelsAvailabilityWrapperProps> = 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 (
<>
<Card>
<CardTitle
title={intl.formatMessage({
defaultMessage: "Availability",
description: "section header"
})}
toolbar={
<RequirePermissions
userPermissions={user?.userPermissions || []}
requiredPermissions={[PermissionEnum.MANAGE_CHANNELS]}
>
<Button
color="primary"
onClick={openModal}
data-test-id="channels-availiability-manage-button"
>
{intl.formatMessage({
defaultMessage: "Manage",
description: "section header button"
})}
</Button>
</RequirePermissions>
}
/>
<CardContent className={classes.card}>
{!!channelsAvailabilityText && (
<>
<Typography className={classes.channelInfo}>
{channelsAvailabilityText}
</Typography>
<Hr className={classes.hr} />
</>
)}
{children}
</CardContent>
</Card>
</>
);
};
export default ChannelsAvailabilityWrapper;

View file

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

View file

@ -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" }
);

View file

@ -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;

View file

@ -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
);

View file

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

View file

@ -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,16 +26,25 @@ export const ChannelsAvailabilityDialog: React.FC<ChannelsAvailabilityDialogProp
isSelected,
channels,
confirmButtonState,
contentType = "",
contentType,
disabled,
open,
onClose,
onChange,
onConfirm,
selected = 0,
selected,
title,
toggleAll
}) => (
}) => {
const { query, onQueryChange, filteredChannels } = useChannelsSearch(
channels
);
const handleToggleAll = () => toggleAll(channels, selected);
const hasAllSelected = selected === channels.length;
return (
<ActionDialog
confirmButtonState={confirmButtonState}
open={open}
@ -41,16 +53,22 @@ export const ChannelsAvailabilityDialog: React.FC<ChannelsAvailabilityDialogProp
title={title}
disabled={disabled}
>
<ChannelsAvailabilityContent
channels={channels}
disabled={disabled}
<ChannelsAvailabilityDialogWrapper
hasAnyChannelsToDisplay={!!filteredChannels.length}
hasAllSelected={hasAllSelected}
query={query}
onQueryChange={onQueryChange}
toggleAll={handleToggleAll}
contentType={contentType}
isSelected={isSelected}
selected={selected}
toggleAll={toggleAll}
>
<ChannelsAvailabilityDialogChannelsList
channels={filteredChannels}
isChannelSelected={isSelected}
onChange={onChange}
/>
</ChannelsAvailabilityDialogWrapper>
</ActionDialog>
);
ChannelsAvailabilityDialog.displayName = "ChannelsAvailabilityDialog";
);
};
export default ChannelsAvailabilityDialog;

View file

@ -0,0 +1,11 @@
import { filter } from "fuzzaldrin";
import React from "react";
export const useChannelsSearch = function<T extends { name: string }>(
channels: T[]
) {
const [query, onQueryChange] = React.useState("");
const filteredChannels = filter<T, "name">(channels, query, { key: "name" });
return { query, onQueryChange, filteredChannels };
};

View file

@ -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<ChannelsAvailabilityContentProps> = ({
isChannelSelected,
channels,
onChange
}) => {
const classes = useStyles({});
return (
<>
{channels.map(option => (
<div
key={option.id}
className={classes.option}
data-test-id="channel-row"
>
<ControlledCheckbox
checked={isChannelSelected(option)}
name={option.name}
label={
<Typography className={classes.label}>{option.name}</Typography>
}
onChange={() => onChange(option)}
/>
<Hr />
</div>
))}
</>
);
};
export default ChannelsAvailabilityContent;

View file

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

View file

@ -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<ChannelsAvailabilityContentProps> = ({
contentType = "",
toggleAll,
toggleAllLabel,
children,
hasAnyChannelsToDisplay,
query,
onQueryChange,
hasAllSelected
}) => {
const classes = useStyles({});
const intl = useIntl();
const searchText = intl.formatMessage({
defaultMessage: "Search through channels"
});
return (
<div className={classes.content}>
{!!contentType && (
<Typography className={classes.text} variant="caption">
<FormattedMessage {...messages.selectTitle} />
</Typography>
)}
<TextField
name="query"
value={query}
className={classes.input}
onChange={e => onQueryChange(e.target.value)}
label={searchText}
placeholder={searchText}
fullWidth
/>
<div className={classes.dialog}>
{!!toggleAll && (
<>
<ControlledCheckbox
checked={hasAllSelected}
name="allChannels"
label={
toggleAllLabel || (
<Label
text={intl.formatMessage(messages.selectAllChannelsLabel)}
/>
)
}
onChange={toggleAll}
/>
<Hr />
</>
)}
<Typography className={classes.contentTitle}>
<FormattedMessage {...messages.channelsAlphabeticallyTitle} />
</Typography>
<div
className={classes.scrollArea}
data-test-id="manage-products-channels-availiability-list"
>
{hasAnyChannelsToDisplay ? (
children
) : (
<div className={classes.notFound}>
<FormattedMessage {...messages.notFoundTitle} />
</div>
)}
</div>
</div>
</div>
);
};
export default ChannelsAvailabilityContentWrapper;

View file

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

View file

@ -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<ControlledCheckboxProps> = ({
name,
label,
onChange,
checkedIcon,
...props
}) => (
<FormControlLabel
disabled={disabled}
control={
<Checkbox
checkedIcon={checkedIcon}
checked={!!checked}
disabled={disabled}
name={name}

View file

@ -0,0 +1,9 @@
import CommonDecorator from "@saleor/storybook/Decorator";
import { storiesOf } from "@storybook/react";
import React from "react";
import DeletableItem from "./DeletableItem";
storiesOf("Generics / Deletable Item", module)
.addDecorator(CommonDecorator)
.add("default", () => <DeletableItem id="1" onDelete={() => undefined} />);

View file

@ -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<DeletableItemProps> = ({ onDelete, id }) => {
const classes = useStyles({});
const handleDelete = () => onDelete(id);
return (
<div className={classes.container} onClick={handleDelete}>
<TrashIcon />
</div>
);
};
export default DeletableItem;

View file

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

View file

@ -31,7 +31,7 @@ const Story: React.FC<Partial<
SingleAutocompleteSelectFieldProps & {
enableLoadMore: boolean;
}
>> = ({ allowCustomValues, emptyOption, enableLoadMore }) => {
>> = ({ allowCustomValues, emptyOption, enableLoadMore, nakedInput }) => {
const [displayValue, setDisplayValue] = React.useState(suggestions[0].label);
return (
@ -59,6 +59,7 @@ const Story: React.FC<Partial<
onFetchMore={enableLoadMore ? onFetchMore : undefined}
allowCustomValues={allowCustomValues}
emptyOption={emptyOption}
nakedInput={nakedInput}
/>
);
}}
@ -104,6 +105,7 @@ storiesOf("Generics / Select with autocomplete", module)
.add("no data", () => (
<SingleAutocompleteSelectFieldContent {...contentProps} choices={[]} />
))
.add("naked", () => <Story nakedInput />)
.add("interactive", () => <Story />)
.add("interactive with custom option", () => (
<Story allowCustomValues={true} />

View file

@ -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<any>) => void;
FormHelperTextProps?: ExtendedFormHelperTextProps;
nakedInput?: boolean;
}
const DebounceAutocomplete: React.ComponentType<DebounceProps<
@ -72,6 +77,7 @@ const SingleAutocompleteSelectFieldComponent: React.FC<SingleAutocompleteSelectF
onChange,
onFetchMore,
FormHelperTextProps,
nakedInput = false,
...rest
} = props;
const classes = useStyles(props);
@ -157,13 +163,9 @@ const SingleAutocompleteSelectFieldComponent: React.FC<SingleAutocompleteSelectF
ensureProperValues();
}
return (
<div
className={classNames(classes.container, className)}
{...rest}
>
<TextField
InputProps={{
const TextFieldComponent = nakedInput ? InputBase : TextField;
const commonInputProps = {
...InputProps,
...getInputProps({
placeholder
@ -177,7 +179,25 @@ const SingleAutocompleteSelectFieldComponent: React.FC<SingleAutocompleteSelectF
id: undefined,
onBlur: handleBlur,
onClick: toggleMenu
}}
};
const nakedInputProps = nakedInput
? {
"aria-label": "naked",
...commonInputProps,
autoFocus: true,
className: classes.nakedInput
}
: {};
return (
<div
className={classNames(classes.container, className)}
{...rest}
>
<TextFieldComponent
{...nakedInputProps}
InputProps={commonInputProps}
error={error}
disabled={disabled}
helperText={helperText}

View file

@ -0,0 +1,84 @@
import { makeStyles } from "@material-ui/core";
import MuiAvatar from "@material-ui/core/Avatar";
import Cached from "@material-ui/icons/Cached";
import classNames from "classnames";
import React from "react";
import Image from "../../icons/Image";
export const AVATAR_MARGIN = 32;
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
},
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<AvatarProps> = ({
children,
alignRight,
thumbnail,
avatarProps
}) => {
const classes = useStyles({});
return (
<div
className={classNames(classes.content, {
[classes.alignRight]: alignRight
})}
>
{thumbnail === undefined ? (
<MuiAvatar className={classNames(classes.avatar, avatarProps)}>
<Cached color="primary" />
</MuiAvatar>
) : thumbnail === null ? (
<MuiAvatar className={classNames(classes.avatar, avatarProps)}>
<Image color="primary" />
</MuiAvatar>
) : (
<MuiAvatar
className={classNames(classes.avatar, avatarProps)}
src={thumbnail}
/>
)}
{!alignRight && <div className={classes.children}>{children}</div>}
</div>
);
};
export default Avatar;

View file

@ -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<TableCellAvatarProps> = props => {
const {
children,
className,
alignRight,
thumbnail,
avatarProps,
...rest
} = props;
const { className, ...rest } = props;
const classes = useStyles(props);
return (
<TableCell className={classNames(classes.root, className)} {...rest}>
<div
className={classNames(classes.content, {
[classes.alignRight]: alignRight
})}
>
{thumbnail === undefined ? (
<Avatar className={classNames(classes.avatar, avatarProps)}>
<Cached color="primary" />
</Avatar>
) : thumbnail === null ? (
<Avatar className={classNames(classes.avatar, avatarProps)}>
<Image color="primary" />
</Avatar>
) : (
<Avatar
className={classNames(classes.avatar, avatarProps)}
src={thumbnail}
/>
)}
{!alignRight && <div className={classes.children}>{children}</div>}
</div>
<Avatar {...rest} />
</TableCell>
);
};
TableCellAvatar.displayName = "TableCellAvatar";
export default TableCellAvatar;

View file

@ -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";

View file

@ -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<SaleCreatePageProps> = ({
/>
</div>
<div>
<ChannelsAvailability
<ChannelsAvailabilityCard
selectedChannelsCount={data.channelListings.length}
allChannelsCount={allChannelsCount}
channelsList={data.channelListings.map(channel => ({

View file

@ -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<SaleDetailsPageProps> = ({
sale={sale}
/>
<CardSpacer />
<ChannelsAvailability
<ChannelsAvailabilityCard
selectedChannelsCount={data.channelListings.length}
allChannelsCount={allChannelsCount}
channelsList={data.channelListings.map(channel => ({

View file

@ -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<VoucherCreatePageProps> = ({
/>
</div>
<div>
<ChannelsAvailability
<ChannelsAvailabilityCard
selectedChannelsCount={data.channelListings.length}
allChannelsCount={allChannelsCount}
channelsList={data.channelListings.map(channel => ({

View file

@ -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<VoucherDetailsPageProps> = ({
selectedChannelId={selectedChannelId}
/>
<CardSpacer />
<ChannelsAvailability
<ChannelsAvailabilityCard
selectedChannelsCount={data.channelListings.length}
allChannelsCount={allChannelsCount}
channelsList={data.channelListings.map(channel => ({

View file

@ -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
}
}
`;

View file

@ -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}

View file

@ -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[];
}

View file

@ -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;

View file

@ -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[];
}

View file

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

View file

@ -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

View file

@ -0,0 +1,12 @@
export interface CommonSearchOpts {
onLoadMore?: () => void;
loading?: boolean;
data?: {
search?: {
totalCount?: number;
pageInfo?: {
hasNextPage: boolean;
};
};
};
}

View file

@ -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) || [];

View file

@ -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) =>

View file

@ -100,11 +100,12 @@ function useForm<T extends FormData>(
setData(initial);
}
function set(newData: Partial<T>) {
function set(newData: Partial<T>, setHasChanged = true) {
setData(data => ({
...data,
...newData
}));
setChanged(setHasChanged);
}
async function submit() {

View file

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

View file

@ -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)
};

View file

@ -0,0 +1,12 @@
import createSvgIcon from "@material-ui/icons/utils/createSvgIcon";
import React from "react";
const CheckboxSemiChecked = createSvgIcon(
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="5" y="5" width="14" height="14" stroke="#06847B" />
<rect x="8" y="11" width="8" height="2" fill="#06847B" />
</svg>,
"CheckboxSemiChecked"
);
export default CheckboxSemiChecked;

25
src/icons/ChevronDown.tsx Normal file
View file

@ -0,0 +1,25 @@
import useTheme from "@saleor/hooks/useTheme";
import React from "react";
const ChevronDown: React.FC = () => {
const { isDark } = useTheme();
return (
<svg
width="10"
height="7"
viewBox="0 0 10 7"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M1 1L5 5L9 1"
stroke={isDark ? "#FAFAFA" : "#28234A"}
stroke-opacity="0.4"
stroke-width="2"
/>
</svg>
);
};
export default ChevronDown;

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