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:
parent
07bc522af1
commit
8fe66a3bde
170 changed files with 14601 additions and 4469 deletions
|
@ -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{
|
||||
|
|
|
@ -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:{
|
||||
|
|
|
@ -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']",
|
||||
|
|
|
@ -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']"
|
||||
};
|
||||
|
|
|
@ -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,10 +107,14 @@ describe("Shipping methods", () => {
|
|||
}) => {
|
||||
shippingZone = shippingZoneResp;
|
||||
shippingMethod = shippingMethodResp;
|
||||
addChannelToShippingMethod(
|
||||
shippingMethod.id,
|
||||
createdChannel.id,
|
||||
createdChannelPrice
|
||||
addChannelToShippingZone(shippingZone.id, createdChannel.id).then(
|
||||
() => {
|
||||
addChannelToShippingMethod(
|
||||
shippingMethod.id,
|
||||
createdChannel.id,
|
||||
createdChannelPrice
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
)
|
||||
|
@ -143,7 +150,12 @@ describe("Shipping methods", () => {
|
|||
it("should create price based shipping method", () => {
|
||||
const shippingName = `${startsWith}${faker.random.number()}`;
|
||||
|
||||
createShippingZone(shippingName, warehouse.name, plAddress.countryFullName);
|
||||
createShippingZone(
|
||||
shippingName,
|
||||
warehouse.name,
|
||||
plAddress.countryFullName,
|
||||
defaultChannel.name
|
||||
);
|
||||
createShippingRate(shippingName, price, rateOptions.PRICE_OPTION);
|
||||
|
||||
createCheckout({
|
||||
|
@ -164,7 +176,12 @@ describe("Shipping methods", () => {
|
|||
it("should create weight based shipping method", () => {
|
||||
const shippingName = `${startsWith}${faker.random.number()}`;
|
||||
|
||||
createShippingZone(shippingName, warehouse.name, plAddress.countryFullName);
|
||||
createShippingZone(
|
||||
shippingName,
|
||||
warehouse.name,
|
||||
plAddress.countryFullName,
|
||||
defaultChannel.name
|
||||
);
|
||||
createShippingRate(shippingName, price, rateOptions.WEIGHT_OPTION);
|
||||
createCheckout({
|
||||
channelSlug: defaultChannel.slug,
|
||||
|
|
|
@ -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 |
|
@ -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");
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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 }) {
|
||||
|
|
BIN
cypress/videos/channels.js.mp4
Normal file
BIN
cypress/videos/channels.js.mp4
Normal file
Binary file not shown.
BIN
cypress/videos/collections.js.mp4
Normal file
BIN
cypress/videos/collections.js.mp4
Normal file
Binary file not shown.
|
@ -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}"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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;
|
|
@ -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} />);
|
|
@ -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;
|
|
@ -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 />);
|
|
@ -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;
|
|
@ -8,6 +8,8 @@ import ChannelForm, { ChannelFormProps } from "./ChannelForm";
|
|||
const props: ChannelFormProps = {
|
||||
data: {
|
||||
currencyCode: "euro",
|
||||
shippingZonesIdsToAdd: [],
|
||||
shippingZonesIdsToRemove: [],
|
||||
name: "Test",
|
||||
slug: "test"
|
||||
},
|
||||
|
|
|
@ -26,6 +26,8 @@ export interface FormData {
|
|||
name: string;
|
||||
currencyCode: string;
|
||||
slug: string;
|
||||
shippingZonesIdsToAdd: string[];
|
||||
shippingZonesIdsToRemove: string[];
|
||||
}
|
||||
|
||||
export interface ChannelFormProps {
|
||||
|
|
11
src/channels/components/ChannelsAvailabilityDialog/utils.ts
Normal file
11
src/channels/components/ChannelsAvailabilityDialog/utils.ts
Normal 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 };
|
||||
};
|
|
@ -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} />);
|
|
@ -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;
|
|
@ -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;
|
|
@ -0,0 +1,2 @@
|
|||
export * from "./ChannelsWithVariantsAvailabilityDialog";
|
||||
export { default } from "./ChannelsWithVariantsAvailabilityDialog";
|
|
@ -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;
|
|
@ -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[]}
|
||||
/>
|
||||
));
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
2
src/channels/components/ShippingZonesCard/index.tsx
Normal file
2
src/channels/components/ShippingZonesCard/index.tsx
Normal file
|
@ -0,0 +1,2 @@
|
|||
export * from "./ShippingZonesCard";
|
||||
export { default } from "./ShippingZonesCard";
|
12
src/channels/components/ShippingZonesCard/types.ts
Normal file
12
src/channels/components/ShippingZonesCard/types.ts
Normal 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[];
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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>
|
||||
<ChannelStatus
|
||||
isActive={channel?.isActive}
|
||||
disabled={disabledStatus}
|
||||
updateChannelStatus={updateChannelStatus}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
{!!updateChannelStatus && (
|
||||
<>
|
||||
<ChannelStatus
|
||||
isActive={channel?.isActive}
|
||||
disabled={disabledStatus}
|
||||
updateChannelStatus={updateChannelStatus}
|
||||
/>
|
||||
<CardSpacer />
|
||||
</>
|
||||
)}
|
||||
<ShippingZonesCard
|
||||
shippingZonesChoices={getFilteredShippingZonesChoices()}
|
||||
shippingZones={shippingZonesToDisplay}
|
||||
addShippingZone={addShippingZone}
|
||||
removeShippingZone={removeShippingZone}
|
||||
searchShippingZones={searchShippingZones}
|
||||
fetchMoreShippingZones={fetchMoreShippingZones}
|
||||
/>
|
||||
</div>
|
||||
</Grid>
|
||||
<SaveButtonBar
|
||||
onCancel={onBack}
|
||||
|
|
7
src/channels/pages/ChannelDetailsPage/utils.ts
Normal file
7
src/channels/pages/ChannelDetailsPage/utils.ts
Normal 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);
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -3,9 +3,15 @@ import { CollectionDetails_collection } from "@saleor/collections/types/Collecti
|
|||
import { SaleDetails_sale } from "@saleor/discounts/types/SaleDetails";
|
||||
import { VoucherDetails_voucher } from "@saleor/discounts/types/VoucherDetails";
|
||||
import { RequireOnlyOne } from "@saleor/misc";
|
||||
import { ProductDetails_product } from "@saleor/products/types/ProductDetails";
|
||||
import {
|
||||
ProductDetails_product,
|
||||
ProductDetails_product_variants
|
||||
} from "@saleor/products/types/ProductDetails";
|
||||
import { ProductVariantDetails_productVariant } from "@saleor/products/types/ProductVariantDetails";
|
||||
import { ShippingZone_shippingZone_shippingMethods_channelListings } from "@saleor/shipping/types/ShippingZone";
|
||||
import {
|
||||
ShippingZone_shippingZone_channels,
|
||||
ShippingZone_shippingZone_shippingMethods_channelListings
|
||||
} from "@saleor/shipping/types/ShippingZone";
|
||||
import { mapNodeToChoice } from "@saleor/utils/maps";
|
||||
import uniqBy from "lodash-es/uniqBy";
|
||||
|
||||
|
@ -16,15 +22,16 @@ export interface Channel {
|
|||
|
||||
export interface ChannelData {
|
||||
id: string;
|
||||
isPublished: boolean;
|
||||
name: string;
|
||||
publicationDate: string | null;
|
||||
currency: string;
|
||||
price: string;
|
||||
costPrice: string;
|
||||
availableForPurchase: string;
|
||||
isAvailableForPurchase: boolean;
|
||||
visibleInListings: boolean;
|
||||
isPublished?: boolean;
|
||||
publicationDate?: string | null;
|
||||
currency?: string;
|
||||
variantsIds?: string[];
|
||||
price?: string;
|
||||
costPrice?: string;
|
||||
availableForPurchase?: string;
|
||||
isAvailableForPurchase?: boolean;
|
||||
visibleInListings?: boolean;
|
||||
}
|
||||
|
||||
export interface ChannelPriceData {
|
||||
|
@ -95,21 +102,13 @@ export const createVariantChannels = (
|
|||
data?: ProductVariantDetails_productVariant
|
||||
): ChannelPriceData[] => {
|
||||
if (data) {
|
||||
const productChannels = data?.product.channelListings.map(listing => ({
|
||||
costPrice: "",
|
||||
currency: listing.channel.currencyCode,
|
||||
id: listing.channel.id,
|
||||
name: listing.channel.name,
|
||||
price: ""
|
||||
}));
|
||||
const variantChannels = data?.channelListings.map(listing => ({
|
||||
return data?.channelListings.map(listing => ({
|
||||
costPrice: listing.costPrice?.amount.toString() || "",
|
||||
currency: listing.channel.currencyCode,
|
||||
id: listing.channel.id,
|
||||
name: listing.channel.name,
|
||||
price: listing.price.amount.toString()
|
||||
price: listing.price?.amount?.toString()
|
||||
}));
|
||||
return uniqBy([...variantChannels, ...productChannels], obj => obj.id);
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
@ -147,6 +146,7 @@ export const createChannelsData = (data?: Channels_channels[]): ChannelData[] =>
|
|||
currency: channel.currencyCode,
|
||||
id: channel.id,
|
||||
isAvailableForPurchase: false,
|
||||
variantsIds: [],
|
||||
isPublished: false,
|
||||
name: channel.name,
|
||||
price: "",
|
||||
|
@ -168,7 +168,7 @@ export const createChannelsDataWithPrice = (
|
|||
};
|
||||
|
||||
export const createShippingChannels = (
|
||||
data?: Channels_channels[]
|
||||
data?: ShippingZone_shippingZone_channels[]
|
||||
): ChannelShippingData[] =>
|
||||
data?.map(channel => ({
|
||||
currency: channel.currencyCode,
|
||||
|
@ -240,29 +240,54 @@ export const createChannelsDataFromSale = (saleData?: SaleDetails_sale) =>
|
|||
export const createChannelsDataFromProduct = (
|
||||
productData?: ProductDetails_product
|
||||
) =>
|
||||
productData?.channelListings?.map(option => {
|
||||
const variantChannel = productData.variants[0]?.channelListings.find(
|
||||
listing => listing.channel.id === option.channel.id
|
||||
);
|
||||
const price = variantChannel?.price;
|
||||
const costPrice = variantChannel?.costPrice;
|
||||
return {
|
||||
availableForPurchase: option?.availableForPurchase,
|
||||
costPrice: costPrice ? costPrice.amount.toString() : "",
|
||||
currency: price ? price.currency : "",
|
||||
id: option.channel.id,
|
||||
isAvailableForPurchase: !!option?.isAvailableForPurchase,
|
||||
isPublished: option.isPublished,
|
||||
name: option.channel.name,
|
||||
price: price ? price.amount.toString() : "",
|
||||
publicationDate: option.publicationDate,
|
||||
visibleInListings: !!option.visibleInListings
|
||||
};
|
||||
}) || [];
|
||||
productData?.channelListings?.map(
|
||||
({
|
||||
channel,
|
||||
availableForPurchase,
|
||||
isAvailableForPurchase,
|
||||
visibleInListings,
|
||||
publicationDate,
|
||||
isPublished
|
||||
}) => {
|
||||
const variantChannel = productData.variants[0]?.channelListings.find(
|
||||
listing => listing.channel.id === channel.id
|
||||
);
|
||||
const price = variantChannel?.price;
|
||||
const costPrice = variantChannel?.costPrice;
|
||||
const variantsIds = extractVariantsIdsForChannel(
|
||||
productData.variants,
|
||||
channel.id
|
||||
);
|
||||
|
||||
return {
|
||||
availableForPurchase,
|
||||
isPublished,
|
||||
publicationDate,
|
||||
variantsIds,
|
||||
costPrice: costPrice?.amount.toString() ?? "",
|
||||
currency: price ? price.currency : "",
|
||||
id: channel.id,
|
||||
isAvailableForPurchase: !!isAvailableForPurchase,
|
||||
name: channel.name,
|
||||
price: price ? price.amount.toString() : "",
|
||||
visibleInListings: !!visibleInListings
|
||||
};
|
||||
}
|
||||
) || [];
|
||||
|
||||
export const extractVariantsIdsForChannel = (
|
||||
productVariants: ProductDetails_product_variants[],
|
||||
channelId: string
|
||||
) =>
|
||||
productVariants
|
||||
?.filter(({ channelListings }) =>
|
||||
channelListings.some(({ channel }) => channel.id === channelId)
|
||||
)
|
||||
.map(({ id }) => id) || [];
|
||||
|
||||
export const createSortedChannelsDataFromProduct = (
|
||||
productData?: ProductDetails_product
|
||||
) =>
|
||||
): ChannelData[] =>
|
||||
createChannelsDataFromProduct(productData).sort((channel, nextChannel) =>
|
||||
channel.name.localeCompare(nextChannel.name)
|
||||
);
|
||||
|
@ -272,7 +297,9 @@ export const createSortedChannelsData = (data?: Channels_channels[]) =>
|
|||
channel.name.localeCompare(nextChannel.name)
|
||||
);
|
||||
|
||||
export const createSortedShippingChannels = (data?: Channels_channels[]) =>
|
||||
export const createSortedShippingChannels = (
|
||||
data?: ShippingZone_shippingZone_channels[]
|
||||
) =>
|
||||
createShippingChannels(data)?.sort((channel, nextChannel) =>
|
||||
channel.name.localeCompare(nextChannel.name)
|
||||
);
|
||||
|
|
|
@ -1,19 +1,22 @@
|
|||
import { FormData } from "@saleor/channels/components/ChannelForm/ChannelForm";
|
||||
import { ChannelCreate } from "@saleor/channels/types/ChannelCreate";
|
||||
import AppHeader from "@saleor/components/AppHeader";
|
||||
import Container from "@saleor/components/Container";
|
||||
import PageHeader from "@saleor/components/PageHeader";
|
||||
import { WindowTitle } from "@saleor/components/WindowTitle";
|
||||
import { DEFAULT_INITIAL_SEARCH_DATA } from "@saleor/config";
|
||||
import { getSearchFetchMoreProps } from "@saleor/hooks/makeTopLevelSearch/utils";
|
||||
import useNavigator from "@saleor/hooks/useNavigator";
|
||||
import useNotifier from "@saleor/hooks/useNotifier";
|
||||
import { commonMessages } from "@saleor/intl";
|
||||
import { getDefaultNotifierSuccessErrorData } from "@saleor/hooks/useNotifier/utils";
|
||||
import { sectionNames } from "@saleor/intl";
|
||||
import useShippingZonesSearch from "@saleor/searches/useShippingZonesSearch";
|
||||
import currencyCodes from "currency-codes";
|
||||
import React from "react";
|
||||
import { useIntl } from "react-intl";
|
||||
|
||||
import { ChannelCreateInput } from "../../../types/globalTypes";
|
||||
import { useChannelCreateMutation } from "../../mutations";
|
||||
import ChannelDetailsPage from "../../pages/ChannelDetailsPage";
|
||||
import { ChannelCreate } from "../../types/ChannelCreate";
|
||||
import { channelPath, channelsListUrl } from "../../urls";
|
||||
|
||||
export const ChannelCreateView = ({}) => {
|
||||
|
@ -23,27 +26,40 @@ export const ChannelCreateView = ({}) => {
|
|||
|
||||
const handleBack = () => navigate(channelsListUrl());
|
||||
|
||||
const onSubmit = (data: ChannelCreate) => {
|
||||
if (!data.channelCreate.errors.length) {
|
||||
notify({
|
||||
status: "success",
|
||||
text: intl.formatMessage(commonMessages.savedChanges)
|
||||
});
|
||||
navigate(channelPath(data.channelCreate.channel.id));
|
||||
}
|
||||
};
|
||||
|
||||
const [createChannel, createChannelOpts] = useChannelCreateMutation({
|
||||
onCompleted: onSubmit
|
||||
onCompleted: ({ channelCreate: { errors, channel } }: ChannelCreate) => {
|
||||
notify(getDefaultNotifierSuccessErrorData(errors, intl));
|
||||
|
||||
if (!errors.length) {
|
||||
navigate(channelPath(channel.id));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const handleSubmit = (data: ChannelCreateInput) =>
|
||||
const handleSubmit = ({
|
||||
shippingZonesIdsToAdd,
|
||||
shippingZonesIdsToRemove,
|
||||
currencyCode,
|
||||
...rest
|
||||
}: FormData) =>
|
||||
createChannel({
|
||||
variables: {
|
||||
input: { ...data, currencyCode: data.currencyCode.toUpperCase() }
|
||||
input: {
|
||||
...rest,
|
||||
currencyCode: currencyCode.toUpperCase(),
|
||||
addShippingZones: shippingZonesIdsToAdd
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const {
|
||||
loadMore: fetchMoreShippingZones,
|
||||
search: searchShippingZones,
|
||||
result: searchShippingZonesResult
|
||||
} = useShippingZonesSearch({
|
||||
variables: DEFAULT_INITIAL_SEARCH_DATA
|
||||
});
|
||||
|
||||
const currencyCodeChoices = currencyCodes.data.map(currencyData => ({
|
||||
label: intl.formatMessage(
|
||||
{
|
||||
|
@ -77,6 +93,12 @@ export const ChannelCreateView = ({}) => {
|
|||
})}
|
||||
/>
|
||||
<ChannelDetailsPage
|
||||
searchShippingZones={searchShippingZones}
|
||||
searchShippingZonesData={searchShippingZonesResult.data}
|
||||
fetchMoreShippingZones={getSearchFetchMoreProps(
|
||||
searchShippingZonesResult,
|
||||
fetchMoreShippingZones
|
||||
)}
|
||||
disabled={createChannelOpts.loading}
|
||||
errors={createChannelOpts?.data?.channelCreate?.errors || []}
|
||||
currencyCodes={currencyCodeChoices}
|
||||
|
|
|
@ -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 } })
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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} />);
|
|
@ -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;
|
|
@ -1,2 +0,0 @@
|
|||
export * from "./AvailabilityCard";
|
||||
export { default } from "./AvailabilityCard";
|
|
@ -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;
|
|
@ -1,2 +0,0 @@
|
|||
export * from "./ChannelsAvailability";
|
||||
export { default } from "./ChannelsAvailability";
|
|
@ -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;
|
|
@ -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;
|
|
@ -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
|
||||
)}
|
||||
/>
|
||||
));
|
|
@ -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;
|
|
@ -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;
|
2
src/components/ChannelsAvailabilityCard/index.ts
Normal file
2
src/components/ChannelsAvailabilityCard/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export * from "./ChannelsAvailabilityCard";
|
||||
export { default } from "./ChannelsAvailabilityCard";
|
85
src/components/ChannelsAvailabilityCard/styles.ts
Normal file
85
src/components/ChannelsAvailabilityCard/styles.ts
Normal 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" }
|
||||
);
|
26
src/components/ChannelsAvailabilityCard/types.tsx
Normal file
26
src/components/ChannelsAvailabilityCard/types.tsx
Normal 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;
|
95
src/components/ChannelsAvailabilityCard/utils.ts
Normal file
95
src/components/ChannelsAvailabilityCard/utils.ts
Normal 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
|
||||
);
|
|
@ -1,2 +0,0 @@
|
|||
export * from "./ChannelsAvailabilityContent";
|
||||
export { default } from "./ChannelsAvailabilityContent";
|
|
@ -1,15 +1,18 @@
|
|||
import { Channel } from "@saleor/channels/utils";
|
||||
import ActionDialog from "@saleor/components/ActionDialog";
|
||||
import { ChannelsAvailabilityContent } from "@saleor/components/ChannelsAvailabilityContent";
|
||||
import { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton";
|
||||
import React from "react";
|
||||
|
||||
import ChannelsAvailabilityDialogChannelsList from "../ChannelsAvailabilityDialogChannelsList";
|
||||
import ChannelsAvailabilityDialogWrapper from "../ChannelsAvailabilityDialogWrapper";
|
||||
import { useChannelsSearch } from "./utils";
|
||||
|
||||
export interface ChannelsAvailabilityDialogProps {
|
||||
isSelected: (option: Channel) => boolean;
|
||||
channels: Channel[];
|
||||
confirmButtonState: ConfirmButtonTransitionState;
|
||||
contentType?: string;
|
||||
disabled: boolean;
|
||||
disabled?: boolean;
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onChange: (option: Channel) => void;
|
||||
|
@ -23,34 +26,49 @@ export const ChannelsAvailabilityDialog: React.FC<ChannelsAvailabilityDialogProp
|
|||
isSelected,
|
||||
channels,
|
||||
confirmButtonState,
|
||||
contentType = "",
|
||||
contentType,
|
||||
disabled,
|
||||
open,
|
||||
onClose,
|
||||
onChange,
|
||||
onConfirm,
|
||||
selected = 0,
|
||||
selected,
|
||||
title,
|
||||
toggleAll
|
||||
}) => (
|
||||
<ActionDialog
|
||||
confirmButtonState={confirmButtonState}
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
onConfirm={onConfirm}
|
||||
title={title}
|
||||
disabled={disabled}
|
||||
>
|
||||
<ChannelsAvailabilityContent
|
||||
channels={channels}
|
||||
}) => {
|
||||
const { query, onQueryChange, filteredChannels } = useChannelsSearch(
|
||||
channels
|
||||
);
|
||||
|
||||
const handleToggleAll = () => toggleAll(channels, selected);
|
||||
|
||||
const hasAllSelected = selected === channels.length;
|
||||
|
||||
return (
|
||||
<ActionDialog
|
||||
confirmButtonState={confirmButtonState}
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
onConfirm={onConfirm}
|
||||
title={title}
|
||||
disabled={disabled}
|
||||
contentType={contentType}
|
||||
isSelected={isSelected}
|
||||
selected={selected}
|
||||
toggleAll={toggleAll}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</ActionDialog>
|
||||
);
|
||||
ChannelsAvailabilityDialog.displayName = "ChannelsAvailabilityDialog";
|
||||
>
|
||||
<ChannelsAvailabilityDialogWrapper
|
||||
hasAnyChannelsToDisplay={!!filteredChannels.length}
|
||||
hasAllSelected={hasAllSelected}
|
||||
query={query}
|
||||
onQueryChange={onQueryChange}
|
||||
toggleAll={handleToggleAll}
|
||||
contentType={contentType}
|
||||
>
|
||||
<ChannelsAvailabilityDialogChannelsList
|
||||
channels={filteredChannels}
|
||||
isChannelSelected={isSelected}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</ChannelsAvailabilityDialogWrapper>
|
||||
</ActionDialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChannelsAvailabilityDialog;
|
||||
|
|
11
src/components/ChannelsAvailabilityDialog/utils.ts
Normal file
11
src/components/ChannelsAvailabilityDialog/utils.ts
Normal 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 };
|
||||
};
|
|
@ -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;
|
|
@ -0,0 +1,2 @@
|
|||
export * from "./ChannelsAvailabilityDialogChannelsList";
|
||||
export { default } from "./ChannelsAvailabilityDialogChannelsList";
|
|
@ -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;
|
|
@ -0,0 +1,2 @@
|
|||
export * from "./ChannelsAvailabilityDialogWrapper";
|
||||
export { default } from "./ChannelsAvailabilityDialogWrapper";
|
|
@ -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}
|
||||
|
|
9
src/components/DeletableItem/DeletableItem.stories.tsx
Normal file
9
src/components/DeletableItem/DeletableItem.stories.tsx
Normal 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} />);
|
35
src/components/DeletableItem/DeletableItem.tsx
Normal file
35
src/components/DeletableItem/DeletableItem.tsx
Normal 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;
|
2
src/components/DeletableItem/index.tsx
Normal file
2
src/components/DeletableItem/index.tsx
Normal file
|
@ -0,0 +1,2 @@
|
|||
export * from "./DeletableItem";
|
||||
export { default } from "./DeletableItem";
|
|
@ -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} />
|
||||
|
|
|
@ -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,27 +163,41 @@ const SingleAutocompleteSelectFieldComponent: React.FC<SingleAutocompleteSelectF
|
|||
ensureProperValues();
|
||||
}
|
||||
|
||||
const TextFieldComponent = nakedInput ? InputBase : TextField;
|
||||
|
||||
const commonInputProps = {
|
||||
...InputProps,
|
||||
...getInputProps({
|
||||
placeholder
|
||||
}),
|
||||
endAdornment: (
|
||||
<div>
|
||||
<ArrowDropdownIcon />
|
||||
</div>
|
||||
),
|
||||
error,
|
||||
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}
|
||||
>
|
||||
<TextField
|
||||
InputProps={{
|
||||
...InputProps,
|
||||
...getInputProps({
|
||||
placeholder
|
||||
}),
|
||||
endAdornment: (
|
||||
<div>
|
||||
<ArrowDropdownIcon />
|
||||
</div>
|
||||
),
|
||||
error,
|
||||
id: undefined,
|
||||
onBlur: handleBlur,
|
||||
onClick: toggleMenu
|
||||
}}
|
||||
<TextFieldComponent
|
||||
{...nakedInputProps}
|
||||
InputProps={commonInputProps}
|
||||
error={error}
|
||||
disabled={disabled}
|
||||
helperText={helperText}
|
||||
|
|
84
src/components/TableCellAvatar/Avatar.tsx
Normal file
84
src/components/TableCellAvatar/Avatar.tsx
Normal 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;
|
|
@ -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;
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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 => ({
|
||||
|
|
|
@ -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 => ({
|
||||
|
|
|
@ -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 => ({
|
||||
|
|
|
@ -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 => ({
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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[];
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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[];
|
||||
}
|
||||
|
|
2
src/hooks/makeTopLevelSearch/index.ts
Normal file
2
src/hooks/makeTopLevelSearch/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export { default } from "./makeTopLevelSearch";
|
||||
export * from "./makeTopLevelSearch";
|
|
@ -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
|
12
src/hooks/makeTopLevelSearch/types.ts
Normal file
12
src/hooks/makeTopLevelSearch/types.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
export interface CommonSearchOpts {
|
||||
onLoadMore?: () => void;
|
||||
loading?: boolean;
|
||||
data?: {
|
||||
search?: {
|
||||
totalCount?: number;
|
||||
pageInfo?: {
|
||||
hasNextPage: boolean;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
17
src/hooks/makeTopLevelSearch/utils.ts
Normal file
17
src/hooks/makeTopLevelSearch/utils.ts
Normal 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) || [];
|
|
@ -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) =>
|
||||
|
|
|
@ -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() {
|
||||
|
|
2
src/hooks/useNotifier/index.ts
Normal file
2
src/hooks/useNotifier/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export { default } from "./useNotifier";
|
||||
export * from "./useNotifier";
|
18
src/hooks/useNotifier/utils.ts
Normal file
18
src/hooks/useNotifier/utils.ts
Normal 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)
|
||||
};
|
12
src/icons/CheckboxSemiChecked.tsx
Normal file
12
src/icons/CheckboxSemiChecked.tsx
Normal 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
25
src/icons/ChevronDown.tsx
Normal 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
Loading…
Reference in a new issue