Excluded Products in shipping view (#866)

* Clean up stories

* Add missing props

* Add zip codes section (#861)

* Add zip code listing

* Add list wrapping

* Update snapshots

* Set up API data

* Fix lgtm warning

* Update snapshots

* Run Actions on all PR

* Checks on PR

* Test envs on PR

* Cleanup action on PR

* Update messages

Co-authored-by: Krzysztof Wolski <krzysztof.k.wolski@gmail.com>

* Allow zip codes to be assigned to shipping method

* Add zip code deletion (#871)

* Add zip code range dialog

* Fix path management

* Use query params to handle modal actions

* Allow zip codes to be assigned to shipping method

* Make params optional

* Fix types

* Clean up urls

* Add zip code range delete action

* Update snapshots and messages

* Update schema

* Refresh zip code list after assigning them

* Update types and snapshots

* Update snapshots

* Fix error message, checkbox default value (#880)

* Fix error message, checkbox default value

* Update snapshots

* Update schema and types

* Update stories

* add excluded products section in shipping methods views

* create UnassignDialog component

* use priceRangeFragment in shipping queries

* remove unneeded price from ShippingMethodAddProductsDialog

* update messages in ShippingMethodProducts

* updates after rebase

* update snapshots, fix lint errors

* fix ShippingMethodProductsAddDialog

* update snapshots

* small fix in ShippingMethodProducts

* update snapshots after rebase

* add handleClose func in ShippingMethodProductsAddDialog

* Fix metadata not showing in category update

* update snapshots again

* update ShippingMethodProductsAddDialog

* updates after rebase

* update Price and Weight rates views

Co-authored-by: dominik-zeglen <flesz3@o2.pl>
Co-authored-by: Krzysztof Wolski <krzysztof.k.wolski@gmail.com>
Co-authored-by: Tomasz Szymański <lime129@gmail.com>
Co-authored-by: Magdalena Markusik <magdalena.markusik@mirumee.com>
This commit is contained in:
AlicjaSzu 2020-12-02 12:35:02 +01:00 committed by GitHub
parent f97ddd0128
commit b774cc9002
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
32 changed files with 6931 additions and 848 deletions

View file

@ -5349,6 +5349,33 @@
"context": "column title",
"string": "Channel name"
},
"src_dot_shipping_dot_components_dot_ShippingMethodProductsAddDialog_dot_2850255786": {
"string": "Search Products"
},
"src_dot_shipping_dot_components_dot_ShippingMethodProductsAddDialog_dot_353369701": {
"string": "No products matching given query"
},
"src_dot_shipping_dot_components_dot_ShippingMethodProductsAddDialog_dot_3756118423": {
"context": "dialog header",
"string": "Assign Products"
},
"src_dot_shipping_dot_components_dot_ShippingMethodProducts_dot_1737533260": {
"string": "No Products"
},
"src_dot_shipping_dot_components_dot_ShippingMethodProducts_dot_1781179817": {
"context": "section header",
"string": "Excluded Products"
},
"src_dot_shipping_dot_components_dot_ShippingMethodProducts_dot_2100305525": {
"context": "button",
"string": "Assign products"
},
"src_dot_shipping_dot_components_dot_ShippingMethodProducts_dot_2697405188": {
"string": "Product Name"
},
"src_dot_shipping_dot_components_dot_ShippingMethodProducts_dot_4190792473": {
"string": "Actions"
},
"src_dot_shipping_dot_components_dot_ShippingRateZipCodeRangeRemoveDialog_dot_1083561409": {
"string": "Are you sure you want to remove this ZIP-code rule?"
},
@ -5427,17 +5454,20 @@
"src_dot_shipping_dot_components_dot_ShippingZoneInfo_dot_579967655": {
"string": "Shipping rate name"
},
"src_dot_shipping_dot_components_dot_ShippingZoneRatesPage_dot_1161979494": {
"src_dot_shipping_dot_components_dot_ShippingZoneRatesCreatePage_dot_1161979494": {
"context": "page title",
"string": "Price Rate Create"
},
"src_dot_shipping_dot_components_dot_ShippingZoneRatesPage_dot_1325966144": {
"src_dot_shipping_dot_components_dot_ShippingZoneRatesCreatePage_dot_1325966144": {
"string": "Shipping"
},
"src_dot_shipping_dot_components_dot_ShippingZoneRatesPage_dot_3538551526": {
"src_dot_shipping_dot_components_dot_ShippingZoneRatesCreatePage_dot_3538551526": {
"context": "page title",
"string": "Weight Rate Create"
},
"src_dot_shipping_dot_components_dot_ShippingZoneRatesPage_dot_1325966144": {
"string": "Shipping"
},
"src_dot_shipping_dot_components_dot_ShippingZoneRates_dot_1134347598": {
"context": "shipping method price",
"string": "Price"
@ -5554,6 +5584,14 @@
"src_dot_shipping_dot_components_dot_ShippingZonesList_dot_655374584": {
"string": "No shipping zones found"
},
"src_dot_shipping_dot_components_dot_UnassignDialog_dot_1203193503": {
"context": "dialog header",
"string": "Unassign Products From Shipping"
},
"src_dot_shipping_dot_components_dot_UnassignDialog_dot_3215481647": {
"context": "dialog content",
"string": "{counter,plural,one{Are you sure you want to unassign this product?} other{Are you sure you want to unassign {displayQuantity} products?}}"
},
"src_dot_shipping_dot_invalid": {
"context": "error message",
"string": "Value is invalid"
@ -5587,6 +5625,10 @@
"src_dot_shipping_dot_views_dot_PriceRatesUpdate_dot_3823295269": {
"string": "Manage Channel Availability"
},
"src_dot_shipping_dot_views_dot_PriceRatesUpdate_dot_870815507": {
"context": "unassign products from shipping method, button",
"string": "Unassign"
},
"src_dot_shipping_dot_views_dot_ShippingZoneDetails_dot_1010705153": {
"context": "dialog header",
"string": "Delete Shipping Zone"
@ -5609,6 +5651,10 @@
"src_dot_shipping_dot_views_dot_WeightRatesUpdate_dot_3014453080": {
"string": "Manage Channels Availability"
},
"src_dot_shipping_dot_views_dot_WeightRatesUpdate_dot_870815507": {
"context": "unassign products from shipping method, button",
"string": "Unassign"
},
"src_dot_shipping_dot_weight": {
"context": "error message",
"string": "Maximum weight cannot be lower than minimum"

View file

@ -75,9 +75,9 @@ function useCategoryUpdateForm(
// Need to make it function to always have description.current up to date
const getData = (): CategoryUpdateData => ({
...form.data,
...getMetadata(form.data, isMetadataModified, isPrivateMetadataModified),
description: description.current
});
const getSubmitData = (): CategoryUpdateData => ({
...getData(),
...getMetadata(form.data, isMetadataModified, isPrivateMetadataModified)

View file

@ -56,6 +56,36 @@ export const shippingMethodFragment = gql`
}
}
`;
export const shippingMethodWithExcludedProductsFragment = gql`
${fragmentMoney}
${shippingMethodFragment}
fragment ShippingMethodWithExcludedProductsFragment on ShippingMethod {
...ShippingMethodFragment
excludedProducts(
before: $before
after: $after
first: $first
last: $last
) {
pageInfo {
hasNextPage
hasPreviousPage
endCursor
startCursor
}
edges {
node {
id
name
thumbnail {
url
}
}
}
}
}
`;
export const shippingZoneDetailsFragment = gql`
${shippingZoneFragment}
${shippingMethodFragment}

View file

@ -0,0 +1,105 @@
/* tslint:disable */
/* eslint-disable */
// This file was automatically generated and should not be edited.
import { WeightUnitsEnum, ShippingMethodTypeEnum } from "./../../types/globalTypes";
// ====================================================
// GraphQL fragment: ShippingMethodWithExcludedProductsFragment
// ====================================================
export interface ShippingMethodWithExcludedProductsFragment_zipCodeRules {
__typename: "ShippingMethodZipCodeRule";
id: string;
start: string | null;
end: string | null;
}
export interface ShippingMethodWithExcludedProductsFragment_minimumOrderWeight {
__typename: "Weight";
unit: WeightUnitsEnum;
value: number;
}
export interface ShippingMethodWithExcludedProductsFragment_maximumOrderWeight {
__typename: "Weight";
unit: WeightUnitsEnum;
value: number;
}
export interface ShippingMethodWithExcludedProductsFragment_channelListings_channel {
__typename: "Channel";
id: string;
name: string;
currencyCode: string;
}
export interface ShippingMethodWithExcludedProductsFragment_channelListings_price {
__typename: "Money";
amount: number;
currency: string;
}
export interface ShippingMethodWithExcludedProductsFragment_channelListings_minimumOrderPrice {
__typename: "Money";
amount: number;
currency: string;
}
export interface ShippingMethodWithExcludedProductsFragment_channelListings_maximumOrderPrice {
__typename: "Money";
amount: number;
currency: string;
}
export interface ShippingMethodWithExcludedProductsFragment_channelListings {
__typename: "ShippingMethodChannelListing";
id: string;
channel: ShippingMethodWithExcludedProductsFragment_channelListings_channel;
price: ShippingMethodWithExcludedProductsFragment_channelListings_price | null;
minimumOrderPrice: ShippingMethodWithExcludedProductsFragment_channelListings_minimumOrderPrice | null;
maximumOrderPrice: ShippingMethodWithExcludedProductsFragment_channelListings_maximumOrderPrice | null;
}
export interface ShippingMethodWithExcludedProductsFragment_excludedProducts_pageInfo {
__typename: "PageInfo";
hasNextPage: boolean;
hasPreviousPage: boolean;
endCursor: string | null;
startCursor: string | null;
}
export interface ShippingMethodWithExcludedProductsFragment_excludedProducts_edges_node_thumbnail {
__typename: "Image";
url: string;
}
export interface ShippingMethodWithExcludedProductsFragment_excludedProducts_edges_node {
__typename: "Product";
id: string;
name: string;
thumbnail: ShippingMethodWithExcludedProductsFragment_excludedProducts_edges_node_thumbnail | null;
}
export interface ShippingMethodWithExcludedProductsFragment_excludedProducts_edges {
__typename: "ProductCountableEdge";
node: ShippingMethodWithExcludedProductsFragment_excludedProducts_edges_node;
}
export interface ShippingMethodWithExcludedProductsFragment_excludedProducts {
__typename: "ProductCountableConnection";
pageInfo: ShippingMethodWithExcludedProductsFragment_excludedProducts_pageInfo;
edges: ShippingMethodWithExcludedProductsFragment_excludedProducts_edges[];
}
export interface ShippingMethodWithExcludedProductsFragment {
__typename: "ShippingMethod";
id: string;
zipCodeRules: (ShippingMethodWithExcludedProductsFragment_zipCodeRules | null)[] | null;
minimumOrderWeight: ShippingMethodWithExcludedProductsFragment_minimumOrderWeight | null;
maximumOrderWeight: ShippingMethodWithExcludedProductsFragment_maximumOrderWeight | null;
name: string;
type: ShippingMethodTypeEnum | null;
channelListings: ShippingMethodWithExcludedProductsFragment_channelListings[] | null;
excludedProducts: ShippingMethodWithExcludedProductsFragment_excludedProducts | null;
}

View file

@ -0,0 +1,34 @@
import { shippingZone } from "@saleor/shipping/fixtures";
import Decorator from "@saleor/storybook//Decorator";
import { storiesOf } from "@storybook/react";
import React from "react";
import ShippingMethodProducts, {
ShippingMethodProductsProps
} from "./ShippingMethodProducts";
const products = shippingZone.shippingMethods[0].excludedProducts.edges.map(
edge => edge.node
);
const props: ShippingMethodProductsProps = {
disabled: false,
isChecked: () => undefined,
onNextPage: () => undefined,
onPreviousPage: () => undefined,
onProductAssign: () => undefined,
onProductUnassign: () => undefined,
pageInfo: {
hasNextPage: false,
hasPreviousPage: false
},
products,
selected: products.length,
toggle: () => undefined,
toggleAll: () => undefined,
toolbar: () => undefined
};
storiesOf("Shipping / ShippingMethodProducts", module)
.addDecorator(Decorator)
.add("default", () => <ShippingMethodProducts {...props} />);

View file

@ -0,0 +1,171 @@
import Button from "@material-ui/core/Button";
import Card from "@material-ui/core/Card";
import IconButton from "@material-ui/core/IconButton";
import { makeStyles } from "@material-ui/core/styles";
import TableBody from "@material-ui/core/TableBody";
import TableCell from "@material-ui/core/TableCell";
import TableFooter from "@material-ui/core/TableFooter";
import TableRow from "@material-ui/core/TableRow";
import Typography from "@material-ui/core/Typography";
import DeleteIcon from "@material-ui/icons/Delete";
import CardTitle from "@saleor/components/CardTitle";
import Checkbox from "@saleor/components/Checkbox";
import ResponsiveTable from "@saleor/components/ResponsiveTable";
import Skeleton from "@saleor/components/Skeleton";
import TableCellAvatar from "@saleor/components/TableCellAvatar";
import TableHead from "@saleor/components/TableHead";
import TablePagination from "@saleor/components/TablePagination";
import { renderCollection } from "@saleor/misc";
import { ShippingZone_shippingZone_shippingMethods_excludedProducts_edges_node } from "@saleor/shipping/types/ShippingZone";
import { ListActions, ListProps } from "@saleor/types";
import React from "react";
import { FormattedMessage, useIntl } from "react-intl";
const useStyles = makeStyles(
theme => ({
colAction: {
"&:last-child": {
paddingRight: theme.spacing(3)
},
textAlign: "right",
width: 100
},
colName: {
width: "auto"
},
colProductName: {
paddingLeft: 0
},
table: {
tableLayout: "fixed"
}
}),
{ name: "ShippingMethodProducts" }
);
export interface ShippingMethodProductsProps
extends Pick<ListProps, Exclude<keyof ListProps, "onRowClick">>,
ListActions {
products: ShippingZone_shippingZone_shippingMethods_excludedProducts_edges_node[];
onProductAssign: () => void;
onProductUnassign: (ids: string[]) => void;
}
const numberOfColumns = 3;
const ShippingMethodProducts: React.FC<ShippingMethodProductsProps> = props => {
const {
disabled,
pageInfo,
products,
onNextPage,
onPreviousPage,
onProductAssign,
onProductUnassign,
isChecked,
selected,
toggle,
toggleAll,
toolbar
} = props;
const classes = useStyles(props);
const intl = useIntl();
return (
<Card>
<CardTitle
title={intl.formatMessage({
defaultMessage: "Excluded Products",
description: "section header"
})}
toolbar={
<Button color="primary" variant="text" onClick={onProductAssign}>
<FormattedMessage
defaultMessage="Assign products"
description="button"
/>
</Button>
}
/>
<ResponsiveTable className={classes.table}>
{!!products?.length && (
<>
<TableHead
colSpan={numberOfColumns}
selected={selected}
disabled={disabled}
items={products}
toggleAll={toggleAll}
toolbar={toolbar}
>
<TableCell className={classes.colProductName}>
<FormattedMessage defaultMessage="Product Name" />
</TableCell>
<TableCell className={classes.colAction}>
<FormattedMessage defaultMessage="Actions" />
</TableCell>
</TableHead>
<TableFooter>
<TableRow>
<TablePagination
colSpan={numberOfColumns}
hasNextPage={
pageInfo && !disabled ? pageInfo.hasNextPage : false
}
onNextPage={onNextPage}
hasPreviousPage={
pageInfo && !disabled ? pageInfo.hasPreviousPage : false
}
onPreviousPage={onPreviousPage}
/>
</TableRow>
</TableFooter>
</>
)}
<TableBody>
{products?.length === 0 ? (
<TableRow>
<TableCell colSpan={5}>
<FormattedMessage defaultMessage="No Products" />
</TableCell>
</TableRow>
) : (
renderCollection(products, product => {
const isSelected = product ? isChecked(product.id) : false;
return (
<TableRow key={product ? product.id : "skeleton"}>
<TableCell padding="checkbox">
<Checkbox
checked={isSelected}
disabled={disabled}
disableClickPropagation
onChange={() => toggle(product.id)}
/>
</TableCell>
<TableCellAvatar
className={classes.colName}
thumbnail={product?.thumbnail?.url}
>
{product?.name ? (
<Typography variant="body2">{product.name}</Typography>
) : (
<Skeleton />
)}
</TableCellAvatar>
<TableCell className={classes.colAction}>
<IconButton onClick={() => onProductUnassign([product.id])}>
<DeleteIcon color="primary" />
</IconButton>
</TableCell>
</TableRow>
);
})
)}
</TableBody>
</ResponsiveTable>
</Card>
);
};
ShippingMethodProducts.displayName = "ShippingMethodProducts";
export default ShippingMethodProducts;

View file

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

View file

@ -0,0 +1,24 @@
import { products } from "@saleor/shipping/fixtures";
import Decorator from "@saleor/storybook//Decorator";
import { storiesOf } from "@storybook/react";
import React from "react";
import ShippingMethodProductsAddDialog, {
ShippingMethodProductsAddDialogProps
} from "./ShippingMethodProductsAddDialog";
const props: ShippingMethodProductsAddDialogProps = {
confirmButtonState: "default",
hasMore: false,
loading: false,
onClose: () => undefined,
onFetch: () => undefined,
onFetchMore: () => undefined,
onSubmit: () => undefined,
open: true,
products
};
storiesOf("Shipping / ShippingMethodProductsAddDialog", module)
.addDecorator(Decorator)
.add("default", () => <ShippingMethodProductsAddDialog {...props} />);

View file

@ -0,0 +1,242 @@
import Button from "@material-ui/core/Button";
import CircularProgress from "@material-ui/core/CircularProgress";
import Dialog from "@material-ui/core/Dialog";
import DialogActions from "@material-ui/core/DialogActions";
import DialogContent from "@material-ui/core/DialogContent";
import DialogTitle from "@material-ui/core/DialogTitle";
import { makeStyles } from "@material-ui/core/styles";
import TableBody from "@material-ui/core/TableBody";
import TableCell from "@material-ui/core/TableCell";
import TableRow from "@material-ui/core/TableRow";
import TextField from "@material-ui/core/TextField";
import Checkbox from "@saleor/components/Checkbox";
import ConfirmButton, {
ConfirmButtonTransitionState
} from "@saleor/components/ConfirmButton";
import ResponsiveTable from "@saleor/components/ResponsiveTable";
import Skeleton from "@saleor/components/Skeleton";
import TableCellAvatar from "@saleor/components/TableCellAvatar";
import useSearchQuery from "@saleor/hooks/useSearchQuery";
import { buttonMessages } from "@saleor/intl";
import { renderCollection } from "@saleor/misc";
import { SearchProducts_search_edges_node } from "@saleor/searches/types/SearchProducts";
import { ShippingPriceExcludeProduct } from "@saleor/shipping/types/ShippingPriceExcludeProduct";
import { FetchMoreProps } from "@saleor/types";
import React from "react";
import { MutationFetchResult } from "react-apollo";
import InfiniteScroll from "react-infinite-scroller";
import { FormattedMessage, useIntl } from "react-intl";
const useStyles = makeStyles(
theme => ({
avatar: {
paddingLeft: 0,
width: 64
},
colName: {
paddingLeft: 0
},
content: {
overflowY: "scroll"
},
loadMoreLoaderContainer: {
alignItems: "center",
display: "flex",
height: theme.spacing(3),
justifyContent: "center",
marginTop: theme.spacing(3)
},
overflow: {
overflowY: "visible"
},
productCheckboxCell: {
"&:first-child": {
paddingLeft: 0,
paddingRight: 0
}
}
}),
{ name: "ShippingMethodProductsAddDialog" }
);
export interface ShippingMethodProductsAddDialogProps extends FetchMoreProps {
confirmButtonState: ConfirmButtonTransitionState;
open: boolean;
products: SearchProducts_search_edges_node[];
onClose: () => void;
onFetch: (query: string) => void;
onSubmit: (
ids: string[]
) => Promise<MutationFetchResult<ShippingPriceExcludeProduct>>;
}
const handleProductAssign = (
product: SearchProducts_search_edges_node,
isSelected: boolean,
selectedProducts: SearchProducts_search_edges_node[],
setSelectedProducts: (data: SearchProducts_search_edges_node[]) => void
) => {
if (isSelected) {
setSelectedProducts(
selectedProducts.filter(
selectedProduct => selectedProduct.id !== product.id
)
);
} else {
setSelectedProducts([...selectedProducts, product]);
}
};
const ShippingMethodProductsAddDialog: React.FC<ShippingMethodProductsAddDialogProps> = props => {
const {
confirmButtonState,
open,
loading,
hasMore,
products,
onFetch,
onFetchMore,
onClose,
onSubmit
} = props;
const classes = useStyles(props);
const intl = useIntl();
const [query, onQueryChange, resetQuery] = useSearchQuery(onFetch);
const [selectedProducts, setSelectedProducts] = React.useState<
SearchProducts_search_edges_node[]
>([]);
const handleSubmit = () => {
onSubmit(selectedProducts.map(product => product.id)).then(() => {
setSelectedProducts([]);
resetQuery();
});
};
const handleClose = () => {
onClose();
setSelectedProducts([]);
resetQuery();
};
return (
<Dialog
onClose={handleClose}
open={open}
classes={{ paper: classes.overflow }}
fullWidth
maxWidth="sm"
>
<DialogTitle>
<FormattedMessage
defaultMessage="Assign Products"
description="dialog header"
/>
</DialogTitle>
<DialogContent className={classes.overflow}>
<TextField
name="query"
value={query}
onChange={onQueryChange}
label={intl.formatMessage({
defaultMessage: "Search Products"
})}
placeholder={intl.formatMessage({
defaultMessage: "Search Products"
})}
fullWidth
InputProps={{
autoComplete: "off",
endAdornment: loading && <CircularProgress size={16} />
}}
/>
</DialogContent>
<DialogContent className={classes.content}>
<InfiniteScroll
pageStart={0}
loadMore={onFetchMore}
hasMore={hasMore}
useWindow={false}
loader={
<div key="loader" className={classes.loadMoreLoaderContainer}>
<CircularProgress size={16} />
</div>
}
threshold={10}
>
<ResponsiveTable key="table">
<TableBody>
{renderCollection(
products,
(product, productIndex) => {
const isSelected = selectedProducts.some(
selectedProduct => selectedProduct.id === product.id
);
return (
<React.Fragment
key={product ? product.id : `skeleton-${productIndex}`}
>
<TableRow>
<TableCell
padding="checkbox"
className={classes.productCheckboxCell}
>
{product && (
<Checkbox
checked={isSelected}
disabled={loading}
onChange={() =>
handleProductAssign(
product,
isSelected,
selectedProducts,
setSelectedProducts
)
}
/>
)}
</TableCell>
<TableCellAvatar
className={classes.avatar}
thumbnail={product?.thumbnail?.url}
/>
<TableCell className={classes.colName} colSpan={2}>
{product?.name || <Skeleton />}
</TableCell>
</TableRow>
</React.Fragment>
);
},
() => (
<TableRow>
<TableCell colSpan={4}>
<FormattedMessage defaultMessage="No products matching given query" />
</TableCell>
</TableRow>
)
)}
</TableBody>
</ResponsiveTable>
</InfiniteScroll>
</DialogContent>
<DialogActions>
<Button onClick={handleClose}>
<FormattedMessage {...buttonMessages.back} />
</Button>
<ConfirmButton
transitionState={confirmButtonState}
color="primary"
variant="contained"
type="submit"
disabled={loading || !selectedProducts?.length}
onClick={handleSubmit}
>
<FormattedMessage {...buttonMessages.confirm} />
</ConfirmButton>
</DialogActions>
</Dialog>
);
};
ShippingMethodProductsAddDialog.displayName = "ShippingMethodProductsAddDialog";
export default ShippingMethodProductsAddDialog;

View file

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

View file

@ -0,0 +1,87 @@
import Decorator from "@saleor/storybook//Decorator";
import { ShippingMethodTypeEnum } from "@saleor/types/globalTypes";
import { storiesOf } from "@storybook/react";
import React from "react";
import ShippingZoneRatesCreatePage, {
ShippingZoneRatesCreatePageProps
} from "./ShippingZoneRatesCreatePage";
const channels = [
{
currency: "USD",
id: "1",
maxValue: "10",
minValue: "0",
name: "channel",
price: "5"
},
{
currency: "USD",
id: "2",
maxValue: "20",
minValue: "1",
name: "test",
price: "6"
}
];
const defaultChannels = [
{
currency: "USD",
id: "1",
maxValue: "",
minValue: "",
name: "channel",
price: ""
}
];
const props: ShippingZoneRatesCreatePageProps = {
allChannelsCount: 3,
channelErrors: [],
disabled: false,
errors: [],
onBack: () => undefined,
onChannelsChange: () => undefined,
onDelete: () => undefined,
onSubmit: () => undefined,
onZipCodeAssign: () => undefined,
onZipCodeUnassign: () => undefined,
openChannelsModal: () => undefined,
saveButtonBarState: "default",
shippingChannels: defaultChannels,
variant: ShippingMethodTypeEnum.PRICE,
zipCodes: [
{
__typename: "ShippingMethodZipCodeRule",
end: "51-200",
id: "1",
start: "51-220"
},
{
__typename: "ShippingMethodZipCodeRule",
end: "31-101",
id: "1",
start: "44-205"
}
]
};
storiesOf("Shipping / ShippingZoneRatesCreatePage page", module)
.addDecorator(Decorator)
.add("create price", () => <ShippingZoneRatesCreatePage {...props} />)
.add("loading", () => (
<ShippingZoneRatesCreatePage
{...props}
disabled={true}
saveButtonBarState={"loading"}
/>
))
.add("create weight", () => (
<ShippingZoneRatesCreatePage
{...props}
shippingChannels={channels}
variant={ShippingMethodTypeEnum.WEIGHT}
/>
));

View file

@ -0,0 +1,190 @@
import { ChannelShippingData } from "@saleor/channels/utils";
import AppHeader from "@saleor/components/AppHeader";
import CardSpacer from "@saleor/components/CardSpacer";
import ChannelsAvailability from "@saleor/components/ChannelsAvailability";
import { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton";
import Container from "@saleor/components/Container";
import Form from "@saleor/components/Form";
import Grid from "@saleor/components/Grid";
import PageHeader from "@saleor/components/PageHeader";
import SaveButtonBar from "@saleor/components/SaveButtonBar";
import { ShippingChannelsErrorFragment } from "@saleor/fragments/types/ShippingChannelsErrorFragment";
import { ShippingErrorFragment } from "@saleor/fragments/types/ShippingErrorFragment";
import { ShippingMethodFragment_zipCodeRules } from "@saleor/fragments/types/ShippingMethodFragment";
import { validatePrice } from "@saleor/products/utils/validation";
import OrderValue from "@saleor/shipping/components/OrderValue";
import OrderWeight from "@saleor/shipping/components/OrderWeight";
import PricingCard from "@saleor/shipping/components/PricingCard";
import ShippingZoneInfo from "@saleor/shipping/components/ShippingZoneInfo";
import { createChannelsChangeHandler } from "@saleor/shipping/handlers";
import { ShippingMethodTypeEnum } from "@saleor/types/globalTypes";
import React from "react";
import { FormattedMessage, useIntl } from "react-intl";
import ShippingZoneZipCodes, {
ZipCodeInclusion
} from "../ShippingZoneZipCodes";
export interface FormData {
channelListings: ChannelShippingData[];
includeZipCodes: ZipCodeInclusion;
name: string;
noLimits: boolean;
minValue: string;
maxValue: string;
type: ShippingMethodTypeEnum;
}
export interface ShippingZoneRatesCreatePageProps {
allChannelsCount?: number;
shippingChannels: ChannelShippingData[];
disabled: boolean;
hasChannelChanged?: boolean;
zipCodes?: ShippingMethodFragment_zipCodeRules[];
channelErrors: ShippingChannelsErrorFragment[];
errors: ShippingErrorFragment[];
saveButtonBarState: ConfirmButtonTransitionState;
onBack: () => void;
onDelete?: () => void;
onSubmit: (data: FormData) => void;
onZipCodeAssign: () => void;
onZipCodeUnassign: (id: string) => void;
onChannelsChange: (data: ChannelShippingData[]) => void;
openChannelsModal: () => void;
variant: ShippingMethodTypeEnum;
}
export const ShippingZoneRatesCreatePage: React.FC<ShippingZoneRatesCreatePageProps> = ({
allChannelsCount,
shippingChannels,
channelErrors,
disabled,
errors,
hasChannelChanged,
onBack,
onDelete,
onSubmit,
onChannelsChange,
onZipCodeAssign,
onZipCodeUnassign,
openChannelsModal,
saveButtonBarState,
variant,
zipCodes
}) => {
const intl = useIntl();
const isPriceVariant = variant === ShippingMethodTypeEnum.PRICE;
const initialForm: FormData = {
channelListings: shippingChannels,
includeZipCodes: ZipCodeInclusion.Include,
maxValue: "",
minValue: "",
name: "",
noLimits: false,
type: null
};
return (
<Form initial={initialForm} onSubmit={onSubmit}>
{({ change, data, hasChanged, submit, triggerChange }) => {
const handleChannelsChange = createChannelsChangeHandler(
shippingChannels,
onChannelsChange,
triggerChange
);
const formDisabled = data.channelListings?.some(channel =>
validatePrice(channel.price)
);
return (
<Container>
<AppHeader onBack={onBack}>
<FormattedMessage defaultMessage="Shipping" />
</AppHeader>
<PageHeader
title={
isPriceVariant
? intl.formatMessage({
defaultMessage: "Price Rate Create",
description: "page title"
})
: intl.formatMessage({
defaultMessage: "Weight Rate Create",
description: "page title"
})
}
/>
<Grid>
<div>
<ShippingZoneInfo
data={data}
disabled={disabled}
errors={errors}
onChange={change}
/>
<CardSpacer />
{isPriceVariant ? (
<OrderValue
channels={data.channelListings}
errors={channelErrors}
noLimits={data.noLimits}
disabled={disabled}
onChange={change}
onChannelsChange={handleChannelsChange}
/>
) : (
<OrderWeight
noLimits={data.noLimits}
disabled={disabled}
minValue={data.minValue}
maxValue={data.maxValue}
onChange={change}
errors={errors}
/>
)}
<CardSpacer />
<PricingCard
channels={data.channelListings}
onChange={handleChannelsChange}
disabled={disabled}
errors={channelErrors}
/>
<CardSpacer />
<ShippingZoneZipCodes
data={data}
disabled={disabled}
onZipCodeDelete={onZipCodeUnassign}
onZipCodeInclusionChange={() => undefined}
onZipCodeRangeAdd={onZipCodeAssign}
zipCodes={zipCodes}
/>
</div>
<div>
<ChannelsAvailability
allChannelsCount={allChannelsCount}
selectedChannelsCount={shippingChannels?.length}
channelsList={data.channelListings.map(channel => ({
id: channel.id,
name: channel.name
}))}
openModal={openChannelsModal}
/>
</div>
</Grid>
<SaveButtonBar
disabled={
disabled || formDisabled || (!hasChanged && !hasChannelChanged)
}
onCancel={onBack}
onDelete={onDelete}
onSave={submit}
state={saveButtonBarState}
/>
</Container>
);
}}
</Form>
);
};
export default ShippingZoneRatesCreatePage;

View file

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

View file

@ -43,31 +43,26 @@ const props: ShippingZoneRatesPageProps = {
channelErrors: [],
disabled: false,
errors: [],
isChecked: () => undefined,
onBack: () => undefined,
onChannelsChange: () => undefined,
onDelete: () => undefined,
onNextPage: () => undefined,
onPreviousPage: () => undefined,
onProductAssign: () => undefined,
onProductUnassign: () => undefined,
onSubmit: () => undefined,
onZipCodeAssign: () => undefined,
onZipCodeUnassign: () => undefined,
openChannelsModal: () => undefined,
rate: null,
rate: shippingZone.shippingMethods[0],
saveButtonBarState: "default",
selected: 0,
shippingChannels: defaultChannels,
variant: ShippingMethodTypeEnum.PRICE,
zipCodes: [
{
__typename: "ShippingMethodZipCodeRule",
end: "51-200",
id: "1",
start: "51-220"
},
{
__typename: "ShippingMethodZipCodeRule",
end: "31-101",
id: "1",
start: "44-205"
}
]
toggle: () => undefined,
toggleAll: () => undefined,
toolbar: () => undefined,
variant: ShippingMethodTypeEnum.PRICE
};
storiesOf("Views / Shipping / Shipping rate", module)
@ -95,10 +90,6 @@ storiesOf("Views / Shipping / Shipping rate", module)
<ShippingZoneRatesPage
{...props}
shippingChannels={channels}
rate={shippingZone.shippingMethods[0]}
variant={ShippingMethodTypeEnum.WEIGHT}
/>
))
.add("no zip codes", () => (
<ShippingZoneRatesPage {...props} zipCodes={[]} />
));

View file

@ -10,19 +10,18 @@ import PageHeader from "@saleor/components/PageHeader";
import SaveButtonBar from "@saleor/components/SaveButtonBar";
import { ShippingChannelsErrorFragment } from "@saleor/fragments/types/ShippingChannelsErrorFragment";
import { ShippingErrorFragment } from "@saleor/fragments/types/ShippingErrorFragment";
import {
ShippingMethodFragment,
ShippingMethodFragment_zipCodeRules
} from "@saleor/fragments/types/ShippingMethodFragment";
import { validatePrice } from "@saleor/products/utils/validation";
import OrderValue from "@saleor/shipping/components/OrderValue";
import OrderWeight from "@saleor/shipping/components/OrderWeight";
import PricingCard from "@saleor/shipping/components/PricingCard";
import ShippingMethodProducts from "@saleor/shipping/components/ShippingMethodProducts";
import ShippingZoneInfo from "@saleor/shipping/components/ShippingZoneInfo";
import { createChannelsChangeHandler } from "@saleor/shipping/handlers";
import { ShippingZone_shippingZone_shippingMethods } from "@saleor/shipping/types/ShippingZone";
import { ListActions, ListProps } from "@saleor/types";
import { ShippingMethodTypeEnum } from "@saleor/types/globalTypes";
import React from "react";
import { FormattedMessage, useIntl } from "react-intl";
import { FormattedMessage } from "react-intl";
import ShippingZoneZipCodes, {
ZipCodeInclusion
@ -38,13 +37,14 @@ export interface FormData {
type: ShippingMethodTypeEnum;
}
export interface ShippingZoneRatesPageProps {
export interface ShippingZoneRatesPageProps
extends Pick<ListProps, Exclude<keyof ListProps, "onRowClick">>,
ListActions {
allChannelsCount?: number;
shippingChannels: ChannelShippingData[];
disabled: boolean;
hasChannelChanged?: boolean;
rate: ShippingMethodFragment | null;
zipCodes?: ShippingMethodFragment_zipCodeRules[];
rate: ShippingZone_shippingZone_shippingMethods;
channelErrors: ShippingChannelsErrorFragment[];
errors: ShippingErrorFragment[];
saveButtonBarState: ConfirmButtonTransitionState;
@ -55,6 +55,8 @@ export interface ShippingZoneRatesPageProps {
onZipCodeUnassign: (id: string) => void;
onChannelsChange: (data: ChannelShippingData[]) => void;
openChannelsModal: () => void;
onProductAssign: () => void;
onProductUnassign: (ids: string[]) => void;
variant: ShippingMethodTypeEnum;
}
@ -71,13 +73,14 @@ export const ShippingZoneRatesPage: React.FC<ShippingZoneRatesPageProps> = ({
onChannelsChange,
onZipCodeAssign,
onZipCodeUnassign,
onProductAssign,
onProductUnassign,
openChannelsModal,
rate,
saveButtonBarState,
variant,
zipCodes
...listProps
}) => {
const intl = useIntl();
const isPriceVariant = variant === ShippingMethodTypeEnum.PRICE;
const initialForm: FormData = {
channelListings: shippingChannels,
@ -89,8 +92,6 @@ export const ShippingZoneRatesPage: React.FC<ShippingZoneRatesPageProps> = ({
type: rate?.type || null
};
const rateExists = rate !== null;
return (
<Form initial={initialForm} onSubmit={onSubmit}>
{({ change, data, hasChanged, submit, triggerChange }) => {
@ -108,20 +109,7 @@ export const ShippingZoneRatesPage: React.FC<ShippingZoneRatesPageProps> = ({
<AppHeader onBack={onBack}>
<FormattedMessage defaultMessage="Shipping" />
</AppHeader>
<PageHeader
title={
rate?.name ||
(isPriceVariant
? intl.formatMessage({
defaultMessage: "Price Rate Create",
description: "page title"
})
: intl.formatMessage({
defaultMessage: "Weight Rate Create",
description: "page title"
}))
}
/>
<PageHeader title={rate?.name} />
<Grid>
<div>
<ShippingZoneInfo
@ -164,7 +152,17 @@ export const ShippingZoneRatesPage: React.FC<ShippingZoneRatesPageProps> = ({
onZipCodeDelete={onZipCodeUnassign}
onZipCodeInclusionChange={() => undefined}
onZipCodeRangeAdd={onZipCodeAssign}
zipCodes={rateExists ? rate?.zipCodeRules : zipCodes}
zipCodes={rate?.zipCodeRules}
/>
<CardSpacer />
<ShippingMethodProducts
products={rate?.excludedProducts?.edges.map(
edge => edge.node
)}
onProductAssign={onProductAssign}
onProductUnassign={onProductUnassign}
disabled={disabled}
{...listProps}
/>
</div>
<div>

View file

@ -0,0 +1,17 @@
import Decorator from "@saleor/storybook//Decorator";
import { storiesOf } from "@storybook/react";
import React from "react";
import UnassignDialog, { UnassignDialogProps } from "./UnassignDialog";
const props: UnassignDialogProps = {
closeModal: () => undefined,
confirmButtonState: "default",
idsLength: 2,
onConfirm: () => undefined,
open: true
};
storiesOf("Shipping / UnassignDialog", module)
.addDecorator(Decorator)
.add("default", () => <UnassignDialog {...props} />);

View file

@ -0,0 +1,48 @@
import DialogContentText from "@material-ui/core/DialogContentText";
import ActionDialog from "@saleor/components/ActionDialog";
import { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton";
import React from "react";
import { FormattedMessage, useIntl } from "react-intl";
export interface UnassignDialogProps {
open: boolean;
confirmButtonState: ConfirmButtonTransitionState;
idsLength: number;
closeModal: () => void;
onConfirm: () => void;
}
export const UnassignDialog: React.FC<UnassignDialogProps> = ({
closeModal,
confirmButtonState,
idsLength,
onConfirm,
open
}) => {
const intl = useIntl();
return (
<ActionDialog
open={open}
title={intl.formatMessage({
defaultMessage: "Unassign Products From Shipping",
description: "dialog header"
})}
confirmButtonState={confirmButtonState}
onClose={closeModal}
onConfirm={onConfirm}
>
<DialogContentText>
<FormattedMessage
defaultMessage="{counter,plural,one{Are you sure you want to unassign this product?} other{Are you sure you want to unassign {displayQuantity} products?}}"
description="dialog content"
values={{
counter: idsLength,
displayQuantity: <strong>{idsLength}</strong>
}}
/>
</DialogContentText>
</ActionDialog>
);
};
export default UnassignDialog;

View file

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

View file

@ -1,5 +1,6 @@
import { ShippingZoneDetailsFragment } from "@saleor/fragments/types/ShippingZoneDetailsFragment";
import { ShippingZoneFragment } from "@saleor/fragments/types/ShippingZoneFragment";
import { SearchProducts_search_edges_node } from "@saleor/searches/types/SearchProducts";
import { ShippingZone_shippingZone } from "@saleor/shipping/types/ShippingZone";
import { ShippingMethodTypeEnum, WeightUnitsEnum } from "../types/globalTypes";
@ -1285,7 +1286,7 @@ export const shippingZones: ShippingZoneFragment[] = [
}
];
export const shippingZone: ShippingZoneDetailsFragment = {
export const shippingZone: ShippingZone_shippingZone = {
__typename: "ShippingZone",
countries: [
{
@ -1577,6 +1578,30 @@ export const shippingZone: ShippingZoneDetailsFragment = {
}
}
],
excludedProducts: {
__typename: "ProductCountableConnection",
edges: [
{
__typename: "ProductCountableEdge",
node: {
__typename: "Product",
id: "1",
name: "Apple Juice",
thumbnail: {
__typename: "Image",
url: ""
}
}
}
],
pageInfo: {
__typename: "PageInfo",
endCursor: "",
hasNextPage: false,
hasPreviousPage: false,
startCursor: ""
}
},
id: "U2hpcHBpbmdNZXRob2Q6NA==",
maximumOrderWeight: {
__typename: "Weight",
@ -1614,6 +1639,30 @@ export const shippingZone: ShippingZoneDetailsFragment = {
{
__typename: "ShippingMethod",
channelListings: [],
excludedProducts: {
__typename: "ProductCountableConnection",
edges: [
{
__typename: "ProductCountableEdge",
node: {
__typename: "Product",
id: "1",
name: "Apple Juice",
thumbnail: {
__typename: "Image",
url: ""
}
}
}
],
pageInfo: {
__typename: "PageInfo",
endCursor: "",
hasNextPage: false,
hasPreviousPage: false,
startCursor: ""
}
},
id: "U2hpcHBpbmdNZXRob2Q6Mw==",
maximumOrderWeight: null,
minimumOrderWeight: {
@ -1647,6 +1696,30 @@ export const shippingZone: ShippingZoneDetailsFragment = {
{
__typename: "ShippingMethod",
channelListings: [],
excludedProducts: {
__typename: "ProductCountableConnection",
edges: [
{
__typename: "ProductCountableEdge",
node: {
__typename: "Product",
id: "1",
name: "Apple Juice",
thumbnail: {
__typename: "Image",
url: ""
}
}
}
],
pageInfo: {
__typename: "PageInfo",
endCursor: "",
hasNextPage: false,
hasPreviousPage: false,
startCursor: ""
}
},
id: "U2hpcHBpbmdNZXRob2Q6Mg==",
maximumOrderWeight: null,
minimumOrderWeight: {
@ -1680,6 +1753,17 @@ export const shippingZone: ShippingZoneDetailsFragment = {
{
__typename: "ShippingMethod",
channelListings: [],
excludedProducts: {
__typename: "ProductCountableConnection",
edges: [],
pageInfo: {
__typename: "PageInfo",
endCursor: "",
hasNextPage: false,
hasPreviousPage: false,
startCursor: ""
}
},
id: "U2hpcHBpbmdNZXRob2Q6MQ==",
maximumOrderWeight: null,
minimumOrderWeight: {
@ -1724,3 +1808,24 @@ export const shippingZone: ShippingZoneDetailsFragment = {
}
]
};
export const products: SearchProducts_search_edges_node[] = [
{
__typename: "Product",
id: "1",
name: "Apple Juice",
thumbnail: {
__typename: "Image",
url: ""
}
},
{
__typename: "Product",
id: "2",
name: "Banana Juice",
thumbnail: {
__typename: "Image",
url: ""
}
}
];

View file

@ -47,6 +47,14 @@ import {
ShippingMethodZipCodeRangeUnassign,
ShippingMethodZipCodeRangeUnassignVariables
} from "./types/ShippingMethodZipCodeRangeUnassign";
import {
ShippingPriceExcludeProduct,
ShippingPriceExcludeProductVariables
} from "./types/ShippingPriceExcludeProduct";
import {
ShippingPriceRemoveProductFromExclude,
ShippingPriceRemoveProductFromExcludeVariables
} from "./types/ShippingPriceRemoveProductFromExclude";
import {
UpdateDefaultWeightUnit,
UpdateDefaultWeightUnitVariables
@ -297,3 +305,38 @@ export const useShippingMethodZipCodeRangeUnassign = makeMutation<
ShippingMethodZipCodeRangeUnassign,
ShippingMethodZipCodeRangeUnassignVariables
>(shippingMethodZipCodeRulesDelete);
export const shippingPriceExcludeProducts = gql`
${shippingErrorFragment}
mutation ShippingPriceExcludeProduct(
$id: ID!
$input: ShippingPriceExcludeProductsInput!
) {
shippingPriceExcludeProducts(id: $id, input: $input) {
errors: shippingErrors {
...ShippingErrorFragment
}
}
}
`;
export const useShippingPriceExcludeProduct = makeMutation<
ShippingPriceExcludeProduct,
ShippingPriceExcludeProductVariables
>(shippingPriceExcludeProducts);
export const shippingPriceRemoveProductsFromExclude = gql`
${shippingErrorFragment}
mutation ShippingPriceRemoveProductFromExclude($id: ID!, $products: [ID]!) {
shippingPriceRemoveProductFromExclude(id: $id, products: $products) {
errors: shippingErrors {
...ShippingErrorFragment
}
}
}
`;
export const useShippingPriceRemoveProductsFromExclude = makeMutation<
ShippingPriceRemoveProductFromExclude,
ShippingPriceRemoveProductFromExcludeVariables
>(shippingPriceRemoveProductsFromExclude);

View file

@ -1,6 +1,6 @@
import { pageInfoFragment } from "@saleor/fragments/pageInfo";
import {
shippingZoneDetailsFragment,
shippingMethodWithExcludedProductsFragment,
shippingZoneFragment
} from "@saleor/fragments/shipping";
import makeQuery from "@saleor/hooks/makeQuery";
@ -36,10 +36,25 @@ export const useShippingZoneList = makeQuery<
>(shippingZones);
const shippingZone = gql`
${shippingZoneDetailsFragment}
query ShippingZone($id: ID!) {
${shippingZoneFragment}
${shippingMethodWithExcludedProductsFragment}
query ShippingZone(
$id: ID!
$before: String
$after: String
$first: Int
$last: Int
) {
shippingZone(id: $id) {
...ShippingZoneDetailsFragment
...ShippingZoneFragment
default
shippingMethods {
...ShippingMethodWithExcludedProductsFragment
}
warehouses {
id
name
}
}
}
`;

View file

@ -0,0 +1,29 @@
/* tslint:disable */
/* eslint-disable */
// This file was automatically generated and should not be edited.
import { ShippingPriceExcludeProductsInput, ShippingErrorCode } from "./../../types/globalTypes";
// ====================================================
// GraphQL mutation operation: ShippingPriceExcludeProduct
// ====================================================
export interface ShippingPriceExcludeProduct_shippingPriceExcludeProducts_errors {
__typename: "ShippingError";
code: ShippingErrorCode;
field: string | null;
}
export interface ShippingPriceExcludeProduct_shippingPriceExcludeProducts {
__typename: "ShippingPriceExcludeProducts";
errors: ShippingPriceExcludeProduct_shippingPriceExcludeProducts_errors[];
}
export interface ShippingPriceExcludeProduct {
shippingPriceExcludeProducts: ShippingPriceExcludeProduct_shippingPriceExcludeProducts | null;
}
export interface ShippingPriceExcludeProductVariables {
id: string;
input: ShippingPriceExcludeProductsInput;
}

View file

@ -0,0 +1,29 @@
/* tslint:disable */
/* eslint-disable */
// This file was automatically generated and should not be edited.
import { ShippingErrorCode } from "./../../types/globalTypes";
// ====================================================
// GraphQL mutation operation: ShippingPriceRemoveProductFromExclude
// ====================================================
export interface ShippingPriceRemoveProductFromExclude_shippingPriceRemoveProductFromExclude_errors {
__typename: "ShippingError";
code: ShippingErrorCode;
field: string | null;
}
export interface ShippingPriceRemoveProductFromExclude_shippingPriceRemoveProductFromExclude {
__typename: "ShippingPriceRemoveProductFromExclude";
errors: ShippingPriceRemoveProductFromExclude_shippingPriceRemoveProductFromExclude_errors[];
}
export interface ShippingPriceRemoveProductFromExclude {
shippingPriceRemoveProductFromExclude: ShippingPriceRemoveProductFromExclude_shippingPriceRemoveProductFromExclude | null;
}
export interface ShippingPriceRemoveProductFromExcludeVariables {
id: string;
products: (string | null)[];
}

View file

@ -67,6 +67,37 @@ export interface ShippingZone_shippingZone_shippingMethods_channelListings {
maximumOrderPrice: ShippingZone_shippingZone_shippingMethods_channelListings_maximumOrderPrice | null;
}
export interface ShippingZone_shippingZone_shippingMethods_excludedProducts_pageInfo {
__typename: "PageInfo";
hasNextPage: boolean;
hasPreviousPage: boolean;
endCursor: string | null;
startCursor: string | null;
}
export interface ShippingZone_shippingZone_shippingMethods_excludedProducts_edges_node_thumbnail {
__typename: "Image";
url: string;
}
export interface ShippingZone_shippingZone_shippingMethods_excludedProducts_edges_node {
__typename: "Product";
id: string;
name: string;
thumbnail: ShippingZone_shippingZone_shippingMethods_excludedProducts_edges_node_thumbnail | null;
}
export interface ShippingZone_shippingZone_shippingMethods_excludedProducts_edges {
__typename: "ProductCountableEdge";
node: ShippingZone_shippingZone_shippingMethods_excludedProducts_edges_node;
}
export interface ShippingZone_shippingZone_shippingMethods_excludedProducts {
__typename: "ProductCountableConnection";
pageInfo: ShippingZone_shippingZone_shippingMethods_excludedProducts_pageInfo;
edges: ShippingZone_shippingZone_shippingMethods_excludedProducts_edges[];
}
export interface ShippingZone_shippingZone_shippingMethods {
__typename: "ShippingMethod";
id: string;
@ -76,6 +107,7 @@ export interface ShippingZone_shippingZone_shippingMethods {
name: string;
type: ShippingMethodTypeEnum | null;
channelListings: ShippingZone_shippingZone_shippingMethods_channelListings[] | null;
excludedProducts: ShippingZone_shippingZone_shippingMethods_excludedProducts | null;
}
export interface ShippingZone_shippingZone_warehouses {
@ -100,4 +132,8 @@ export interface ShippingZone {
export interface ShippingZoneVariables {
id: string;
before?: string | null;
after?: string | null;
first?: number | null;
last?: number | null;
}

View file

@ -26,20 +26,30 @@ export type ShippingZoneUrlDialog =
| "remove"
| "remove-rate"
| "unassign-country";
export type ShippingMethodActions = "assign-product" | "unassign-product";
export type ShippingZoneUrlQueryParams = Dialog<ShippingZoneUrlDialog> &
SingleAction &
Partial<{
type: ShippingMethodTypeEnum;
}>;
}> &
Pagination;
export const shippingZoneUrl = (
id: string,
params?: ShippingZoneUrlQueryParams
) => shippingZonePath(encodeURIComponent(id)) + "?" + stringifyQs(params);
type ZipCodeRangeActions = "add-range" | "remove-range";
export type ShippingRateUrlDialog = ZipCodeRangeActions | "remove";
export type ShippingRateUrlDialog =
| ZipCodeRangeActions
| "remove"
| ShippingMethodActions;
export type ShippingRateUrlQueryParams = Dialog<ShippingRateUrlDialog> &
SingleAction;
SingleAction &
BulkAction &
Pagination;
export type ShippingRateCreateUrlDialog = ZipCodeRangeActions;
export type ShippingRateCreateUrlQueryParams = Dialog<
ShippingRateCreateUrlDialog

View file

@ -7,7 +7,7 @@ import useChannels from "@saleor/hooks/useChannels";
import useNavigator from "@saleor/hooks/useNavigator";
import { sectionNames } from "@saleor/intl";
import ShippingRateZipCodeRangeRemoveDialog from "@saleor/shipping/components/ShippingRateZipCodeRangeRemoveDialog";
import ShippingZoneRatesPage from "@saleor/shipping/components/ShippingZoneRatesPage";
import ShippingZoneRatesCreatePage from "@saleor/shipping/components/ShippingZoneRatesCreatePage";
import ShippingZoneZipCodeRangeDialog from "@saleor/shipping/components/ShippingZoneZipCodeRangeDialog";
import { useShippingRateCreator } from "@saleor/shipping/handlers";
import {
@ -114,7 +114,7 @@ export const PriceRatesCreate: React.FC<PriceRatesCreateProps> = ({
/>
)}
<ShippingZoneRatesPage
<ShippingZoneRatesCreatePage
allChannelsCount={allChannels?.length}
shippingChannels={currentChannels}
disabled={channelsLoading || status === "loading"}
@ -123,7 +123,6 @@ export const PriceRatesCreate: React.FC<PriceRatesCreateProps> = ({
onBack={handleBack}
errors={errors}
channelErrors={channelErrors}
rate={null}
zipCodes={zipCodes}
openChannelsModal={handleChannelsModalOpen}
onChannelsChange={setCurrentChannels}

View file

@ -1,3 +1,4 @@
import Button from "@material-ui/core/Button";
import { useChannelsList } from "@saleor/channels/queries";
import {
createShippingChannelsFromRate,
@ -5,17 +6,26 @@ import {
} from "@saleor/channels/utils";
import ChannelsAvailabilityDialog from "@saleor/components/ChannelsAvailabilityDialog";
import { WindowTitle } from "@saleor/components/WindowTitle";
import { DEFAULT_INITIAL_SEARCH_DATA } from "@saleor/config";
import { PAGINATE_BY } from "@saleor/config";
import useBulkActions from "@saleor/hooks/useBulkActions";
import useChannels from "@saleor/hooks/useChannels";
import useNavigator from "@saleor/hooks/useNavigator";
import useNotifier from "@saleor/hooks/useNotifier";
import usePaginator, {
createPaginationState
} from "@saleor/hooks/usePaginator";
import { sectionNames } from "@saleor/intl";
import { commonMessages } from "@saleor/intl";
import useProductSearch from "@saleor/searches/useProductSearch";
import DeleteShippingRateDialog from "@saleor/shipping/components/DeleteShippingRateDialog";
import ShippingMethodProductsAddDialog from "@saleor/shipping/components/ShippingMethodProductsAddDialog";
import ShippingRateZipCodeRangeRemoveDialog from "@saleor/shipping/components/ShippingRateZipCodeRangeRemoveDialog";
import ShippingZoneRatesPage, {
FormData
} from "@saleor/shipping/components/ShippingZoneRatesPage";
import ShippingZoneZipCodeRangeDialog from "@saleor/shipping/components/ShippingZoneZipCodeRangeDialog";
import UnassignDialog from "@saleor/shipping/components/UnassignDialog";
import {
getShippingMethodChannelVariables,
getUpdateShippingPriceRateVariables
@ -23,9 +33,9 @@ import {
import {
useShippingMethodChannelListingUpdate,
useShippingMethodZipCodeRangeAssign,
useShippingMethodZipCodeRangeUnassign
} from "@saleor/shipping/mutations";
import {
useShippingMethodZipCodeRangeUnassign,
useShippingPriceExcludeProduct,
useShippingPriceRemoveProductsFromExclude,
useShippingRateDelete,
useShippingRateUpdate
} from "@saleor/shipping/mutations";
@ -39,7 +49,7 @@ import {
import { ShippingMethodTypeEnum } from "@saleor/types/globalTypes";
import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers";
import React from "react";
import { useIntl } from "react-intl";
import { FormattedMessage, useIntl } from "react-intl";
export interface PriceRatesUpdateProps {
id: string;
@ -55,11 +65,19 @@ export const PriceRatesUpdate: React.FC<PriceRatesUpdateProps> = ({
const navigate = useNavigator();
const notify = useNotifier();
const intl = useIntl();
const paginate = usePaginator();
const { data, loading } = useShippingZone({
const paginationState = createPaginationState(PAGINATE_BY, params);
const { data, loading, refetch } = useShippingZone({
displayLoader: true,
variables: { id }
variables: { id, ...paginationState }
});
const {
loadMore,
search: productsSearch,
result: productsSearchOpts
} = useProductSearch({ variables: DEFAULT_INITIAL_SEARCH_DATA });
const [openModal, closeModal] = createDialogActionHandlers<
ShippingRateUrlDialog,
@ -69,6 +87,17 @@ export const PriceRatesUpdate: React.FC<PriceRatesUpdateProps> = ({
const rate = data?.shippingZone?.shippingMethods.find(
rate => rate.id === rateId
);
const { isSelected, listElements, reset, toggle, toggleAll } = useBulkActions(
[]
);
const { loadNextPage, loadPreviousPage, pageInfo } = paginate(
rate?.excludedProducts.pageInfo,
paginationState,
params
);
const { data: channelsData } = useChannelsList({});
const [
@ -98,6 +127,7 @@ export const PriceRatesUpdate: React.FC<PriceRatesUpdateProps> = ({
}
}
});
const [
unassignZipCodeRange,
unassignZipCodeRangeOpts
@ -108,11 +138,32 @@ export const PriceRatesUpdate: React.FC<PriceRatesUpdateProps> = ({
status: "success",
text: intl.formatMessage(commonMessages.savedChanges)
});
}
}
});
const [
unassignProduct,
unassignProductOpts
] = useShippingPriceRemoveProductsFromExclude({
onCompleted: data => {
if (data.shippingPriceRemoveProductFromExclude.errors.length === 0) {
handleSuccess();
refetch();
closeModal();
}
}
});
const [assignProduct, assignProductOpts] = useShippingPriceExcludeProduct({
onCompleted: data => {
if (data.shippingPriceExcludeProducts.errors.length === 0) {
handleSuccess();
refetch();
closeModal();
}
}
});
const shippingChannels = createShippingChannelsFromRate(
rate?.channelListings
);
@ -168,6 +219,18 @@ export const PriceRatesUpdate: React.FC<PriceRatesUpdateProps> = ({
}
};
const handleProductAssign = (ids: string[]) =>
assignProduct({
variables: { id: rateId, input: { products: ids } }
});
const handleProductUnassign = (ids: string[]) => {
unassignProduct({
variables: { id: rateId, products: ids }
});
reset();
};
const handleBack = () => navigate(shippingZoneUrl(id));
return (
@ -203,13 +266,35 @@ export const PriceRatesUpdate: React.FC<PriceRatesUpdateProps> = ({
open={params.action === "remove"}
name={rate?.name}
/>
<UnassignDialog
open={params.action === "unassign-product" && !!listElements.length}
idsLength={listElements.length}
confirmButtonState={unassignProductOpts.status}
closeModal={closeModal}
onConfirm={() => handleProductUnassign(listElements)}
/>
<ShippingMethodProductsAddDialog
confirmButtonState={assignProductOpts.status}
loading={productsSearchOpts.loading}
open={params.action === "assign-product"}
hasMore={productsSearchOpts.data?.search?.pageInfo.hasNextPage}
products={productsSearchOpts.data?.search?.edges
.map(edge => edge.node)
.filter(suggestedProduct => suggestedProduct.id)}
onClose={closeModal}
onFetch={productsSearch}
onFetchMore={loadMore}
onSubmit={handleProductAssign}
/>
<ShippingZoneRatesPage
allChannelsCount={allChannels?.length}
shippingChannels={currentChannels}
disabled={
loading ||
updateShippingRateOpts?.status === "loading" ||
updateShippingMethodChannelListingOpts?.status === "loading"
updateShippingMethodChannelListingOpts?.status === "loading" ||
unassignProductOpts?.status === "loading" ||
assignProductOpts?.status === "loading"
}
hasChannelChanged={shippingChannels?.length !== currentChannels?.length}
saveButtonBarState={updateShippingRateOpts.status}
@ -224,7 +309,24 @@ export const PriceRatesUpdate: React.FC<PriceRatesUpdateProps> = ({
}
openChannelsModal={handleChannelsModalOpen}
onChannelsChange={setCurrentChannels}
onProductUnassign={handleProductUnassign}
onProductAssign={() => openModal("assign-product")}
variant={ShippingMethodTypeEnum.PRICE}
isChecked={isSelected}
selected={listElements.length}
toggle={toggle}
toggleAll={toggleAll}
onNextPage={loadNextPage}
onPreviousPage={loadPreviousPage}
pageInfo={pageInfo}
toolbar={
<Button color="primary" onClick={() => openModal("unassign-product")}>
<FormattedMessage
defaultMessage="Unassign"
description="unassign products from shipping method, button"
/>
</Button>
}
onZipCodeAssign={() => openModal("add-range")}
onZipCodeUnassign={id =>
openModal("remove-range", {

View file

@ -3,8 +3,10 @@ import ActionDialog from "@saleor/components/ActionDialog";
import useAppChannel from "@saleor/components/AppLayout/AppChannelContext";
import NotFoundPage from "@saleor/components/NotFoundPage";
import { DEFAULT_INITIAL_SEARCH_DATA } from "@saleor/config";
import { PAGINATE_BY } from "@saleor/config";
import useNavigator from "@saleor/hooks/useNavigator";
import useNotifier from "@saleor/hooks/useNotifier";
import { createPaginationState } from "@saleor/hooks/usePaginator";
import useShop from "@saleor/hooks/useShop";
import { commonMessages } from "@saleor/intl";
import useWarehouseSearch from "@saleor/searches/useWarehouseSearch";
@ -53,6 +55,8 @@ const ShippingZoneDetails: React.FC<ShippingZoneDetailsProps> = ({
const intl = useIntl();
const shop = useShop();
const paginationState = createPaginationState(PAGINATE_BY, params);
const { result: searchWarehousesOpts, loadMore, search } = useWarehouseSearch(
{
variables: DEFAULT_INITIAL_SEARCH_DATA
@ -61,7 +65,7 @@ const ShippingZoneDetails: React.FC<ShippingZoneDetailsProps> = ({
const { data, loading } = useShippingZone({
displayLoader: true,
variables: { id }
variables: { id, ...paginationState }
});
const { channel } = useAppChannel();

View file

@ -10,7 +10,7 @@ import useChannels from "@saleor/hooks/useChannels";
import useNavigator from "@saleor/hooks/useNavigator";
import { sectionNames } from "@saleor/intl";
import ShippingRateZipCodeRangeRemoveDialog from "@saleor/shipping/components/ShippingRateZipCodeRangeRemoveDialog";
import ShippingZoneRatesPage from "@saleor/shipping/components/ShippingZoneRatesPage";
import ShippingZoneRatesCreatePage from "@saleor/shipping/components/ShippingZoneRatesCreatePage";
import ShippingZoneZipCodeRangeDialog from "@saleor/shipping/components/ShippingZoneZipCodeRangeDialog";
import { useShippingRateCreator } from "@saleor/shipping/handlers";
import {
@ -117,7 +117,7 @@ export const WeightRatesCreate: React.FC<WeightRatesCreateProps> = ({
toggleAll={toggleAllChannels}
/>
)}
<ShippingZoneRatesPage
<ShippingZoneRatesCreatePage
allChannelsCount={allChannels?.length}
shippingChannels={currentChannels}
disabled={channelsLoading || status === "loading"}
@ -126,7 +126,6 @@ export const WeightRatesCreate: React.FC<WeightRatesCreateProps> = ({
onBack={handleBack}
errors={errors}
channelErrors={channelErrors}
rate={null}
zipCodes={zipCodes}
openChannelsModal={handleChannelsModalOpen}
onChannelsChange={setCurrentChannels}

View file

@ -1,3 +1,4 @@
import Button from "@material-ui/core/Button";
import { useChannelsList } from "@saleor/channels/queries";
import {
createShippingChannelsFromRate,
@ -5,28 +6,39 @@ import {
} from "@saleor/channels/utils";
import ChannelsAvailabilityDialog from "@saleor/components/ChannelsAvailabilityDialog";
import { WindowTitle } from "@saleor/components/WindowTitle";
import { DEFAULT_INITIAL_SEARCH_DATA } from "@saleor/config";
import { PAGINATE_BY } from "@saleor/config";
import useBulkActions from "@saleor/hooks/useBulkActions";
import useChannels from "@saleor/hooks/useChannels";
import useNavigator from "@saleor/hooks/useNavigator";
import useNotifier from "@saleor/hooks/useNotifier";
import usePaginator, {
createPaginationState
} from "@saleor/hooks/usePaginator";
import { sectionNames } from "@saleor/intl";
import { commonMessages } from "@saleor/intl";
import useProductSearch from "@saleor/searches/useProductSearch";
import DeleteShippingRateDialog from "@saleor/shipping/components/DeleteShippingRateDialog";
import ShippingMethodProductsAddDialog from "@saleor/shipping/components/ShippingMethodProductsAddDialog";
import ShippingRateZipCodeRangeRemoveDialog from "@saleor/shipping/components/ShippingRateZipCodeRangeRemoveDialog";
import ShippingZoneRatesPage, {
FormData
} from "@saleor/shipping/components/ShippingZoneRatesPage";
import ShippingZoneZipCodeRangeDialog from "@saleor/shipping/components/ShippingZoneZipCodeRangeDialog";
import UnassignDialog from "@saleor/shipping/components/UnassignDialog";
import {
getShippingMethodChannelVariables,
getUpdateShippingWeightRateVariables
} from "@saleor/shipping/handlers";
import {
useShippingMethodChannelListingUpdate,
useShippingMethodZipCodeRangeAssign,
useShippingMethodZipCodeRangeUnassign,
useShippingPriceExcludeProduct,
useShippingPriceRemoveProductsFromExclude,
useShippingRateDelete,
useShippingRateUpdate
} from "@saleor/shipping/mutations";
import { useShippingMethodChannelListingUpdate } from "@saleor/shipping/mutations";
import { useShippingZone } from "@saleor/shipping/queries";
import {
ShippingRateUrlDialog,
@ -37,7 +49,7 @@ import {
import { ShippingMethodTypeEnum } from "@saleor/types/globalTypes";
import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers";
import React from "react";
import { useIntl } from "react-intl";
import { FormattedMessage, useIntl } from "react-intl";
export interface WeightRatesUpdateProps {
id: string;
@ -53,10 +65,13 @@ export const WeightRatesUpdate: React.FC<WeightRatesUpdateProps> = ({
const navigate = useNavigator();
const notify = useNotifier();
const intl = useIntl();
const paginate = usePaginator();
const { data, loading } = useShippingZone({
const paginationState = createPaginationState(PAGINATE_BY, params);
const { data, loading, refetch } = useShippingZone({
displayLoader: true,
variables: { id }
variables: { id, ...paginationState }
});
const [openModal, closeModal] = createDialogActionHandlers<
@ -64,15 +79,55 @@ export const WeightRatesUpdate: React.FC<WeightRatesUpdateProps> = ({
ShippingRateUrlQueryParams
>(navigate, params => shippingWeightRatesEditUrl(id, rateId, params), params);
const {
loadMore,
search: productsSearch,
result: productsSearchOpts
} = useProductSearch({ variables: DEFAULT_INITIAL_SEARCH_DATA });
const rate = data?.shippingZone?.shippingMethods.find(
rate => rate.id === rateId
);
const { isSelected, listElements, reset, toggle, toggleAll } = useBulkActions(
[]
);
const { loadNextPage, loadPreviousPage, pageInfo } = paginate(
rate?.excludedProducts.pageInfo,
paginationState,
params
);
const { data: channelsData } = useChannelsList({});
const [
updateShippingMethodChannelListing,
updateShippingMethodChannelListingOpts
] = useShippingMethodChannelListingUpdate({});
const [
unassignProduct,
unassignProductOpts
] = useShippingPriceRemoveProductsFromExclude({
onCompleted: data => {
if (data.shippingPriceRemoveProductFromExclude.errors.length === 0) {
handleSuccess();
refetch();
closeModal();
}
}
});
const [assignProduct, assignProductOpts] = useShippingPriceExcludeProduct({
onCompleted: data => {
if (data.shippingPriceExcludeProducts.errors.length === 0) {
handleSuccess();
refetch();
closeModal();
}
}
});
const shippingChannels = createShippingChannelsFromRate(
rate?.channelListings
);
@ -166,6 +221,18 @@ export const WeightRatesUpdate: React.FC<WeightRatesUpdateProps> = ({
}
};
const handleProductAssign = (ids: string[]) =>
assignProduct({
variables: { id: rateId, input: { products: ids } }
});
const handleProductUnassign = (ids: string[]) => {
unassignProduct({
variables: { id: rateId, products: ids }
});
reset();
};
const handleBack = () => navigate(shippingZoneUrl(id));
return (
@ -201,13 +268,35 @@ export const WeightRatesUpdate: React.FC<WeightRatesUpdateProps> = ({
open={params.action === "remove"}
name={rate?.name}
/>
<UnassignDialog
open={params.action === "unassign-product" && !!listElements.length}
idsLength={listElements.length}
confirmButtonState={unassignProductOpts.status}
closeModal={closeModal}
onConfirm={() => handleProductUnassign(listElements)}
/>
<ShippingMethodProductsAddDialog
confirmButtonState={assignProductOpts.status}
loading={productsSearchOpts.loading}
open={params.action === "assign-product"}
hasMore={productsSearchOpts.data?.search?.pageInfo.hasNextPage}
products={productsSearchOpts.data?.search?.edges
.map(edge => edge.node)
.filter(suggestedProduct => suggestedProduct.id)}
onClose={closeModal}
onFetch={productsSearch}
onFetchMore={loadMore}
onSubmit={handleProductAssign}
/>
<ShippingZoneRatesPage
allChannelsCount={allChannels?.length}
shippingChannels={currentChannels}
disabled={
loading ||
updateShippingRateOpts?.status === "loading" ||
updateShippingMethodChannelListingOpts?.status === "loading"
updateShippingMethodChannelListingOpts?.status === "loading" ||
unassignProductOpts?.status === "loading" ||
assignProductOpts?.status === "loading"
}
hasChannelChanged={shippingChannels?.length !== currentChannels?.length}
saveButtonBarState={updateShippingRateOpts.status}
@ -222,7 +311,24 @@ export const WeightRatesUpdate: React.FC<WeightRatesUpdateProps> = ({
}
openChannelsModal={handleChannelsModalOpen}
onChannelsChange={setCurrentChannels}
onProductUnassign={handleProductUnassign}
onProductAssign={() => openModal("assign-product")}
variant={ShippingMethodTypeEnum.WEIGHT}
isChecked={isSelected}
selected={listElements.length}
toggle={toggle}
toggleAll={toggleAll}
onNextPage={loadNextPage}
onPreviousPage={loadPreviousPage}
pageInfo={pageInfo}
toolbar={
<Button color="primary" onClick={() => openModal("unassign-product")}>
<FormattedMessage
defaultMessage="Unassign"
description="unassign products from shipping method, button"
/>
</Button>
}
onZipCodeAssign={() => openModal("add-range")}
onZipCodeUnassign={id =>
openModal("remove-range", {

File diff suppressed because it is too large Load diff

View file

@ -1689,6 +1689,10 @@ export interface ShippingMethodChannelListingInput {
removeChannels?: string[] | null;
}
export interface ShippingPriceExcludeProductsInput {
products: (string | null)[];
}
export interface ShippingPriceInput {
name?: string | null;
minimumOrderWeight?: any | null;