Add assign warehouse section in channel page (#2127)

* Add assign warehouse section in channel page

* Update data-test-ids on channel page

* Update channel page form details

* Update shipping zones and warehouses cards in chaannel page

* Assigning warehouses by channel in product and variant pages (#2135)

* Assigning warehouses by channel in product and variant pages

* Disable warehouse assignment when no channel on variant page

* Update products stocks section messages
This commit is contained in:
Dawid 2022-07-11 11:43:08 +02:00 committed by GitHub
parent eb58e2c8ed
commit f1ffb5093f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
46 changed files with 4263 additions and 1560 deletions

View file

@ -9,7 +9,10 @@ export const ADD_CHANNEL_FORM_SELECTORS = {
slugValidationMessage: "[data-test-id='slug-text-input-helper-text']",
currencyAutocompleteDropdown:
"[data-test-id='single-autocomplete-select-option'][data-test-type='custom']",
addShippingZoneButton: '[data-test-id="add-shipping-zone-button"]',
addShippingZoneButton: '[data-test-id="shipping-add-button"]',
addWarehouseButton: '[data-test-id="warehouse-add-button"]',
shippingAutocompleteSelect: "[data-test-id='shipping-auto-complete-select']",
countryAutocompleteInput: '[data-test-id="country-select-input"]'
warehouseAutocompleteSelect:
"[data-test-id='warehouse-auto-complete-select']",
countryAutocompleteInput: '[data-test-id="country-select-input"]',
};

File diff suppressed because it is too large Load diff

View file

@ -27,10 +27,6 @@
"context": "order shipping method name",
"string": "Shipping"
},
"+G9l7u": {
"context": "all selected zones card message",
"string": "All available shipping zones have been selected"
},
"+HuipK": {
"context": "variant sku",
"string": "SKU {sku}"
@ -170,6 +166,10 @@
"context": "dialog header",
"string": "Add Tracking Code"
},
"/C//FB": {
"context": "header, allocated product quantity",
"string": "Allocated"
},
"/JENWS": {
"string": "Reduced Tax Rates"
},
@ -224,6 +224,10 @@
"/glQgs": {
"string": "No channels found"
},
"/iijFq": {
"context": "input label",
"string": "Global threshold"
},
"/kWzY1": {
"string": "Are you sure you want to delete this address from users address book?"
},
@ -601,9 +605,6 @@
"context": "payment status",
"string": "Fully paid"
},
"2qJc9y": {
"string": "CANCEL END DATE"
},
"2r4cTE": {
"context": "button",
"string": "Enable Dark Mode"
@ -1126,9 +1127,6 @@
"context": "attribute value deleted",
"string": "Value deleted"
},
"7Ii5ZQ": {
"string": "SETUP END DATE"
},
"7JAAul": {
"context": "product field",
"string": "Export Product Weight"
@ -1206,10 +1204,6 @@
"context": "info text",
"string": "This rate will apply to all orders"
},
"7wkGxW": {
"context": "app has been installed",
"string": "{unitsLeft} units left"
},
"7yKZvp": {
"context": "section header",
"string": "User Status"
@ -1226,10 +1220,6 @@
"context": "navigator placeholder",
"string": "Order Number"
},
"8CbACQ": {
"context": "add shipping zone title",
"string": "Add Shipping Zones"
},
"8EGagh": {
"context": "search box label",
"string": "Filter Countries"
@ -1331,6 +1321,10 @@
"context": "option",
"string": "Create single variant"
},
"9IWg/f": {
"context": "button",
"string": "SETUP END DATE"
},
"9OtpHt": {
"string": "Order line deleted"
},
@ -1462,10 +1456,6 @@
"context": "order refund subtitle",
"string": "Refunded items can't be fulfilled"
},
"ANRRpG": {
"context": "card title",
"string": "Shipping Zones"
},
"AOI4LW": {
"context": "navigator placeholder",
"string": "Search in Catalog"
@ -1710,10 +1700,6 @@
"context": "dialog content",
"string": "{counter,plural,one{Are you sure you want to delete this shipping zone?} other{Are you sure you want to delete {displayQuantity} shipping zones?}}"
},
"CEavJt": {
"context": "section header",
"string": "Unlimited"
},
"CG+awx": {
"context": "dialog content",
"string": "Which address would you like to use as shipping address for selected customer:"
@ -2033,6 +2019,10 @@
"ErNH3D": {
"string": "Define where this attribute should be used in Saleor system"
},
"ErvPaM": {
"context": "header",
"string": "Warehouse Name"
},
"EsZH44": {
"context": "VariantDetailsChannelsAvailabilityCard item subtitle hidden",
"string": "Hidden"
@ -2041,6 +2031,10 @@
"context": "header",
"string": "{pluginName} Details"
},
"EuOXmr": {
"context": "add items title",
"string": "Add {itemsName}"
},
"Ev6SEF": {
"string": "New Password"
},
@ -2428,6 +2422,10 @@
"context": "number of variants",
"string": "Variants ({quantity})"
},
"HYC6cH": {
"context": "input description",
"string": "Threshold that cannot be exceeded even if per channel thresholds are still available"
},
"HYHLsB": {
"context": "product field",
"string": "Export Variant ID"
@ -2585,6 +2583,10 @@
"context": "header",
"string": "Edit Media"
},
"ImTelT": {
"context": "card subtitle",
"string": "Select warehouses that will be used in this channel. You can assign warehouses to multiple channels."
},
"IoCMjg": {
"context": "no collections",
"string": "No collections found"
@ -2748,6 +2750,10 @@
"context": "section header",
"string": "Organize Product"
},
"JkO0jp": {
"context": "input description",
"string": "{unitsLeft} units left"
},
"JnzDrI": {
"context": "discount type",
"string": "Fixed Amount"
@ -2779,10 +2785,6 @@
"context": "select product informations to be exported",
"string": "Information exported:"
},
"JyQEHU": {
"context": "tabel column header",
"string": "Channels"
},
"JyQoES": {
"context": "delete attribute value",
"string": "Are you sure you want to delete \"{name}\" value?"
@ -2847,10 +2849,6 @@
"context": "product available for purchase date",
"string": "will become available on {date}"
},
"KTAg0f": {
"context": "tabel column header",
"string": "Warehouse Name"
},
"KXkdMH": {
"context": "order discount removed title",
"string": "Order discount was removed by"
@ -3202,9 +3200,6 @@
"context": "gift card bulk create success dialog content",
"string": "We have issued all of your requested gift cards. You can download the list of new gift cards using the button below."
},
"NcY4ph": {
"string": "Threshold that cannot be exceeded even if per channel thresholds are still available"
},
"Nfh9QM": {
"context": "checkbox gift cards label description",
"string": "when activated non-shippable gift cards will be automatically set as fulfilled and sent to customer"
@ -3699,9 +3694,6 @@
"context": "use attribute in filtering",
"string": "Use in Filtering"
},
"RJ5QxE": {
"string": "Global threshold"
},
"RLBLPQ": {
"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>"
@ -3833,6 +3825,10 @@
"SKFr04": {
"string": "Attribute not found."
},
"SM+yG0": {
"context": "input label",
"string": "SKU (Stock Keeping Unit)"
},
"SMakqb": {
"context": "customer contact section, header",
"string": "Contact Information"
@ -4208,6 +4204,10 @@
"context": "dialog header",
"string": "Edit Shipping Method"
},
"V1MytH": {
"context": "shipping zones section name",
"string": "Shipping Zones"
},
"V1mqpZ": {
"context": "button",
"string": "Create Permission Group"
@ -4466,6 +4466,10 @@
"context": "gift card bulk create success dialog title",
"string": "Bulk Issue Gift Cards"
},
"Wyl25+": {
"context": "product inventory, checkbox description",
"string": "Active inventory tracking will automatically calculate changes of stock"
},
"WzA5Ll": {
"string": "Cannot remove user from last group"
},
@ -5395,9 +5399,6 @@
"context": "button",
"string": "Choose file"
},
"ekXood": {
"string": "Unlimited"
},
"erC44f": {
"context": "filters error messages dependencies missing",
"string": "Filter requires other filters: {dependencies}"
@ -5550,10 +5551,6 @@
"g/BrOt": {
"string": "Url has invalid format"
},
"g/FRtd": {
"context": "table column header, allocated product quantity",
"string": "Allocated"
},
"g1WQlC": {
"context": "page title",
"string": "Summary"
@ -5612,10 +5609,6 @@
"context": "plugin filters error messages channels",
"string": "No channels selected"
},
"ge/xFX": {
"context": "table column header",
"string": "Quantity"
},
"ghGLbJ": {
"context": "OrderPayment click&collect shipping method",
"string": "click&collect"
@ -5643,10 +5636,6 @@
"context": "page header",
"string": "Create Page"
},
"gtKcPf": {
"context": "title",
"string": "{zonesCount} / {totalCount} shipping zones"
},
"gvOzOl": {
"string": "Page Title"
},
@ -5890,9 +5879,6 @@
"context": "attribute values",
"string": "Values"
},
"jABdx1": {
"string": "Active inventory tracking will automatically calculate changes of stock"
},
"jBu2yj": {
"context": "acre-inch unit",
"string": "acre-inch"
@ -7174,10 +7160,18 @@
"context": "Header row stock label",
"string": "Stock"
},
"taS/08": {
"context": "variant stocks section subtitle",
"string": "Assign this variant to a channel in the product channel manager to define warehouses allocation"
},
"taX/V3": {
"context": "order total amount",
"string": "Total"
},
"tlGXkh": {
"context": "input description",
"string": "Unlimited"
},
"toDL5R": {
"context": "order status",
"string": "Draft"
@ -7247,6 +7241,10 @@
"context": "app has been removed",
"string": "App successfully removed"
},
"uKlrEk": {
"context": "all selected items message",
"string": "All available {itemsName} have been selected"
},
"uMpv1v": {
"string": "Fulfillment successfully cancelled"
},
@ -7364,6 +7362,10 @@
"v3WWK+": {
"string": "Status is invalid"
},
"v9ILn/": {
"context": "button",
"string": "CANCEL END DATE"
},
"vC8vyb": {
"context": "enabled status option label",
"string": "Enabled"
@ -7628,9 +7630,6 @@
"context": "dialog header",
"string": "Assign Variant"
},
"xB7BTp": {
"string": "SKU (Stock Keeping Unit)"
},
"xHj9Qe": {
"context": "gift card settings header",
"string": "Gift Cards Settings"

View file

@ -493,7 +493,7 @@ type App implements Node & ObjectWithMetadata {
"""Description of the data privacy defined for this app."""
dataPrivacy: String @deprecated(reason: "This field will be removed in Saleor 4.0. Use `dataPrivacyUrl` instead.")
"""Url to details about the privacy policy on the app owner page."""
"""URL to details about the privacy policy on the app owner page."""
dataPrivacyUrl: String
"""Homepage of the app."""
@ -502,14 +502,14 @@ type App implements Node & ObjectWithMetadata {
"""Support page for the app."""
supportUrl: String
"""Url to iframe with the configuration for the app."""
"""URL to iframe with the configuration for the app."""
configurationUrl: String @deprecated(reason: "This field will be removed in Saleor 4.0. Use `appUrl` instead.")
"""Url to iframe with the app."""
"""URL to iframe with the app."""
appUrl: String
"""
Url to manifest used during app's installation.
URL to manifest used during app's installation.
Added in Saleor 3.5.
"""
@ -1711,6 +1711,29 @@ input AttributeValueCreateInput {
name: String!
}
type AttributeValueCreated implements Event {
"""Time of the event."""
issuedAt: DateTime
"""Saleor version that triggered the event."""
version: String
"""The user or application that triggered the event."""
issuingPrincipal: IssuingPrincipal
"""The application receiving the webhook."""
recipient: App
"""
The attribute value the event relates to.
Added in Saleor 3.5.
Note: this API is currently in Feature Preview and can be subject to changes at later point.
"""
attributeValue: AttributeValue
}
"""
Deletes a value of an attribute.
@ -1724,6 +1747,29 @@ type AttributeValueDelete {
attributeValue: AttributeValue
}
type AttributeValueDeleted implements Event {
"""Time of the event."""
issuedAt: DateTime
"""Saleor version that triggered the event."""
version: String
"""The user or application that triggered the event."""
issuingPrincipal: IssuingPrincipal
"""The application receiving the webhook."""
recipient: App
"""
The attribute value the event relates to.
Added in Saleor 3.5.
Note: this API is currently in Feature Preview and can be subject to changes at later point.
"""
attributeValue: AttributeValue
}
input AttributeValueFilterInput {
search: String
ids: [ID!]
@ -1871,6 +1917,29 @@ input AttributeValueUpdateInput {
name: String
}
type AttributeValueUpdated implements Event {
"""Time of the event."""
issuedAt: DateTime
"""Saleor version that triggered the event."""
version: String
"""The user or application that triggered the event."""
issuingPrincipal: IssuingPrincipal
"""The application receiving the webhook."""
recipient: App
"""
The attribute value the event relates to.
Added in Saleor 3.5.
Note: this API is currently in Feature Preview and can be subject to changes at later point.
"""
attributeValue: AttributeValue
}
input BulkAttributeValueInput {
"""ID of the selected attribute."""
id: ID
@ -2373,6 +2442,15 @@ type Channel implements Node {
Added in Saleor 3.1.
"""
defaultCountry: CountryDisplay!
"""
List of warehouses assigned to this channel.
Added in Saleor 3.5.
Note: this API is currently in Feature Preview and can be subject to changes at later point.
"""
warehouses: [Warehouse!]!
}
"""
@ -2402,6 +2480,18 @@ input ChannelCreateInput {
"""isActive flag."""
isActive: Boolean
"""List of shipping zones to assign to the channel."""
addShippingZones: [ID!]
"""
List of warehouses to assign to the channel.
Added in Saleor 3.5.
Note: this API is currently in Feature Preview and can be subject to changes at later point.
"""
addWarehouses: [ID!]
"""Name of the channel."""
name: String!
@ -2415,11 +2505,10 @@ input ChannelCreateInput {
Default country for the channel. Default country can be used in checkout to determine the stock quantities or calculate taxes when the country was not explicitly provided.
Added in Saleor 3.1.
Note: this API is currently in Feature Preview and can be subject to changes at later point.
"""
defaultCountry: CountryCode!
"""List of shipping zones to assign to the channel."""
addShippingZones: [ID!]
}
type ChannelCreated implements Event {
@ -2510,6 +2599,9 @@ type ChannelError {
"""List of shipping zone IDs which causes the error."""
shippingZones: [ID!]
"""List of warehouses IDs which causes the error."""
warehouses: [ID!]
}
"""An enumeration."""
@ -2563,6 +2655,18 @@ input ChannelUpdateInput {
"""isActive flag."""
isActive: Boolean
"""List of shipping zones to assign to the channel."""
addShippingZones: [ID!]
"""
List of warehouses to assign to the channel.
Added in Saleor 3.5.
Note: this API is currently in Feature Preview and can be subject to changes at later point.
"""
addWarehouses: [ID!]
"""Name of the channel."""
name: String
@ -2576,11 +2680,17 @@ input ChannelUpdateInput {
"""
defaultCountry: CountryCode
"""List of shipping zones to assign to the channel."""
addShippingZones: [ID!]
"""List of shipping zones to unassign from the channel."""
removeShippingZones: [ID!]
"""
List of warehouses to unassign from the channel.
Added in Saleor 3.5.
Note: this API is currently in Feature Preview and can be subject to changes at later point.
"""
removeWarehouses: [ID!]
}
type ChannelUpdated implements Event {
@ -2972,8 +3082,54 @@ type CheckoutLanguageCodeUpdate {
}
"""Represents an item in the checkout."""
type CheckoutLine implements Node {
type CheckoutLine implements Node & ObjectWithMetadata {
id: ID!
"""List of private metadata items. Requires staff permissions to access."""
privateMetadata: [MetadataItem!]!
"""
A single key from private metadata. Requires staff permissions to access.
Tip: Use GraphQL aliases to fetch multiple keys.
Added in Saleor 3.3.
Note: this API is currently in Feature Preview and can be subject to changes at later point.
"""
privateMetafield(key: String!): String
"""
Private metadata. Requires staff permissions to access. Use `keys` to control which fields you want to include. The default is to include everything.
Added in Saleor 3.3.
Note: this API is currently in Feature Preview and can be subject to changes at later point.
"""
privateMetafields(keys: [String!]): Metadata
"""List of public metadata items. Can be accessed without permissions."""
metadata: [MetadataItem!]!
"""
A single key from public metadata.
Tip: Use GraphQL aliases to fetch multiple keys.
Added in Saleor 3.3.
Note: this API is currently in Feature Preview and can be subject to changes at later point.
"""
metafield(key: String!): String
"""
Public metadata. Use `keys` to control which fields you want to include. The default is to include everything.
Added in Saleor 3.3.
Note: this API is currently in Feature Preview and can be subject to changes at later point.
"""
metafields(keys: [String!]): Metadata
variant: ProductVariant!
quantity: Int!
@ -7520,7 +7676,7 @@ type Manifest {
permissions: [Permission!]
appUrl: String
"""Url to iframe with the configuration for the app."""
"""URL to iframe with the configuration for the app."""
configurationUrl: String @deprecated(reason: "This field will be removed in Saleor 4.0. Use `appUrl` instead.")
tokenTargetUrl: String
@ -12818,8 +12974,54 @@ type OrderFullyPaid implements Event {
}
"""Represents order line of particular order."""
type OrderLine implements Node {
type OrderLine implements Node & ObjectWithMetadata {
id: ID!
"""List of private metadata items. Requires staff permissions to access."""
privateMetadata: [MetadataItem!]!
"""
A single key from private metadata. Requires staff permissions to access.
Tip: Use GraphQL aliases to fetch multiple keys.
Added in Saleor 3.3.
Note: this API is currently in Feature Preview and can be subject to changes at later point.
"""
privateMetafield(key: String!): String
"""
Private metadata. Requires staff permissions to access. Use `keys` to control which fields you want to include. The default is to include everything.
Added in Saleor 3.3.
Note: this API is currently in Feature Preview and can be subject to changes at later point.
"""
privateMetafields(keys: [String!]): Metadata
"""List of public metadata items. Can be accessed without permissions."""
metadata: [MetadataItem!]!
"""
A single key from public metadata.
Tip: Use GraphQL aliases to fetch multiple keys.
Added in Saleor 3.3.
Note: this API is currently in Feature Preview and can be subject to changes at later point.
"""
metafield(key: String!): String
"""
Public metadata. Use `keys` to control which fields you want to include. The default is to include everything.
Added in Saleor 3.3.
Note: this API is currently in Feature Preview and can be subject to changes at later point.
"""
metafields(keys: [String!]): Metadata
productName: String!
variantName: String!
productSku: String
@ -21946,6 +22148,7 @@ input WarehouseFilterInput {
search: String
ids: [ID!]
isPrivate: Boolean
channels: [ID!]
}
"""
@ -22247,6 +22450,15 @@ enum WebhookEventTypeAsyncEnum {
"""An attribute is deleted."""
ATTRIBUTE_DELETED
"""A new attribute value is created."""
ATTRIBUTE_VALUE_CREATED
"""An attribute value is updated."""
ATTRIBUTE_VALUE_UPDATED
"""An attribute value is deleted."""
ATTRIBUTE_VALUE_DELETED
"""A new category created."""
CATEGORY_CREATED
@ -22341,6 +22553,9 @@ enum WebhookEventTypeAsyncEnum {
"""A customer account is updated."""
CUSTOMER_UPDATED
"""A customer account is deleted."""
CUSTOMER_DELETED
"""A new collection is created."""
COLLECTION_CREATED
@ -22488,6 +22703,15 @@ enum WebhookEventTypeEnum {
"""An attribute is deleted."""
ATTRIBUTE_DELETED
"""A new attribute value is created."""
ATTRIBUTE_VALUE_CREATED
"""An attribute value is updated."""
ATTRIBUTE_VALUE_UPDATED
"""An attribute value is deleted."""
ATTRIBUTE_VALUE_DELETED
"""A new category created."""
CATEGORY_CREATED
@ -22582,6 +22806,9 @@ enum WebhookEventTypeEnum {
"""A customer account is updated."""
CUSTOMER_UPDATED
"""A customer account is deleted."""
CUSTOMER_DELETED
"""A new collection is created."""
COLLECTION_CREATED
@ -22730,6 +22957,9 @@ enum WebhookSampleEventTypeEnum {
ATTRIBUTE_CREATED
ATTRIBUTE_UPDATED
ATTRIBUTE_DELETED
ATTRIBUTE_VALUE_CREATED
ATTRIBUTE_VALUE_UPDATED
ATTRIBUTE_VALUE_DELETED
CATEGORY_CREATED
CATEGORY_UPDATED
CATEGORY_DELETED
@ -22764,6 +22994,7 @@ enum WebhookSampleEventTypeEnum {
INVOICE_SENT
CUSTOMER_CREATED
CUSTOMER_UPDATED
CUSTOMER_DELETED
COLLECTION_CREATED
COLLECTION_UPDATED
COLLECTION_DELETED

View file

@ -0,0 +1,60 @@
import { Accordion, Divider, Typography } from "@material-ui/core";
import React from "react";
import { defineMessages, useIntl } from "react-intl";
import AssignmentListFooter from "./AssignmentListFooter";
import AssignmentListHeader from "./AssignmentListHeader";
import Item from "./Item";
import { useExpanderStyles, useStyles } from "./styles";
import { AssignmentListProps } from "./types";
const messages = defineMessages({
allSelectedMessage: {
id: "uKlrEk",
defaultMessage: "All available {itemsName} have been selected",
description: "all selected items message",
},
});
const AssignmentList: React.FC<AssignmentListProps> = props => {
const {
items,
itemsName,
fetchMoreItems: { totalCount },
removeItem,
} = props;
const intl = useIntl();
const classes = useStyles();
const expanderClasses = useExpanderStyles();
const hasMoreItemsToBeSelected = totalCount !== items.length;
return (
<Accordion classes={expanderClasses}>
<AssignmentListHeader
assignCount={items.length}
totalCount={totalCount}
itemsName={itemsName}
/>
<Divider />
{items.map(item => (
<Item key={item.id} item={item} onDelete={removeItem} />
))}
{hasMoreItemsToBeSelected ? (
<AssignmentListFooter {...props} />
) : (
<Typography
color="textSecondary"
variant="subtitle1"
className={classes.infoMessage}
>
{intl.formatMessage(messages.allSelectedMessage, {
itemsName: itemsName.toLowerCase(),
})}
</Typography>
)}
</Accordion>
);
};
export default AssignmentList;

View file

@ -1,82 +1,87 @@
import { ClickAwayListener } from "@material-ui/core";
import { ChannelShippingZones } from "@saleor/channels/pages/ChannelDetailsPage/types";
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 { defineMessages, useIntl } from "react-intl";
import useStyles from "./styles";
import { ShippingZonesProps } from "./types";
import { useStyles } from "./styles";
import { AssignItem, AssignmentListProps } from "./types";
const messages = defineMessages({
addZoneTitle: {
id: "8CbACQ",
defaultMessage: "Add Shipping Zones",
description: "add shipping zone title",
addItemTitle: {
id: "EuOXmr",
defaultMessage: "Add {itemsName}",
description: "add items title",
},
});
type ShippingZonesCardListFooterProps = ShippingZonesProps;
type AssignmentListFooterProps = AssignmentListProps;
const ShippingZonesCardListFooter: React.FC<ShippingZonesCardListFooterProps> = ({
shippingZonesChoices,
searchShippingZones,
fetchMoreShippingZones,
addShippingZone,
shippingZones,
const AssignmentListFooter: React.FC<AssignmentListFooterProps> = ({
items,
itemsChoices,
itemsName,
inputName,
dataTestId,
addItem,
searchItems,
fetchMoreItems,
}) => {
const intl = useIntl();
const classes = useStyles();
const [isChoicesSelectShown, setIsChoicesSelectShown] = useState(false);
const shippingZonesRef = useRef<ChannelShippingZones>(shippingZones);
const itemsRef = useRef<AssignItem[]>(items);
// 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) {
if (items.length > itemsRef.current.length) {
setIsChoicesSelectShown(true);
}
shippingZonesRef.current = shippingZones;
}, [shippingZones]);
itemsRef.current = items;
}, [items]);
const handleChoice = ({ target }) => {
setIsChoicesSelectShown(false);
addShippingZone(target.value);
addItem(target.value);
};
const handleFooterClickAway = () => {
setIsChoicesSelectShown(false);
searchShippingZones("");
searchItems("");
};
return isChoicesSelectShown ? (
<ClickAwayListener onClickAway={handleFooterClickAway}>
<div className={classes.root}>
<SingleAutocompleteSelectField
data-test-id="shipping-auto-complete-select"
data-test-id={`${dataTestId}-auto-complete-select`}
value=""
displayValue=""
nakedInput
name="shippingZone"
choices={mapNodeToChoice(shippingZonesChoices)}
fetchChoices={searchShippingZones}
name={inputName}
choices={mapNodeToChoice(itemsChoices)}
fetchChoices={searchItems}
onChange={handleChoice}
{...fetchMoreShippingZones}
{...fetchMoreItems}
/>
</div>
</ClickAwayListener>
) : (
<CardAddItemsFooter
onAdd={() => setIsChoicesSelectShown(true)}
title={messages.addZoneTitle}
title={intl.formatMessage(messages.addItemTitle, {
itemsName,
})}
testIds={{
link: "add-shipping-zone-link",
button: "add-shipping-zone-button",
link: `${dataTestId}-add-link`,
button: `${dataTestId}-add-button`,
}}
/>
);
};
export default ShippingZonesCardListFooter;
export default AssignmentListFooter;

View file

@ -0,0 +1,33 @@
import { AccordionSummary, Typography } from "@material-ui/core";
import HorizontalSpacer from "@saleor/apps/components/HorizontalSpacer";
import IconChevronDown from "@saleor/icons/ChevronDown";
import React from "react";
import { useHeaderStyles } from "./styles";
interface AssignmentListHeaderProps {
assignCount: number;
totalCount: number;
itemsName: string;
}
const AssignmentListHeader: React.FC<AssignmentListHeaderProps> = ({
assignCount,
totalCount,
itemsName,
}) => {
const classes = useHeaderStyles();
return (
<div className={classes.container}>
<AccordionSummary expandIcon={<IconChevronDown />} classes={classes}>
<Typography variant="subtitle2" color="textSecondary">
{`${assignCount} / ${totalCount} ${itemsName.toLowerCase()}`}
</Typography>
</AccordionSummary>
<HorizontalSpacer spacing={1.5} />
</div>
);
};
export default AssignmentListHeader;

View file

@ -1,21 +1,18 @@
import { Divider, Typography } from "@material-ui/core";
import { ChannelShippingZone } from "@saleor/channels/pages/ChannelDetailsPage/types";
import DeletableItem from "@saleor/components/DeletableItem";
import React from "react";
import useStyles from "./styles";
import { useStyles } from "./styles";
import { AssignItem } from "./types";
interface ShippingZoneItemProps {
zone: ChannelShippingZone;
interface ItemProps {
item: AssignItem;
onDelete: (id: string) => void;
}
const ShippingZoneItem: React.FC<ShippingZoneItemProps> = ({
zone,
onDelete,
}) => {
const { id, name } = zone;
const classes = useStyles({});
const Item: React.FC<ItemProps> = ({ item, onDelete }) => {
const { id, name } = item;
const classes = useStyles();
return (
<>
@ -28,4 +25,4 @@ const ShippingZoneItem: React.FC<ShippingZoneItemProps> = ({
);
};
export default ShippingZoneItem;
export default Item;

View file

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

View file

@ -0,0 +1,73 @@
import { makeStyles } from "@saleor/macaw-ui";
export const useExpanderStyles = makeStyles(
theme => ({
expanded: {},
root: {
boxShadow: "none",
padding: theme.spacing(1, 4),
"&:before": {
content: "none",
},
"&$expanded": {
margin: 0,
border: "none",
},
},
}),
{ name: "Expander" },
);
export const useHeaderStyles = 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),
padding: 0,
paddingBottom: theme.spacing(2),
minHeight: 0,
"&$expanded": {
minHeight: 0,
},
},
content: {
margin: 0,
"&$expanded": {
margin: 0,
},
},
}),
{ name: "AssignmentListHeader" },
);
export const useStyles = makeStyles(
theme => ({
container: {
padding: theme.spacing(1, 0),
display: "flex",
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
},
root: {
paddingRight: theme.spacing(1),
},
infoMessage: {
padding: theme.spacing(3),
},
}),
{ name: "AssignmentList" },
);

View file

@ -0,0 +1,18 @@
import { FetchMoreProps } from "@saleor/types";
export interface AssignItem {
id: string;
name: string;
}
export interface AssignmentListProps {
items: AssignItem[];
itemsChoices: AssignItem[];
itemsName: string;
fetchMoreItems: FetchMoreProps;
inputName: string;
dataTestId: string;
addItem: (id: string) => void;
removeItem: (id: string) => void;
searchItems: (searchPhrase: string) => void;
}

View file

@ -12,6 +12,8 @@ const props: ChannelFormProps = {
currencyCode: "euro",
shippingZonesIdsToAdd: [],
shippingZonesIdsToRemove: [],
warehousesIdsToAdd: [],
warehousesIdsToRemove: [],
name: "Test",
slug: "test",
defaultCountry: CountryCode.PL,

View file

@ -29,6 +29,8 @@ export interface FormData {
slug: string;
shippingZonesIdsToAdd: string[];
shippingZonesIdsToRemove: string[];
warehousesIdsToAdd: string[];
warehousesIdsToRemove: string[];
defaultCountry: CountryCode;
}

View file

@ -3,7 +3,7 @@ import CommonDecorator from "@saleor/storybook/Decorator";
import { storiesOf } from "@storybook/react";
import React from "react";
import ShippingZonesCard from "./ShippingZonesCard";
import ShippingZones from "./ShippingZones";
const shippingZones = [
{
@ -32,11 +32,11 @@ const baseProps = {
shippingZonesChoices: shippingZones as ChannelShippingZones,
};
storiesOf("Shipping zones card", module)
storiesOf("Shipping zones", module)
.addDecorator(CommonDecorator)
.add("with no options selected", () => <ShippingZonesCard {...baseProps} />)
.add("with no options selected", () => <ShippingZones {...baseProps} />)
.add("with options selected", () => (
<ShippingZonesCard
<ShippingZones
{...baseProps}
shippingZones={shippingZones as ChannelShippingZones}
/>

View file

@ -0,0 +1,63 @@
import { Card, CardContent, Typography } from "@material-ui/core";
import { ChannelShippingZones } from "@saleor/channels/pages/ChannelDetailsPage/types";
import CardTitle from "@saleor/components/CardTitle";
import { SearchShippingZonesQuery } from "@saleor/graphql";
import { sectionNames } from "@saleor/intl";
import { FetchMoreProps, RelayToFlat } from "@saleor/types";
import React from "react";
import { defineMessages, useIntl } from "react-intl";
import AssignmentList from "../AssignmentList";
const messages = defineMessages({
subtitle: {
id: "Ic7Wln",
defaultMessage:
"Select shipping zones that will be supplied via this channel. You can assign shipping zones to multiple channels.",
description: "card subtitle",
},
});
interface ShippingZonesProps {
addShippingZone: (id: string) => void;
removeShippingZone: (id: string) => void;
searchShippingZones: (searchPhrase: string) => void;
fetchMoreShippingZones: FetchMoreProps;
shippingZones: ChannelShippingZones;
shippingZonesChoices: RelayToFlat<SearchShippingZonesQuery["search"]>;
}
const ShippingZones: React.FC<ShippingZonesProps> = props => {
const {
addShippingZone,
removeShippingZone,
searchShippingZones,
fetchMoreShippingZones,
shippingZones,
shippingZonesChoices,
} = props;
const intl = useIntl();
return (
<Card>
<CardTitle title={intl.formatMessage(sectionNames.shippingZones)} />
<CardContent>
<Typography>{intl.formatMessage(messages.subtitle)}</Typography>
</CardContent>
<AssignmentList
items={shippingZones}
itemsChoices={shippingZonesChoices}
addItem={addShippingZone}
removeItem={removeShippingZone}
searchItems={searchShippingZones}
fetchMoreItems={fetchMoreShippingZones}
dataTestId="shipping"
inputName="shippingZone"
itemsName={intl.formatMessage(sectionNames.shippingZones)}
/>
</Card>
);
};
export default ShippingZones;

View file

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

View file

@ -1,25 +1,5 @@
import { makeStyles } from "@saleor/macaw-ui";
export const useExpanderStyles = makeStyles(
theme => ({
expanded: {},
root: {
boxShadow: "none",
padding: theme.spacing(1, 4),
"&:before": {
content: "none",
},
"&$expanded": {
margin: 0,
border: "none",
},
},
}),
{ name: "Expander" },
);
const useStyles = makeStyles(
theme => ({
container: {
@ -33,7 +13,7 @@ const useStyles = makeStyles(
paddingRight: theme.spacing(1),
},
}),
{ name: "ShippingZonesCard" },
{ name: "ShippingZones" },
);
export default useStyles;

View file

@ -1,93 +0,0 @@
import {
Accordion,
Card,
CardContent,
Divider,
makeStyles,
Typography,
} from "@material-ui/core";
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 { useExpanderStyles } from "./styles";
import { ShippingZonesProps } from "./types";
const messages = defineMessages({
title: {
id: "ANRRpG",
defaultMessage: "Shipping Zones",
description: "card title",
},
subtitle: {
id: "Ic7Wln",
defaultMessage:
"Select shipping zones that will be supplied via this channel. You can assign shipping zones to multiple channels.",
description: "card subtitle",
},
allSelectedMessage: {
id: "+G9l7u",
defaultMessage: "All available shipping zones have been selected",
description: "all selected zones card message",
},
});
const useStyles = makeStyles(
theme => ({
infoMessage: {
padding: theme.spacing(3),
},
}),
{ name: "ShippingZonesCard" },
);
type ShippingZonesCardProps = ShippingZonesProps;
const ShippingZonesCard: React.FC<ShippingZonesCardProps> = props => {
const {
shippingZones,
removeShippingZone,
fetchMoreShippingZones: { totalCount },
} = props;
const expanderClasses = useExpanderStyles({});
const classes = useStyles();
const intl = useIntl();
const hasMoreZonesToBeSelected = totalCount !== shippingZones.length;
return (
<Card>
<CardTitle title={intl.formatMessage(messages.title)} />
<CardContent>
<Typography>{intl.formatMessage(messages.subtitle)}</Typography>
</CardContent>
<Accordion classes={expanderClasses}>
<ShippingZonesListHeader
shippingZones={shippingZones}
totalCount={totalCount}
/>
<Divider />
{shippingZones.map(zone => (
<ShippingZoneItem zone={zone} onDelete={removeShippingZone} />
))}
{hasMoreZonesToBeSelected ? (
<ShippingZonesCardListFooter {...props} />
) : (
<Typography
color="textSecondary"
variant="subtitle1"
className={classes.infoMessage}
>
{intl.formatMessage(messages.allSelectedMessage)}
</Typography>
)}
</Accordion>
</Card>
);
};
export default ShippingZonesCard;

View file

@ -1,77 +0,0 @@
import { AccordionSummary, Typography } from "@material-ui/core";
import HorizontalSpacer from "@saleor/apps/components/HorizontalSpacer";
import { ChannelShippingZones } from "@saleor/channels/pages/ChannelDetailsPage/types";
import IconChevronDown from "@saleor/icons/ChevronDown";
import { makeStyles } from "@saleor/macaw-ui";
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),
padding: 0,
paddingBottom: theme.spacing(2),
minHeight: 0,
"&$expanded": {
minHeight: 0,
},
},
content: {
margin: 0,
"&$expanded": {
margin: 0,
},
},
}),
{ name: "ShippingZonesListHeader" },
);
const messages = defineMessages({
title: {
id: "gtKcPf",
defaultMessage: "{zonesCount} / {totalCount} shipping zones",
description: "title",
},
});
interface ShippingZonesListHeaderProps {
shippingZones: ChannelShippingZones;
totalCount: number;
}
const ShippingZonesListHeader: React.FC<ShippingZonesListHeaderProps> = ({
shippingZones,
totalCount,
}) => {
const classes = useStyles({});
const intl = useIntl();
return (
<div className={classes.container}>
<AccordionSummary expandIcon={<IconChevronDown />} classes={classes}>
<Typography variant="subtitle2" color="textSecondary">
{intl.formatMessage(messages.title, {
zonesCount: shippingZones.length,
totalCount,
})}
</Typography>
</AccordionSummary>
<HorizontalSpacer spacing={1.5} />
</div>
);
};
export default ShippingZonesListHeader;

View file

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

View file

@ -1,12 +0,0 @@
import { ChannelShippingZones } from "@saleor/channels/pages/ChannelDetailsPage/types";
import { SearchShippingZonesQuery } from "@saleor/graphql";
import { FetchMoreProps, RelayToFlat } from "@saleor/types";
export interface ShippingZonesProps {
addShippingZone: (id: string) => void;
removeShippingZone: (id: string) => void;
searchShippingZones: (searchPhrase: string) => void;
fetchMoreShippingZones: FetchMoreProps;
shippingZones: ChannelShippingZones;
shippingZonesChoices: RelayToFlat<SearchShippingZonesQuery["search"]>;
}

View file

@ -0,0 +1,40 @@
import { ChannelWarehouses } from "@saleor/channels/pages/ChannelDetailsPage/types";
import CommonDecorator from "@saleor/storybook/Decorator";
import { storiesOf } from "@storybook/react";
import React from "react";
import Warehouses from "./Warehouses";
const warehouses = [
{
__typename: "Warehouse",
id: "2",
name: "Fancy warehouse",
},
{
__typename: "Warehouse",
id: "3",
name: "Nice warehouse",
},
];
const baseProps = {
addWarehouse: () => undefined,
removeWarehouse: () => undefined,
searchWarehouses: () => undefined,
fetchMoreWarehouses: {
loading: false,
hasMore: false,
onFetchMore: () => undefined,
totalCount: 0,
},
warehouses: [],
warehousesChoices: warehouses as ChannelWarehouses,
};
storiesOf("Warehouses", module)
.addDecorator(CommonDecorator)
.add("with no options selected", () => <Warehouses {...baseProps} />)
.add("with options selected", () => (
<Warehouses {...baseProps} warehouses={warehouses as ChannelWarehouses} />
));

View file

@ -0,0 +1,62 @@
import { Card, CardContent, Typography } from "@material-ui/core";
import { ChannelWarehouses } from "@saleor/channels/pages/ChannelDetailsPage/types";
import CardTitle from "@saleor/components/CardTitle";
import { SearchWarehousesQuery } from "@saleor/graphql";
import { sectionNames } from "@saleor/intl";
import { FetchMoreProps, RelayToFlat } from "@saleor/types";
import React from "react";
import { defineMessages, useIntl } from "react-intl";
import AssignmentList from "../AssignmentList";
const messages = defineMessages({
subtitle: {
id: "ImTelT",
defaultMessage:
"Select warehouses that will be used in this channel. You can assign warehouses to multiple channels.",
description: "card subtitle",
},
});
interface WarehousesProps {
addWarehouse: (id: string) => void;
removeWarehouse: (id: string) => void;
searchWarehouses: (searchPhrase: string) => void;
fetchMoreWarehouses: FetchMoreProps;
warehouses: ChannelWarehouses;
warehousesChoices: RelayToFlat<SearchWarehousesQuery["search"]>;
}
const Warehouses: React.FC<WarehousesProps> = props => {
const {
addWarehouse,
removeWarehouse,
searchWarehouses,
fetchMoreWarehouses,
warehouses,
warehousesChoices,
} = props;
const intl = useIntl();
return (
<Card>
<CardTitle title={intl.formatMessage(sectionNames.warehouses)} />
<CardContent>
<Typography>{intl.formatMessage(messages.subtitle)}</Typography>
</CardContent>
<AssignmentList
items={warehouses}
itemsChoices={warehousesChoices}
addItem={addWarehouse}
removeItem={removeWarehouse}
searchItems={searchWarehouses}
fetchMoreItems={fetchMoreWarehouses}
dataTestId="warehouse"
inputName="warehouse"
itemsName={intl.formatMessage(sectionNames.warehouses)}
/>
</Card>
);
};
export default Warehouses;

View file

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

View file

@ -0,0 +1,19 @@
import { makeStyles } from "@saleor/macaw-ui";
const useStyles = makeStyles(
theme => ({
container: {
padding: theme.spacing(1, 0),
display: "flex",
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
},
root: {
paddingRight: theme.spacing(1),
},
}),
{ name: "Warehouses" },
);
export default useStyles;

View file

@ -22,6 +22,8 @@ const props: ChannelDetailsPageProps<ChannelErrorFragment[]> = {
updateChannelStatus: () => undefined,
searchShippingZones: () => undefined,
searchShippingZonesData: undefined,
searchWarehouses: () => undefined,
searchWarehousesData: undefined,
countries: countries.map(({ name, code }) => ({
code,
country: name,
@ -45,6 +47,24 @@ const props: ChannelDetailsPageProps<ChannelErrorFragment[]> = {
onFetchMore: () => undefined,
totalCount: 0,
},
channelWarehouses: [
{
__typename: "Warehouse",
id: "warehouse-1",
name: "Warehouse 1",
},
{
__typename: "Warehouse",
id: "warehouse-2",
name: "Warehouse 2",
},
],
fetchMoreWarehouses: {
loading: false,
hasMore: false,
onFetchMore: () => undefined,
totalCount: 0,
},
};
storiesOf("Views / Channels / Channel details", module)

View file

@ -1,4 +1,5 @@
import ShippingZonesCard from "@saleor/channels/components/ShippingZonesCard/ShippingZonesCard";
import ShippingZones from "@saleor/channels/components/ShippingZones";
import Warehouses from "@saleor/channels/components/Warehouses";
import { channelsListUrl } from "@saleor/channels/urls";
import CardSpacer from "@saleor/components/CardSpacer";
import Form from "@saleor/components/Form";
@ -11,6 +12,7 @@ import {
CountryCode,
CountryFragment,
SearchShippingZonesQuery,
SearchWarehousesQuery,
} from "@saleor/graphql";
import { SearchData } from "@saleor/hooks/makeTopLevelSearch";
import { getParsedSearchData } from "@saleor/hooks/makeTopLevelSearch/utils";
@ -29,7 +31,7 @@ import React, { useState } from "react";
import { ChannelForm, FormData } from "../../components/ChannelForm";
import { ChannelStatus } from "../../components/ChannelStatus/ChannelStatus";
import { ChannelShippingZones } from "./types";
import { ChannelShippingZones, ChannelWarehouses } from "./types";
import { getUpdatedIdsWithNewId, getUpdatedIdsWithoutNewId } from "./utils";
export interface ChannelDetailsPageProps<TErrors> {
@ -42,11 +44,15 @@ export interface ChannelDetailsPageProps<TErrors> {
searchShippingZonesData?: SearchData;
fetchMoreShippingZones: FetchMoreProps;
channelShippingZones?: ChannelShippingZones;
searchWarehousesData?: SearchData;
fetchMoreWarehouses: FetchMoreProps;
channelWarehouses?: ChannelWarehouses;
countries: CountryFragment[];
onDelete?: () => void;
onSubmit: (data: FormData) => SubmitPromise<TErrors[]>;
updateChannelStatus?: () => void;
searchShippingZones: (query: string) => void;
searchWarehouses: (query: string) => void;
}
const ChannelDetailsPage = function<TErrors>({
@ -62,8 +68,12 @@ const ChannelDetailsPage = function<TErrors>({
searchShippingZones,
searchShippingZonesData,
fetchMoreShippingZones,
countries,
channelShippingZones = [],
searchWarehouses,
searchWarehousesData,
fetchMoreWarehouses,
channelWarehouses = [],
countries,
}: ChannelDetailsPageProps<TErrors>) {
const navigate = useNavigator();
@ -76,6 +86,9 @@ const ChannelDetailsPage = function<TErrors>({
const [shippingZonesToDisplay, setShippingZonesToDisplay] = useStateFromProps<
ChannelShippingZones
>(channelShippingZones);
const [warehousesToDisplay, setWarehousesToDisplay] = useStateFromProps<
ChannelWarehouses
>(channelWarehouses);
const countryChoices = mapCountriesToChoices(countries || []);
@ -86,6 +99,8 @@ const ChannelDetailsPage = function<TErrors>({
slug: "",
shippingZonesIdsToAdd: [],
shippingZonesIdsToRemove: [],
warehousesIdsToAdd: [],
warehousesIdsToRemove: [],
defaultCountry: (defaultCountry?.code || "") as CountryCode,
...formData,
};
@ -96,6 +111,12 @@ const ChannelDetailsPage = function<TErrors>({
!shippingZonesToDisplay.some(({ id }) => id === searchedZoneId),
);
const getFilteredWarehousesChoices = (): RelayToFlat<SearchWarehousesQuery["search"]> =>
getParsedSearchData({ data: searchWarehousesData }).filter(
({ id: searchedWarehouseId }) =>
!warehousesToDisplay.some(({ id }) => id === searchedWarehouseId),
);
const checkIfSaveIsDisabled = (data: FormData) => {
const isValid =
!!data.name &&
@ -114,7 +135,7 @@ const ChannelDetailsPage = function<TErrors>({
initial={initialData}
checkIfSaveIsDisabled={checkIfSaveIsDisabled}
>
{({ change, data, submit, set, isSaveDisabled }) => {
{({ change, data, submit, set, isSaveDisabled, triggerChange }) => {
const handleCurrencyCodeSelect = createSingleAutocompleteSelectHandler(
change,
setSelectedCurrencyCode,
@ -127,6 +148,8 @@ const ChannelDetailsPage = function<TErrors>({
);
const addShippingZone = (zoneId: string) => {
triggerChange();
set({
...data,
shippingZonesIdsToRemove: getUpdatedIdsWithoutNewId(
@ -148,6 +171,8 @@ const ChannelDetailsPage = function<TErrors>({
};
const removeShippingZone = (zoneId: string) => {
triggerChange();
set({
...data,
shippingZonesIdsToAdd: getUpdatedIdsWithoutNewId(
@ -165,6 +190,49 @@ const ChannelDetailsPage = function<TErrors>({
);
};
const addWarehouse = (warehouseId: string) => {
triggerChange();
set({
...data,
warehousesIdsToRemove: getUpdatedIdsWithoutNewId(
data.warehousesIdsToRemove,
warehouseId,
),
warehousesIdsToAdd: getUpdatedIdsWithNewId(
data.warehousesIdsToAdd,
warehouseId,
),
});
setWarehousesToDisplay([
...warehousesToDisplay,
getParsedSearchData({ data: searchWarehousesData }).find(
getById(warehouseId),
),
]);
};
const removeWarehouse = (warehouseId: string) => {
triggerChange();
set({
...data,
warehousesIdsToAdd: getUpdatedIdsWithoutNewId(
data.warehousesIdsToAdd,
warehouseId,
),
warehousesIdsToRemove: getUpdatedIdsWithNewId(
data.warehousesIdsToRemove,
warehouseId,
),
});
setWarehousesToDisplay(
warehousesToDisplay.filter(getByUnmatchingId(warehouseId)),
);
};
return (
<>
<Grid>
@ -193,7 +261,7 @@ const ChannelDetailsPage = function<TErrors>({
<CardSpacer />
</>
)}
<ShippingZonesCard
<ShippingZones
shippingZonesChoices={getFilteredShippingZonesChoices()}
shippingZones={shippingZonesToDisplay}
addShippingZone={addShippingZone}
@ -201,6 +269,15 @@ const ChannelDetailsPage = function<TErrors>({
searchShippingZones={searchShippingZones}
fetchMoreShippingZones={fetchMoreShippingZones}
/>
<CardSpacer />
<Warehouses
warehousesChoices={getFilteredWarehousesChoices()}
warehouses={warehousesToDisplay}
addWarehouse={addWarehouse}
removeWarehouse={removeWarehouse}
searchWarehouses={searchWarehouses}
fetchMoreWarehouses={fetchMoreWarehouses}
/>
</div>
</Grid>
<Savebar

View file

@ -1,4 +1,7 @@
import { ChannelShippingZonesQuery } from "@saleor/graphql";
import {
ChannelShippingZonesQuery,
ChannelWarehousesQuery,
} from "@saleor/graphql";
import { RelayToFlat } from "@saleor/types";
export type ChannelShippingZones = RelayToFlat<
@ -6,3 +9,9 @@ export type ChannelShippingZones = RelayToFlat<
>;
export type ChannelShippingZone = ChannelShippingZones[0];
export type ChannelWarehouses = RelayToFlat<
ChannelWarehousesQuery["warehouses"]
>;
export type ChannelWarehouse = ChannelWarehouses[0];

View file

@ -16,6 +16,7 @@ import useShop from "@saleor/hooks/useShop";
import { sectionNames } from "@saleor/intl";
import { extractMutationErrors } from "@saleor/misc";
import useShippingZonesSearch from "@saleor/searches/useShippingZonesSearch";
import useWarehouseSearch from "@saleor/searches/useWarehouseSearch";
import currencyCodes from "currency-codes";
import React from "react";
import { useIntl } from "react-intl";
@ -44,6 +45,8 @@ export const ChannelCreateView = ({}) => {
const handleSubmit = ({
shippingZonesIdsToAdd,
shippingZonesIdsToRemove,
warehousesIdsToAdd,
warehousesIdsToRemove,
currencyCode,
...rest
}: FormData) =>
@ -54,6 +57,7 @@ export const ChannelCreateView = ({}) => {
...rest,
currencyCode: currencyCode.toUpperCase(),
addShippingZones: shippingZonesIdsToAdd,
addWarehouses: warehousesIdsToAdd,
},
},
}),
@ -67,6 +71,14 @@ export const ChannelCreateView = ({}) => {
variables: DEFAULT_INITIAL_SEARCH_DATA,
});
const {
loadMore: fetchMoreWarehouses,
search: searchWarehouses,
result: searchWarehousesResult,
} = useWarehouseSearch({
variables: DEFAULT_INITIAL_SEARCH_DATA,
});
const currencyCodeChoices = currencyCodes.data.map(currencyData => ({
label: intl.formatMessage(
{
@ -109,6 +121,12 @@ export const ChannelCreateView = ({}) => {
searchShippingZonesResult,
fetchMoreShippingZones,
)}
searchWarehouses={searchWarehouses}
searchWarehousesData={searchWarehousesResult.data}
fetchMoreWarehouses={getSearchFetchMoreProps(
searchWarehousesResult,
fetchMoreWarehouses,
)}
disabled={createChannelOpts.loading}
errors={createChannelOpts?.data?.channelCreate?.errors || []}
currencyCodes={currencyCodeChoices}

View file

@ -17,6 +17,7 @@ import {
useChannelShippingZonesQuery,
useChannelsQuery,
useChannelUpdateMutation,
useChannelWarehousesQuery,
} from "@saleor/graphql";
import { getSearchFetchMoreProps } from "@saleor/hooks/makeTopLevelSearch/utils";
import useNavigator from "@saleor/hooks/useNavigator";
@ -26,6 +27,7 @@ import useShop from "@saleor/hooks/useShop";
import { sectionNames } from "@saleor/intl";
import { extractMutationErrors } from "@saleor/misc";
import useShippingZonesSearch from "@saleor/searches/useShippingZonesSearch";
import useWarehouseSearch from "@saleor/searches/useWarehouseSearch";
import getChannelsErrorMessage from "@saleor/utils/errors/channels";
import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers";
import React from "react";
@ -103,6 +105,8 @@ export const ChannelDetails: React.FC<ChannelDetailsProps> = ({
slug,
shippingZonesIdsToRemove,
shippingZonesIdsToAdd,
warehousesIdsToRemove,
warehousesIdsToAdd,
defaultCountry,
}: FormData) =>
extractMutationErrors(
@ -115,6 +119,8 @@ export const ChannelDetails: React.FC<ChannelDetailsProps> = ({
defaultCountry,
addShippingZones: shippingZonesIdsToAdd,
removeShippingZones: shippingZonesIdsToRemove,
addWarehouses: warehousesIdsToAdd,
removeWarehouses: warehousesIdsToRemove,
},
},
}),
@ -176,6 +182,25 @@ export const ChannelDetails: React.FC<ChannelDetailsProps> = ({
variables: DEFAULT_INITIAL_SEARCH_DATA,
});
const {
data: channelWarehousesData,
loading: channelsWarehousesLoading,
} = useChannelWarehousesQuery({
variables: {
filter: {
channels: [id],
},
},
});
const {
loadMore: fetchMoreWarehouses,
search: searchWarehouses,
result: searchWarehousesResult,
} = useWarehouseSearch({
variables: DEFAULT_INITIAL_SEARCH_DATA,
});
return (
<>
<WindowTitle
@ -200,9 +225,21 @@ export const ChannelDetails: React.FC<ChannelDetailsProps> = ({
searchShippingZonesResult,
fetchMoreShippingZones,
)}
channelWarehouses={channelWarehousesData?.warehouses?.edges?.map(
({ node }) => node,
)}
searchWarehouses={searchWarehouses}
searchWarehousesData={searchWarehousesResult.data}
fetchMoreWarehouses={getSearchFetchMoreProps(
searchWarehousesResult,
fetchMoreWarehouses,
)}
channel={data?.channel}
disabled={
updateChannelOpts.loading || loading || channelsShippingZonesLoading
updateChannelOpts.loading ||
loading ||
channelsShippingZonesLoading ||
channelsWarehousesLoading
}
disabledStatus={
activateChannelOpts.loading || deactivateChannelOpts.loading

View file

@ -22,6 +22,9 @@
"AttributeCreated",
"AttributeDeleted",
"AttributeUpdated",
"AttributeValueCreated",
"AttributeValueDeleted",
"AttributeValueUpdated",
"CategoryCreated",
"CategoryDeleted",
"CategoryUpdated",
@ -195,6 +198,7 @@
"Attribute",
"Category",
"Checkout",
"CheckoutLine",
"Collection",
"DigitalContent",
"Fulfillment",
@ -203,6 +207,7 @@
"Menu",
"MenuItem",
"Order",
"OrderLine",
"Page",
"PageType",
"Payment",

View file

@ -13495,6 +13495,7 @@ export const SearchWarehousesDocument = gql`
sortBy: {direction: ASC, field: NAME}
filter: {search: $query}
) {
totalCount
edges {
node {
id
@ -16343,6 +16344,46 @@ export function useWarehouseDetailsLazyQuery(baseOptions?: ApolloReactHooks.Lazy
export type WarehouseDetailsQueryHookResult = ReturnType<typeof useWarehouseDetailsQuery>;
export type WarehouseDetailsLazyQueryHookResult = ReturnType<typeof useWarehouseDetailsLazyQuery>;
export type WarehouseDetailsQueryResult = Apollo.QueryResult<Types.WarehouseDetailsQuery, Types.WarehouseDetailsQueryVariables>;
export const ChannelWarehousesDocument = gql`
query ChannelWarehouses($filter: WarehouseFilterInput) {
warehouses(filter: $filter, first: 100) {
edges {
node {
id
name
}
}
}
}
`;
/**
* __useChannelWarehousesQuery__
*
* To run a query within a React component, call `useChannelWarehousesQuery` and pass it any options that fit your needs.
* When your component renders, `useChannelWarehousesQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useChannelWarehousesQuery({
* variables: {
* filter: // value for 'filter'
* },
* });
*/
export function useChannelWarehousesQuery(baseOptions?: ApolloReactHooks.QueryHookOptions<Types.ChannelWarehousesQuery, Types.ChannelWarehousesQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return ApolloReactHooks.useQuery<Types.ChannelWarehousesQuery, Types.ChannelWarehousesQueryVariables>(ChannelWarehousesDocument, options);
}
export function useChannelWarehousesLazyQuery(baseOptions?: ApolloReactHooks.LazyQueryHookOptions<Types.ChannelWarehousesQuery, Types.ChannelWarehousesQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return ApolloReactHooks.useLazyQuery<Types.ChannelWarehousesQuery, Types.ChannelWarehousesQueryVariables>(ChannelWarehousesDocument, options);
}
export type ChannelWarehousesQueryHookResult = ReturnType<typeof useChannelWarehousesQuery>;
export type ChannelWarehousesLazyQueryHookResult = ReturnType<typeof useChannelWarehousesLazyQuery>;
export type ChannelWarehousesQueryResult = Apollo.QueryResult<Types.ChannelWarehousesQuery, Types.ChannelWarehousesQueryVariables>;
export const WebhookCreateDocument = gql`
mutation WebhookCreate($input: WebhookCreateInput!) {
webhookCreate(input: $input) {

View file

@ -518,6 +518,14 @@ export type AttributeValueCreateFieldPolicy = {
errors?: FieldPolicy<any> | FieldReadFunction<any>,
attributeValue?: FieldPolicy<any> | FieldReadFunction<any>
};
export type AttributeValueCreatedKeySpecifier = ('issuedAt' | 'version' | 'issuingPrincipal' | 'recipient' | 'attributeValue' | AttributeValueCreatedKeySpecifier)[];
export type AttributeValueCreatedFieldPolicy = {
issuedAt?: FieldPolicy<any> | FieldReadFunction<any>,
version?: FieldPolicy<any> | FieldReadFunction<any>,
issuingPrincipal?: FieldPolicy<any> | FieldReadFunction<any>,
recipient?: FieldPolicy<any> | FieldReadFunction<any>,
attributeValue?: FieldPolicy<any> | FieldReadFunction<any>
};
export type AttributeValueDeleteKeySpecifier = ('attribute' | 'attributeErrors' | 'errors' | 'attributeValue' | AttributeValueDeleteKeySpecifier)[];
export type AttributeValueDeleteFieldPolicy = {
attribute?: FieldPolicy<any> | FieldReadFunction<any>,
@ -525,6 +533,14 @@ export type AttributeValueDeleteFieldPolicy = {
errors?: FieldPolicy<any> | FieldReadFunction<any>,
attributeValue?: FieldPolicy<any> | FieldReadFunction<any>
};
export type AttributeValueDeletedKeySpecifier = ('issuedAt' | 'version' | 'issuingPrincipal' | 'recipient' | 'attributeValue' | AttributeValueDeletedKeySpecifier)[];
export type AttributeValueDeletedFieldPolicy = {
issuedAt?: FieldPolicy<any> | FieldReadFunction<any>,
version?: FieldPolicy<any> | FieldReadFunction<any>,
issuingPrincipal?: FieldPolicy<any> | FieldReadFunction<any>,
recipient?: FieldPolicy<any> | FieldReadFunction<any>,
attributeValue?: FieldPolicy<any> | FieldReadFunction<any>
};
export type AttributeValueTranslatableContentKeySpecifier = ('id' | 'name' | 'richText' | 'plainText' | 'translation' | 'attributeValue' | AttributeValueTranslatableContentKeySpecifier)[];
export type AttributeValueTranslatableContentFieldPolicy = {
id?: FieldPolicy<any> | FieldReadFunction<any>,
@ -555,6 +571,14 @@ export type AttributeValueUpdateFieldPolicy = {
errors?: FieldPolicy<any> | FieldReadFunction<any>,
attributeValue?: FieldPolicy<any> | FieldReadFunction<any>
};
export type AttributeValueUpdatedKeySpecifier = ('issuedAt' | 'version' | 'issuingPrincipal' | 'recipient' | 'attributeValue' | AttributeValueUpdatedKeySpecifier)[];
export type AttributeValueUpdatedFieldPolicy = {
issuedAt?: FieldPolicy<any> | FieldReadFunction<any>,
version?: FieldPolicy<any> | FieldReadFunction<any>,
issuingPrincipal?: FieldPolicy<any> | FieldReadFunction<any>,
recipient?: FieldPolicy<any> | FieldReadFunction<any>,
attributeValue?: FieldPolicy<any> | FieldReadFunction<any>
};
export type BulkProductErrorKeySpecifier = ('field' | 'message' | 'code' | 'attributes' | 'values' | 'index' | 'warehouses' | 'channels' | BulkProductErrorKeySpecifier)[];
export type BulkProductErrorFieldPolicy = {
field?: FieldPolicy<any> | FieldReadFunction<any>,
@ -684,7 +708,7 @@ export type CategoryUpdatedFieldPolicy = {
recipient?: FieldPolicy<any> | FieldReadFunction<any>,
category?: FieldPolicy<any> | FieldReadFunction<any>
};
export type ChannelKeySpecifier = ('id' | 'name' | 'isActive' | 'currencyCode' | 'slug' | 'hasOrders' | 'defaultCountry' | ChannelKeySpecifier)[];
export type ChannelKeySpecifier = ('id' | 'name' | 'isActive' | 'currencyCode' | 'slug' | 'hasOrders' | 'defaultCountry' | 'warehouses' | ChannelKeySpecifier)[];
export type ChannelFieldPolicy = {
id?: FieldPolicy<any> | FieldReadFunction<any>,
name?: FieldPolicy<any> | FieldReadFunction<any>,
@ -692,7 +716,8 @@ export type ChannelFieldPolicy = {
currencyCode?: FieldPolicy<any> | FieldReadFunction<any>,
slug?: FieldPolicy<any> | FieldReadFunction<any>,
hasOrders?: FieldPolicy<any> | FieldReadFunction<any>,
defaultCountry?: FieldPolicy<any> | FieldReadFunction<any>
defaultCountry?: FieldPolicy<any> | FieldReadFunction<any>,
warehouses?: FieldPolicy<any> | FieldReadFunction<any>
};
export type ChannelActivateKeySpecifier = ('channel' | 'channelErrors' | 'errors' | ChannelActivateKeySpecifier)[];
export type ChannelActivateFieldPolicy = {
@ -734,12 +759,13 @@ export type ChannelDeletedFieldPolicy = {
recipient?: FieldPolicy<any> | FieldReadFunction<any>,
channel?: FieldPolicy<any> | FieldReadFunction<any>
};
export type ChannelErrorKeySpecifier = ('field' | 'message' | 'code' | 'shippingZones' | ChannelErrorKeySpecifier)[];
export type ChannelErrorKeySpecifier = ('field' | 'message' | 'code' | 'shippingZones' | 'warehouses' | ChannelErrorKeySpecifier)[];
export type ChannelErrorFieldPolicy = {
field?: FieldPolicy<any> | FieldReadFunction<any>,
message?: FieldPolicy<any> | FieldReadFunction<any>,
code?: FieldPolicy<any> | FieldReadFunction<any>,
shippingZones?: FieldPolicy<any> | FieldReadFunction<any>
shippingZones?: FieldPolicy<any> | FieldReadFunction<any>,
warehouses?: FieldPolicy<any> | FieldReadFunction<any>
};
export type ChannelStatusChangedKeySpecifier = ('issuedAt' | 'version' | 'issuingPrincipal' | 'recipient' | 'channel' | ChannelStatusChangedKeySpecifier)[];
export type ChannelStatusChangedFieldPolicy = {
@ -886,9 +912,15 @@ export type CheckoutLanguageCodeUpdateFieldPolicy = {
checkoutErrors?: FieldPolicy<any> | FieldReadFunction<any>,
errors?: FieldPolicy<any> | FieldReadFunction<any>
};
export type CheckoutLineKeySpecifier = ('id' | 'variant' | 'quantity' | 'unitPrice' | 'undiscountedUnitPrice' | 'totalPrice' | 'undiscountedTotalPrice' | 'requiresShipping' | CheckoutLineKeySpecifier)[];
export type CheckoutLineKeySpecifier = ('id' | 'privateMetadata' | 'privateMetafield' | 'privateMetafields' | 'metadata' | 'metafield' | 'metafields' | 'variant' | 'quantity' | 'unitPrice' | 'undiscountedUnitPrice' | 'totalPrice' | 'undiscountedTotalPrice' | 'requiresShipping' | CheckoutLineKeySpecifier)[];
export type CheckoutLineFieldPolicy = {
id?: FieldPolicy<any> | FieldReadFunction<any>,
privateMetadata?: FieldPolicy<any> | FieldReadFunction<any>,
privateMetafield?: FieldPolicy<any> | FieldReadFunction<any>,
privateMetafields?: FieldPolicy<any> | FieldReadFunction<any>,
metadata?: FieldPolicy<any> | FieldReadFunction<any>,
metafield?: FieldPolicy<any> | FieldReadFunction<any>,
metafields?: FieldPolicy<any> | FieldReadFunction<any>,
variant?: FieldPolicy<any> | FieldReadFunction<any>,
quantity?: FieldPolicy<any> | FieldReadFunction<any>,
unitPrice?: FieldPolicy<any> | FieldReadFunction<any>,
@ -2736,9 +2768,15 @@ export type OrderFullyPaidFieldPolicy = {
recipient?: FieldPolicy<any> | FieldReadFunction<any>,
order?: FieldPolicy<any> | FieldReadFunction<any>
};
export type OrderLineKeySpecifier = ('id' | 'productName' | 'variantName' | 'productSku' | 'productVariantId' | 'isShippingRequired' | 'quantity' | 'quantityFulfilled' | 'unitDiscountReason' | 'taxRate' | 'digitalContentUrl' | 'thumbnail' | 'unitPrice' | 'undiscountedUnitPrice' | 'unitDiscount' | 'unitDiscountValue' | 'totalPrice' | 'variant' | 'translatedProductName' | 'translatedVariantName' | 'allocations' | 'quantityToFulfill' | 'unitDiscountType' | OrderLineKeySpecifier)[];
export type OrderLineKeySpecifier = ('id' | 'privateMetadata' | 'privateMetafield' | 'privateMetafields' | 'metadata' | 'metafield' | 'metafields' | 'productName' | 'variantName' | 'productSku' | 'productVariantId' | 'isShippingRequired' | 'quantity' | 'quantityFulfilled' | 'unitDiscountReason' | 'taxRate' | 'digitalContentUrl' | 'thumbnail' | 'unitPrice' | 'undiscountedUnitPrice' | 'unitDiscount' | 'unitDiscountValue' | 'totalPrice' | 'variant' | 'translatedProductName' | 'translatedVariantName' | 'allocations' | 'quantityToFulfill' | 'unitDiscountType' | OrderLineKeySpecifier)[];
export type OrderLineFieldPolicy = {
id?: FieldPolicy<any> | FieldReadFunction<any>,
privateMetadata?: FieldPolicy<any> | FieldReadFunction<any>,
privateMetafield?: FieldPolicy<any> | FieldReadFunction<any>,
privateMetafields?: FieldPolicy<any> | FieldReadFunction<any>,
metadata?: FieldPolicy<any> | FieldReadFunction<any>,
metafield?: FieldPolicy<any> | FieldReadFunction<any>,
metafields?: FieldPolicy<any> | FieldReadFunction<any>,
productName?: FieldPolicy<any> | FieldReadFunction<any>,
variantName?: FieldPolicy<any> | FieldReadFunction<any>,
productSku?: FieldPolicy<any> | FieldReadFunction<any>,
@ -5271,10 +5309,18 @@ export type StrictTypedTypePolicies = {
keyFields?: false | AttributeValueCreateKeySpecifier | (() => undefined | AttributeValueCreateKeySpecifier),
fields?: AttributeValueCreateFieldPolicy,
},
AttributeValueCreated?: Omit<TypePolicy, "fields" | "keyFields"> & {
keyFields?: false | AttributeValueCreatedKeySpecifier | (() => undefined | AttributeValueCreatedKeySpecifier),
fields?: AttributeValueCreatedFieldPolicy,
},
AttributeValueDelete?: Omit<TypePolicy, "fields" | "keyFields"> & {
keyFields?: false | AttributeValueDeleteKeySpecifier | (() => undefined | AttributeValueDeleteKeySpecifier),
fields?: AttributeValueDeleteFieldPolicy,
},
AttributeValueDeleted?: Omit<TypePolicy, "fields" | "keyFields"> & {
keyFields?: false | AttributeValueDeletedKeySpecifier | (() => undefined | AttributeValueDeletedKeySpecifier),
fields?: AttributeValueDeletedFieldPolicy,
},
AttributeValueTranslatableContent?: Omit<TypePolicy, "fields" | "keyFields"> & {
keyFields?: false | AttributeValueTranslatableContentKeySpecifier | (() => undefined | AttributeValueTranslatableContentKeySpecifier),
fields?: AttributeValueTranslatableContentFieldPolicy,
@ -5291,6 +5337,10 @@ export type StrictTypedTypePolicies = {
keyFields?: false | AttributeValueUpdateKeySpecifier | (() => undefined | AttributeValueUpdateKeySpecifier),
fields?: AttributeValueUpdateFieldPolicy,
},
AttributeValueUpdated?: Omit<TypePolicy, "fields" | "keyFields"> & {
keyFields?: false | AttributeValueUpdatedKeySpecifier | (() => undefined | AttributeValueUpdatedKeySpecifier),
fields?: AttributeValueUpdatedFieldPolicy,
},
BulkProductError?: Omit<TypePolicy, "fields" | "keyFields"> & {
keyFields?: false | BulkProductErrorKeySpecifier | (() => undefined | BulkProductErrorKeySpecifier),
fields?: BulkProductErrorFieldPolicy,

File diff suppressed because one or more lines are too long

View file

@ -447,6 +447,11 @@ export const sectionNames = defineMessages({
defaultMessage: "Shipping Methods",
description: "shipping section name",
},
shippingZones: {
id: "V1MytH",
defaultMessage: "Shipping Zones",
description: "shipping zones section name",
},
siteSettings: {
id: "viFkCw",
defaultMessage: "Site Settings",

View file

@ -2319,6 +2319,7 @@ export const shopOrderSettings: ShopOrderSettingsFragment = {
};
export const warehouseSearch: SearchWarehousesQuery["search"] = {
totalCount: 20,
edges: [
{
node: {

View file

@ -2,7 +2,6 @@ import AddIcon from "@material-ui/icons/Add";
import Link from "@saleor/components/Link";
import { IconButton, makeStyles } from "@saleor/macaw-ui";
import React, { MutableRefObject } from "react";
import { MessageDescriptor, useIntl } from "react-intl";
const useStyles = makeStyles(
theme => ({
@ -21,7 +20,7 @@ const useStyles = makeStyles(
);
interface CardAddItemsFooterProps {
title: MessageDescriptor;
title: string;
onAdd: () => void;
testIds: {
link: string;
@ -37,13 +36,12 @@ const CardAddItemsFooter: React.FC<CardAddItemsFooterProps> = ({
ref,
children,
}) => {
const intl = useIntl();
const classes = useStyles({});
return (
<div className={classes.container} ref={ref}>
<Link data-test-id={testIds.link} onClick={onAdd}>
{intl.formatMessage(title)}
{title}
</Link>
<IconButton
variant="secondary"

View file

@ -28,14 +28,8 @@ import PreviewPill from "@saleor/components/PreviewPill";
import { ProductErrorFragment, WarehouseFragment } from "@saleor/graphql";
import { FormChange, FormErrors } from "@saleor/hooks/useForm";
import { FormsetAtomicData, FormsetChange } from "@saleor/hooks/useFormset";
import {
Button,
DeleteIcon,
IconButton,
ICONBUTTON_SIZE,
makeStyles,
PlusIcon,
} from "@saleor/macaw-ui";
import { sectionNames } from "@saleor/intl";
import { Button, DeleteIcon, IconButton, PlusIcon } from "@saleor/macaw-ui";
import { renderCollection } from "@saleor/misc";
import { getFormErrors, getProductErrorMessage } from "@saleor/utils/errors";
import createNonNegativeValueChangeHandler from "@saleor/utils/handlers/nonNegativeValueChangeHandler";
@ -46,6 +40,8 @@ import { ProductCreateData } from "../ProductCreatePage";
import { ProductUpdateSubmitData } from "../ProductUpdatePage/form";
import { ProductVariantCreateData } from "../ProductVariantCreatePage/form";
import { ProductVariantUpdateData } from "../ProductVariantPage/form";
import { messages } from "./messages";
import { useStyles } from "./styles";
export interface ProductStockFormsetData {
quantityAllocated: number;
@ -90,98 +86,6 @@ export interface ProductStocksProps {
onWarehouseConfigure: () => void;
}
const useStyles = makeStyles(
theme => ({
colAction: {
padding: 0,
width: `calc(${ICONBUTTON_SIZE}px + ${theme.spacing(1)})`,
},
colName: {},
colQuantity: {
textAlign: "right",
width: 150,
},
colSoldUnits: {
textAlign: "right",
width: 150,
},
colThreshold: {
textAlign: "right",
width: 180,
},
editWarehouses: {
marginRight: theme.spacing(-1),
},
input: {
padding: theme.spacing(1.5),
textAlign: "right",
},
menuItem: {
"&:not(:last-of-type)": {
marginBottom: theme.spacing(2),
},
},
noWarehouseInfo: {
marginTop: theme.spacing(),
},
paper: {
padding: theme.spacing(2),
},
popper: {
marginTop: theme.spacing(1),
zIndex: 2,
},
quantityContainer: {
paddingTop: theme.spacing(),
},
quantityHeader: {
alignItems: "center",
display: "flex",
justifyContent: "space-between",
},
skuInputContainer: {
display: "grid",
gridColumnGap: theme.spacing(3),
gridTemplateColumns: "repeat(2, 1fr)",
},
dateTimeInputs: {
marginTop: theme.spacing(2),
marginBottom: theme.spacing(2),
},
preorderInfo: {
marginBottom: theme.spacing(2),
marginTop: theme.spacing(2),
display: "block",
},
caption: {
fontSize: 14,
},
thresholdRow: {
display: "grid",
gridColumnGap: theme.spacing(3),
gridTemplateColumns: "3fr 1fr",
marginTop: theme.spacing(1),
},
thresholdInput: {
maxWidth: 400,
},
preorderItemsLeftCount: {
fontSize: 14,
paddingTop: theme.spacing(2),
textAlign: "center",
},
preorderLimitInfo: {
marginTop: theme.spacing(3),
},
preview: {
marginLeft: theme.spacing(1),
},
}),
{
name: "ProductStocks",
},
);
const ProductStocks: React.FC<ProductStocksProps> = ({
data,
disabled,
@ -200,7 +104,7 @@ const ProductStocks: React.FC<ProductStocksProps> = ({
onWarehouseStockDelete,
onWarehouseConfigure,
}) => {
const classes = useStyles({});
const classes = useStyles();
const intl = useIntl();
const anchor = React.useRef<HTMLDivElement>();
const [isExpanded, setExpansionState] = React.useState(false);
@ -218,13 +122,7 @@ const ProductStocks: React.FC<ProductStocksProps> = ({
return (
<Card>
<CardTitle
title={intl.formatMessage({
id: "4qe6hO",
defaultMessage: "Inventory",
description: "product stock, section header",
})}
/>
<CardTitle title={intl.formatMessage(messages.title)} />
<CardContent>
<div className={classes.skuInputContainer}>
<TextField
@ -232,10 +130,7 @@ const ProductStocks: React.FC<ProductStocksProps> = ({
error={!!formErrors.sku}
fullWidth
helperText={getProductErrorMessage(formErrors.sku, intl)}
label={intl.formatMessage({
id: "xB7BTp",
defaultMessage: "SKU (Stock Keeping Unit)",
})}
label={intl.formatMessage(messages.sku)}
name="sku"
onChange={onFormDataChange}
value={data.sku}
@ -252,11 +147,7 @@ const ProductStocks: React.FC<ProductStocksProps> = ({
disabled={disabled}
label={
<>
<FormattedMessage
id="eAFU/E"
defaultMessage="Variant currently in preorder"
description="product inventory, checkbox"
/>
<FormattedMessage {...messages.variantInPreorder} />
<PreviewPill className={classes.preview} />
</>
}
@ -272,16 +163,9 @@ const ProductStocks: React.FC<ProductStocksProps> = ({
disabled={disabled}
label={
<>
<FormattedMessage
id="TjGYna"
defaultMessage="Track Inventory"
description="product inventory, checkbox"
/>
<FormattedMessage {...messages.trackInventory} />
<Typography variant="caption">
<FormattedMessage
id="jABdx1"
defaultMessage="Active inventory tracking will automatically calculate changes of stock"
/>
<FormattedMessage {...messages.trackInventoryDescription} />
</Typography>
</>
}
@ -295,14 +179,18 @@ const ProductStocks: React.FC<ProductStocksProps> = ({
<Typography>
<div className={classes.quantityHeader}>
<span>
<FormattedMessage
id="bp/i0x"
defaultMessage="Quantity"
description="header"
/>
<FormattedMessage {...messages.quantity} />
</span>
</div>
</Typography>
{!productVariantChannelListings?.length && (
<>
<FormSpacer />
<Typography variant="caption">
<FormattedMessage {...messages.noChannelWarehousesAllocation} />
</Typography>
</>
)}
{!warehouses?.length && (
<Typography
@ -312,9 +200,7 @@ const ProductStocks: React.FC<ProductStocksProps> = ({
{hasVariants ? (
<>
<FormattedMessage
id="D8nsBc"
defaultMessage="There are no warehouses set up for your store. To add stock quantity to the variant please <a>configure a warehouse</a>"
description="no warehouses info"
{...messages.configureWarehouseForVariant}
values={{
a: chunks => (
<Link onClick={onWarehouseConfigure}>{chunks}</Link>
@ -325,9 +211,7 @@ const ProductStocks: React.FC<ProductStocksProps> = ({
) : (
<>
<FormattedMessage
id="RLBLPQ"
defaultMessage="There are no warehouses set up for your store. To add stock quantity to the product please <a>configure a warehouse</a>"
description="no warehouses info"
{...messages.configureWarehouseForProduct}
values={{
a: chunks => (
<Link onClick={onWarehouseConfigure}>{chunks}</Link>
@ -340,149 +224,130 @@ const ProductStocks: React.FC<ProductStocksProps> = ({
)}
</CardContent>
)}
{warehouses?.length > 0 && !data.isPreorder && (
<Table>
<colgroup>
<col className={classes.colName} />
<col className={classes.colQuantity} />
<col className={classes.colQuantity} />
</colgroup>
<TableHead>
<TableRow>
<TableCell className={classes.colName}>
<FormattedMessage
id="KTAg0f"
defaultMessage="Warehouse Name"
description="tabel column header"
/>
</TableCell>
<TableCell className={classes.colQuantity}>
<FormattedMessage
id="g/FRtd"
defaultMessage="Allocated"
description="table column header, allocated product quantity"
/>
</TableCell>
<TableCell className={classes.colQuantity}>
<FormattedMessage
id="ge/xFX"
defaultMessage="Quantity"
description="table column header"
/>
</TableCell>
<TableCell className={classes.colAction} />
</TableRow>
</TableHead>
<TableBody>
{renderCollection(stocks, stock => {
const handleQuantityChange = createNonNegativeValueChangeHandler(
event => onChange(stock.id, event.target.value),
);
{productVariantChannelListings?.length > 0 &&
warehouses?.length > 0 &&
!data.isPreorder && (
<Table>
<colgroup>
<col className={classes.colName} />
<col className={classes.colQuantity} />
<col className={classes.colQuantity} />
</colgroup>
<TableHead>
<TableRow>
<TableCell className={classes.colName}>
<FormattedMessage {...messages.warehouseName} />
</TableCell>
<TableCell className={classes.colQuantity}>
<FormattedMessage {...messages.allocated} />
</TableCell>
<TableCell className={classes.colQuantity}>
<FormattedMessage {...messages.quantity} />
</TableCell>
<TableCell className={classes.colAction} />
</TableRow>
</TableHead>
<TableBody>
{renderCollection(stocks, stock => {
const handleQuantityChange = createNonNegativeValueChangeHandler(
event => onChange(stock.id, event.target.value),
);
return (
<TableRow key={stock.id}>
<TableCell className={classes.colName}>
{stock.label}
</TableCell>
<TableCell className={classes.colQuantity}>
{stock.data?.quantityAllocated || 0}
</TableCell>
<TableCell className={classes.colQuantity}>
<TextField
data-test-id="stock-input"
disabled={disabled}
fullWidth
inputProps={{
className: classes.input,
min: 0,
type: "number",
}}
onChange={handleQuantityChange}
value={stock.value}
/>
return (
<TableRow key={stock.id}>
<TableCell className={classes.colName}>
{stock.label}
</TableCell>
<TableCell className={classes.colQuantity}>
{stock.data?.quantityAllocated || 0}
</TableCell>
<TableCell className={classes.colQuantity}>
<TextField
data-test-id="stock-input"
disabled={disabled}
fullWidth
inputProps={{
className: classes.input,
min: 0,
type: "number",
}}
onChange={handleQuantityChange}
value={stock.value}
/>
</TableCell>
<TableCell className={classes.colAction}>
<IconButton
variant="secondary"
color="primary"
onClick={() => onWarehouseStockDelete(stock.id)}
>
<DeleteIcon />
</IconButton>
</TableCell>
</TableRow>
);
})}
{warehousesToAssign.length > 0 && (
<TableRow>
<TableCell colSpan={3}>
<Typography variant="body2">
<FormattedMessage {...messages.assignWarehouse} />
</Typography>
</TableCell>
<TableCell className={classes.colAction}>
<IconButton
variant="secondary"
color="primary"
onClick={() => onWarehouseStockDelete(stock.id)}
<ClickAwayListener
onClickAway={() => setExpansionState(false)}
>
<DeleteIcon />
</IconButton>
<div ref={anchor}>
<IconButton
data-test-id="add-warehouse"
color="primary"
variant="secondary"
onClick={() => setExpansionState(!isExpanded)}
>
<PlusIcon />
</IconButton>
<Popper
className={classes.popper}
open={isExpanded}
anchorEl={anchor.current}
transition
placement="top-end"
>
{({ TransitionProps }) => (
<Grow
{...TransitionProps}
style={{
transformOrigin: "right top",
}}
>
<Paper className={classes.paper} elevation={8}>
{warehousesToAssign.map(warehouse => (
<MenuItem
className={classes.menuItem}
onClick={() =>
onWarehouseStockAdd(warehouse.id)
}
>
{warehouse.name}
</MenuItem>
))}
</Paper>
</Grow>
)}
</Popper>
</div>
</ClickAwayListener>
</TableCell>
</TableRow>
);
})}
{warehousesToAssign.length > 0 && (
<TableRow>
<TableCell colSpan={3}>
<Typography variant="body2">
<FormattedMessage
id="cBHRxx"
defaultMessage="Assign Warehouse"
description="button"
/>
</Typography>
</TableCell>
<TableCell className={classes.colAction}>
<ClickAwayListener
onClickAway={() => setExpansionState(false)}
>
<div ref={anchor}>
<IconButton
data-test-id="add-warehouse"
color="primary"
variant="secondary"
onClick={() => setExpansionState(!isExpanded)}
>
<PlusIcon />
</IconButton>
<Popper
className={classes.popper}
open={isExpanded}
anchorEl={anchor.current}
transition
placement="top-end"
>
{({ TransitionProps }) => (
<Grow
{...TransitionProps}
style={{
transformOrigin: "right top",
}}
>
<Paper className={classes.paper} elevation={8}>
{warehousesToAssign.map(warehouse => (
<MenuItem
className={classes.menuItem}
onClick={() =>
onWarehouseStockAdd(warehouse.id)
}
>
{warehouse.name}
</MenuItem>
))}
</Paper>
</Grow>
)}
</Popper>
</div>
</ClickAwayListener>
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
)}
)}
</TableBody>
</Table>
)}
{data.isPreorder && (
<CardContent>
<Typography variant="caption" className={classes.caption}>
{intl.formatMessage({
id: "REVk27",
defaultMessage:
"Set up an end date of preorder. When end date will be reached product will be automatically taken from preorder to standard selling",
description: "info text",
})}
<FormattedMessage {...messages.preorderEndDateSetup} />
</Typography>
{data.hasPreorderEndDate && (
@ -519,22 +384,11 @@ const ProductStocks: React.FC<ProductStocksProps> = ({
}
>
{data.hasPreorderEndDate
? intl.formatMessage({
id: "2qJc9y",
defaultMessage: "CANCEL END DATE",
})
: intl.formatMessage({
id: "7Ii5ZQ",
defaultMessage: "SETUP END DATE",
})}
? intl.formatMessage(messages.endDateCancel)
: intl.formatMessage(messages.endDateSetup)}
</Button>
<Typography variant="caption" className={classes.preorderLimitInfo}>
{intl.formatMessage({
id: "Gz+4CI",
defaultMessage:
"Preordered products will be available in all warehouses. You can set a threshold for sold quantity. Leaving input blank will be interpreted as no limit to sale. Sold items will be allocated at the warehouse assigned to chosen shipping zone.",
description: "info text",
})}
<FormattedMessage {...messages.preorderProductsAvailability} />
</Typography>
<div className={classes.thresholdRow}>
<TextField
@ -543,15 +397,10 @@ const ProductStocks: React.FC<ProductStocksProps> = ({
}}
disabled={disabled}
fullWidth
helperText={intl.formatMessage({
id: "NcY4ph",
defaultMessage:
"Threshold that cannot be exceeded even if per channel thresholds are still available",
})}
label={intl.formatMessage({
id: "RJ5QxE",
defaultMessage: "Global threshold",
})}
helperText={intl.formatMessage(
messages.preorderTresholdDescription,
)}
label={intl.formatMessage(messages.preorderTresholdLabel)}
name="globalThreshold"
onChange={onThresholdChange}
value={data.globalThreshold ?? ""}
@ -563,19 +412,10 @@ const ProductStocks: React.FC<ProductStocksProps> = ({
className={classes.preorderItemsLeftCount}
>
{data.globalThreshold
? intl.formatMessage(
{
id: "7wkGxW",
defaultMessage: "{unitsLeft} units left",
description: "app has been installed",
},
{ unitsLeft },
)
: intl.formatMessage({
id: "CEavJt",
defaultMessage: "Unlimited",
description: "section header",
})}
? intl.formatMessage(messages.preorderTresholdUnitsLeft, {
unitsLeft,
})
: intl.formatMessage(messages.preorderTresholdUnlimited)}
</Typography>
)}
</div>
@ -592,25 +432,13 @@ const ProductStocks: React.FC<ProductStocksProps> = ({
<TableHead>
<TableRow>
<TableCell className={classes.colName}>
<FormattedMessage
id="JyQEHU"
defaultMessage="Channels"
description="tabel column header"
/>
<FormattedMessage {...sectionNames.channels} />
</TableCell>
<TableCell className={classes.colSoldUnits}>
<FormattedMessage
id="HcQEUk"
defaultMessage="Sold units"
description="table column header, sold units preorder quantity"
/>
<FormattedMessage {...messages.soldUnits} />
</TableCell>
<TableCell className={classes.colThreshold}>
<FormattedMessage
id="MNZY28"
defaultMessage="Channel threshold"
description="table column header"
/>
<FormattedMessage {...messages.channelTreshold} />
</TableCell>
</TableRow>
</TableHead>
@ -638,10 +466,9 @@ const ProductStocks: React.FC<ProductStocksProps> = ({
min: 0,
type: "number",
}}
placeholder={intl.formatMessage({
id: "ekXood",
defaultMessage: "Unlimited",
})}
placeholder={intl.formatMessage(
messages.preorderTresholdUnlimited,
)}
onChange={e => {
onVariantChannelListingChange(listing.id, {
costPrice: listing.costPrice,

View file

@ -0,0 +1,121 @@
import { defineMessages } from "react-intl";
export const messages = defineMessages({
title: {
id: "4qe6hO",
defaultMessage: "Inventory",
description: "product stock, section header",
},
sku: {
id: "SM+yG0",
defaultMessage: "SKU (Stock Keeping Unit)",
description: "input label",
},
variantInPreorder: {
id: "eAFU/E",
defaultMessage: "Variant currently in preorder",
description: "product inventory, checkbox",
},
trackInventory: {
id: "TjGYna",
defaultMessage: "Track Inventory",
description: "product inventory, checkbox",
},
trackInventoryDescription: {
id: "Wyl25+",
defaultMessage:
"Active inventory tracking will automatically calculate changes of stock",
description: "product inventory, checkbox description",
},
quantity: {
id: "bp/i0x",
defaultMessage: "Quantity",
description: "header",
},
warehouseName: {
id: "ErvPaM",
defaultMessage: "Warehouse Name",
description: "header",
},
allocated: {
id: "/C//FB",
defaultMessage: "Allocated",
description: "header, allocated product quantity",
},
noChannelWarehousesAllocation: {
id: "taS/08",
defaultMessage:
"Assign this variant to a channel in the product channel manager to define warehouses allocation",
description: "variant stocks section subtitle",
},
configureWarehouseForVariant: {
id: "D8nsBc",
defaultMessage:
"There are no warehouses set up for your store. To add stock quantity to the variant please <a>configure a warehouse</a>",
description: "no warehouses info",
},
configureWarehouseForProduct: {
id: "RLBLPQ",
defaultMessage:
"There are no warehouses set up for your store. To add stock quantity to the product please <a>configure a warehouse</a>",
description: "no warehouses info",
},
assignWarehouse: {
id: "cBHRxx",
defaultMessage: "Assign Warehouse",
description: "button",
},
preorderEndDateSetup: {
id: "REVk27",
defaultMessage:
"Set up an end date of preorder. When end date will be reached product will be automatically taken from preorder to standard selling",
description: "info text",
},
endDateCancel: {
id: "v9ILn/",
defaultMessage: "CANCEL END DATE",
description: "button",
},
endDateSetup: {
id: "9IWg/f",
defaultMessage: "SETUP END DATE",
description: "button",
},
preorderProductsAvailability: {
id: "Gz+4CI",
defaultMessage:
"Preordered products will be available in all warehouses. You can set a threshold for sold quantity. Leaving input blank will be interpreted as no limit to sale. Sold items will be allocated at the warehouse assigned to chosen shipping zone.",
description: "info text",
},
preorderTresholdLabel: {
id: "/iijFq",
defaultMessage: "Global threshold",
description: "input label",
},
preorderTresholdDescription: {
id: "HYC6cH",
defaultMessage:
"Threshold that cannot be exceeded even if per channel thresholds are still available",
description: "input description",
},
preorderTresholdUnitsLeft: {
id: "JkO0jp",
defaultMessage: "{unitsLeft} units left",
description: "input description",
},
preorderTresholdUnlimited: {
id: "tlGXkh",
defaultMessage: "Unlimited",
description: "input description",
},
soldUnits: {
id: "HcQEUk",
defaultMessage: "Sold units",
description: "table column header, sold units preorder quantity",
},
channelTreshold: {
id: "MNZY28",
defaultMessage: "Channel threshold",
description: "table column header",
},
});

View file

@ -0,0 +1,93 @@
import { ICONBUTTON_SIZE, makeStyles } from "@saleor/macaw-ui";
export const useStyles = makeStyles(
theme => ({
colAction: {
padding: 0,
width: `calc(${ICONBUTTON_SIZE}px + ${theme.spacing(1)})`,
},
colName: {},
colQuantity: {
textAlign: "right",
width: 150,
},
colSoldUnits: {
textAlign: "right",
width: 150,
},
colThreshold: {
textAlign: "right",
width: 180,
},
editWarehouses: {
marginRight: theme.spacing(-1),
},
input: {
padding: theme.spacing(1.5),
textAlign: "right",
},
menuItem: {
"&:not(:last-of-type)": {
marginBottom: theme.spacing(2),
},
},
noWarehouseInfo: {
marginTop: theme.spacing(),
},
paper: {
padding: theme.spacing(2),
},
popper: {
marginTop: theme.spacing(1),
zIndex: 2,
},
quantityContainer: {
paddingTop: theme.spacing(),
},
quantityHeader: {
alignItems: "center",
display: "flex",
justifyContent: "space-between",
},
skuInputContainer: {
display: "grid",
gridColumnGap: theme.spacing(3),
gridTemplateColumns: "repeat(2, 1fr)",
},
dateTimeInputs: {
marginTop: theme.spacing(2),
marginBottom: theme.spacing(2),
},
preorderInfo: {
marginBottom: theme.spacing(2),
marginTop: theme.spacing(2),
display: "block",
},
caption: {
fontSize: 14,
},
thresholdRow: {
display: "grid",
gridColumnGap: theme.spacing(3),
gridTemplateColumns: "3fr 1fr",
marginTop: theme.spacing(1),
},
thresholdInput: {
maxWidth: 400,
},
preorderItemsLeftCount: {
fontSize: 14,
paddingTop: theme.spacing(2),
textAlign: "center",
},
preorderLimitInfo: {
marginTop: theme.spacing(3),
},
preview: {
marginLeft: theme.spacing(1),
},
}),
{
name: "ProductStocks",
},
);

View file

@ -112,12 +112,6 @@ export const ProductCreateView: React.FC<ProductCreateProps> = ({ params }) => {
result: searchAttributeValuesOpts,
reset: searchAttributeReset,
} = useAttributeValueSearchHandler(DEFAULT_INITIAL_SEARCH_DATA);
const warehouses = useWarehouseListQuery({
displayLoader: true,
variables: {
first: 50,
},
});
const [updateMetadata] = useUpdateMetadataMutation({});
const [updatePrivateMetadata] = useUpdatePrivateMetadataMutation({});
const taxTypes = useTaxTypeListQuery({});
@ -160,6 +154,16 @@ export const ProductCreateView: React.FC<ProductCreateProps> = ({ params }) => {
},
);
const warehouses = useWarehouseListQuery({
displayLoader: true,
variables: {
first: 50,
filter: {
channels: currentChannels.map(channel => channel.id),
},
},
});
const handleSuccess = (productId: string) => {
notify({
status: "success",

View file

@ -156,12 +156,6 @@ export const ProductUpdate: React.FC<ProductUpdateProps> = ({ id, params }) => {
result: searchAttributeValuesOpts,
reset: searchAttributeReset,
} = useAttributeValueSearchHandler(DEFAULT_INITIAL_SEARCH_DATA);
const warehouses = useWarehouseListQuery({
displayLoader: true,
variables: {
first: 50,
},
});
const shop = useShop();
const [updateMetadata] = useUpdateMetadataMutation({});
const [updatePrivateMetadata] = useUpdatePrivateMetadataMutation({});
@ -324,6 +318,16 @@ export const ProductUpdate: React.FC<ProductUpdateProps> = ({ id, params }) => {
{ formId: PRODUCT_UPDATE_FORM_ID },
);
const warehouses = useWarehouseListQuery({
displayLoader: true,
variables: {
first: 50,
filter: {
channels: currentChannels.map(channel => channel.id),
},
},
});
const [
updateChannels,
updateChannelsOpts,

View file

@ -82,13 +82,6 @@ export const ProductVariant: React.FC<ProductUpdateProps> = ({
setErrors([]);
}, [variantId]);
const warehouses = useWarehouseListQuery({
displayLoader: true,
variables: {
first: 50,
},
});
const { data, loading } = useProductVariantDetailsQuery({
displayLoader: true,
variables: {
@ -196,6 +189,16 @@ export const ProductVariant: React.FC<ProductUpdateProps> = ({
const variant = data?.productVariant;
const channels = createVariantChannels(variant);
const warehouses = useWarehouseListQuery({
displayLoader: true,
variables: {
first: 50,
filter: {
channels: channels.map(channel => channel.id),
},
},
});
const [
deactivatePreorder,
deactivatePreoderOpts,

View file

@ -14,6 +14,7 @@ export const searchWarehouses = gql`
sortBy: { direction: ASC, field: NAME }
filter: { search: $query }
) {
totalCount
edges {
node {
id

File diff suppressed because it is too large Load diff

View file

@ -36,3 +36,17 @@ export const warehouseDetails = gql`
}
}
`;
// first: 100 - to be removed when we implement pagintion in ui for this query
export const channelWarehouses = gql`
query ChannelWarehouses($filter: WarehouseFilterInput) {
warehouses(filter: $filter, first: 100) {
edges {
node {
id
name
}
}
}
}
`;