Handle errors on finalizing draft order (#2191)

* Add warning alert before finilizing draft order

* Add line error indicators in draft order view

* Handle unfilled fields errors before draft order finalize

* Handle draft order line errors

* Differentiate line alert severity

* Fix order line alert margin

* Remove unnecessairy comment

* Refactor order draft alert components

* Update order draft test snapshots

* Refaactor order details code

* Hide add products button when no products available on order draft page

* Hide no shipping methods warning if they cannot be determined

* Update product assignment dialog messaages

* Update order channel error messages

* Fix missing order lines in error crash
This commit is contained in:
Dawid 2022-08-10 10:11:32 +01:00 committed by GitHub
parent fc15227b4c
commit f01966f0d4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
60 changed files with 3138 additions and 1427 deletions

View file

@ -1,6 +1,6 @@
export const DRAFT_ORDER_SELECTORS = {
addProducts: "[data-test-id='add-products-button']",
salesChannel: "[data-test-id='sales-channel']",
salesChannel: "[data-test-id='order-sales-channel']",
editCustomerButton: "[data-test-id='edit-customer']",
selectCustomer: "[data-test-id='select-customer']",
selectCustomerOption: "[data-test-type='option']",
@ -9,5 +9,5 @@ export const DRAFT_ORDER_SELECTORS = {
pageHeader: "[data-test-id='page-header']",
editShippingAddress: '[data-test-id="edit-shipping-address"]',
editBillingAddress: '[data-test-id="edit-billing-address"]',
customerEmail: '[data-test-id="customer-email"]'
customerEmail: '[data-test-id="customer-email"]',
};

View file

@ -8,5 +8,5 @@ export const ORDERS_SELECTORS = {
"[data-test-id='cancel-fulfillment-select-field']",
orderFulfillmentFrame: "[data-test-id='order-fulfillment']",
refundButton: '[data-test-id="refund-button"]',
fulfillMenuButton: '[data-test-id="fulfill-menu"]'
fulfillMenuButton: '[data-test-id="fulfill-menu"]',
};

View file

@ -741,6 +741,10 @@
"context": "notification",
"string": "Removed pages"
},
"43AOvZ": {
"context": "alert group message",
"string": "You will not be able to finalize this draft because:"
},
"43Nlay": {
"context": "warehouse",
"string": "Address Information"
@ -1359,6 +1363,10 @@
"9UHfux": {
"string": "Voucher Specific Information"
},
"9Y6vg+": {
"context": "dialog header",
"string": "Add product"
},
"9YazHG": {
"string": "Company"
},
@ -1390,6 +1398,10 @@
"context": "button linking to dashboard",
"string": "Dashboard"
},
"9mrWKz": {
"context": "no products placeholder",
"string": "No products are available matching query in the channel assigned to this order."
},
"9piUVz": {
"context": "order history message",
"string": "Order refund information was sent to customer"
@ -1659,6 +1671,10 @@
"BtErCZ": {
"string": "Search Plugins..."
},
"BvRyoX": {
"context": "alert message",
"string": "There are no available shipping methods in this channel."
},
"BvmnJq": {
"context": "section header",
"string": "Third Party Apps"
@ -3298,6 +3314,10 @@
"context": "deletion error message",
"string": "Cant's delete group which is out of your permission scope"
},
"O4QNFx": {
"context": "alert message",
"string": "There are no available products in this channel."
},
"O95R3Z": {
"string": "Phone"
},
@ -3344,6 +3364,10 @@
"context": "input label",
"string": "Stock reservation for authenticated user (in minutes)"
},
"Oad+ES": {
"context": "alert message",
"string": "This product is not published in this channel."
},
"ObRk1O": {
"string": "If this option is disabled, discount will be counted for every eligible product"
},
@ -3818,6 +3842,10 @@
"context": "button",
"string": "Set new password"
},
"S2xLxV": {
"context": "search placeholder",
"string": "Search by product name, attribute, product type etc..."
},
"S52JMl": {
"context": "default gift card delete description",
"string": "{selectedItemsCount,plural,one{Are you sure you want to delete this gift card?} other{Are you sure you want to delete {selectedItemsCount} giftCards?}}"
@ -3864,6 +3892,10 @@
"context": "header",
"string": "Translation Attribute \"{attribute}\" - {languageCode}"
},
"SPp3cx": {
"context": "alert message",
"string": "Orders cannot be placed in an inactive channel."
},
"SSWFo8": {
"context": "window title",
"string": "Create Product Type"
@ -4408,9 +4440,6 @@
"context": "list of warehouses",
"string": "Warehouses A to Z"
},
"WQnltU": {
"string": "No products available in order channel matching given query"
},
"WR8rir": {
"context": "button",
"string": "Create rate"
@ -6357,10 +6386,6 @@
"mxtAFx": {
"string": "Are you sure you want to delete draft #{orderNumber}?"
},
"myyWNp": {
"context": "dialog header",
"string": "Add Product"
},
"mzASqs": {
"context": "days after label",
"string": "days after issue"
@ -6993,6 +7018,10 @@
"context": "option label",
"string": "Change address"
},
"s6oAC+": {
"context": "search label",
"string": "Search products"
},
"s8FlDW": {
"context": "hide error log label in notification",
"string": "Hide log"
@ -7062,6 +7091,10 @@
"sfErC+": {
"string": "Voucher Name"
},
"shmSDX": {
"context": "no products placeholder",
"string": "No products are available in the channel assigned to this order."
},
"sidKce": {
"context": "delete channel",
"string": "All order information from this channel need to be moved to a different channel. Please select channel orders need to be moved to:."
@ -7881,6 +7914,10 @@
"context": "gift card removed success alert message",
"string": "{selectedItemsCount,plural,one{Successfully deleted gift card} other{Successfully deleted gift cards}}"
},
"zO+l0L": {
"context": "alert message",
"string": "This product is not available for sale in this channel."
},
"zQX6xO": {
"context": "dialog header",
"string": "Delete App"

4
package-lock.json generated
View file

@ -29949,7 +29949,7 @@
"throttleit": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/throttleit/-/throttleit-1.0.0.tgz",
"integrity": "sha512-rkTVqu6IjfQ/6+uNuuc3sZek4CEYxTJom3IktzgdSxcZqdARuebbA/f4QmAxMQIxqq9ZLEUkSYqvuk1I6VKq4g==",
"integrity": "sha1-nnhYNtr0Z0MUWlmEtiaNgoUorGw=",
"dev": true
},
"through": {
@ -32750,7 +32750,7 @@
"yauzl": {
"version": "2.10.0",
"resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",
"integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==",
"integrity": "sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=",
"dev": true,
"requires": {
"buffer-crc32": "~0.2.3",

View file

@ -8,6 +8,7 @@ import {
TableCell,
TableRow,
TextField,
Typography,
} from "@material-ui/core";
import ConfirmButton from "@saleor/components/ConfirmButton";
import Money from "@saleor/components/Money";
@ -17,10 +18,6 @@ import { SearchProductsQuery } from "@saleor/graphql";
import useSearchQuery from "@saleor/hooks/useSearchQuery";
import { ConfirmButtonTransitionState } from "@saleor/macaw-ui";
import { maybe, renderCollection } from "@saleor/misc";
import {
getById,
getByUnmatchingId,
} from "@saleor/orders/components/OrderReturnPage/utils";
import useScrollableDialogStyle from "@saleor/styles/useScrollableDialogStyle";
import { DialogProps, FetchMoreProps, RelayToFlat } from "@saleor/types";
import React from "react";
@ -31,12 +28,14 @@ import BackButton from "../BackButton";
import Checkbox from "../Checkbox";
import { messages } from "./messages";
import { useStyles } from "./styles";
import {
handleProductAssign,
handleVariantAssign,
hasAllVariantsSelected,
isVariantSelected,
SearchVariant,
} from "./utils";
type SearchVariant = RelayToFlat<
SearchProductsQuery["search"]
>[0]["variants"][0];
type SetVariantsAction = (data: SearchVariant[]) => void;
export interface AssignVariantDialogFormData {
products: RelayToFlat<SearchProductsQuery["search"]>;
query: string;
@ -49,57 +48,6 @@ export interface AssignVariantDialogProps extends FetchMoreProps, DialogProps {
onSubmit: (data: SearchVariant[]) => void;
}
function isVariantSelected(
variant: SearchVariant,
selectedVariantsToProductsMap: SearchVariant[],
): boolean {
return !!selectedVariantsToProductsMap.find(getById(variant.id));
}
const handleProductAssign = (
product: RelayToFlat<SearchProductsQuery["search"]>[0],
productIndex: number,
productsWithAllVariantsSelected: boolean[],
variants: SearchVariant[],
setVariants: SetVariantsAction,
) =>
productsWithAllVariantsSelected[productIndex]
? setVariants(
variants.filter(
selectedVariant =>
!product.variants.find(getById(selectedVariant.id)),
),
)
: setVariants([
...variants,
...product.variants.filter(
productVariant => !variants.find(getById(productVariant.id)),
),
]);
const handleVariantAssign = (
variant: SearchVariant,
variantIndex: number,
productIndex: number,
variants: SearchVariant[],
selectedVariantsToProductsMap: boolean[][],
setVariants: SetVariantsAction,
) =>
selectedVariantsToProductsMap[productIndex][variantIndex]
? setVariants(variants.filter(getByUnmatchingId(variant.id)))
: setVariants([...variants, variant]);
function hasAllVariantsSelected(
productVariants: SearchVariant[],
selectedVariantsToProductsMap: SearchVariant[],
): boolean {
return productVariants.reduce(
(acc, productVariant) =>
acc && !!selectedVariantsToProductsMap.find(getById(productVariant.id)),
true,
);
}
const scrollableTargetId = "assignVariantScrollableDialog";
const AssignVariantDialog: React.FC<AssignVariantDialogProps> = props => {
@ -264,14 +212,11 @@ const AssignVariantDialog: React.FC<AssignVariantDialogProps> = props => {
</React.Fragment>
),
() => (
<TableRow>
<TableCell colSpan={4}>
<FormattedMessage
id="WQnltU"
defaultMessage="No products available in order channel matching given query"
/>
</TableCell>
</TableRow>
<Typography className={classes.noContentText}>
{!!query
? intl.formatMessage(messages.noProductsInQuery)
: intl.formatMessage(messages.noProductsInChannel)}
</Typography>
),
)}
</TableBody>

View file

@ -24,4 +24,16 @@ export const messages = defineMessages({
defaultMessage: "SKU {sku}",
description: "variant sku",
},
noProductsInChannel: {
id: "shmSDX",
defaultMessage:
"No products are available in the channel assigned to this order.",
description: "no products placeholder",
},
noProductsInQuery: {
id: "9mrWKz",
defaultMessage:
"No products are available matching query in the channel assigned to this order.",
description: "no products placeholder",
},
});

View file

@ -12,6 +12,9 @@ export const useStyles = makeStyles(
colVariantCheckbox: {
padding: 0,
},
noContentText: {
marginBottom: theme.spacing(3),
},
content: {
overflowY: "scroll",
paddingTop: 0,

View file

@ -0,0 +1,63 @@
import { SearchProductsQuery } from "@saleor/graphql";
import {
getById,
getByUnmatchingId,
} from "@saleor/orders/components/OrderReturnPage/utils";
import { RelayToFlat } from "@saleor/types";
export type SearchVariant = RelayToFlat<
SearchProductsQuery["search"]
>[0]["variants"][0];
type SetVariantsAction = (data: SearchVariant[]) => void;
export function isVariantSelected(
variant: SearchVariant,
selectedVariantsToProductsMap: SearchVariant[],
): boolean {
return !!selectedVariantsToProductsMap.find(getById(variant.id));
}
export const handleProductAssign = (
product: RelayToFlat<SearchProductsQuery["search"]>[0],
productIndex: number,
productsWithAllVariantsSelected: boolean[],
variants: SearchVariant[],
setVariants: SetVariantsAction,
) =>
productsWithAllVariantsSelected[productIndex]
? setVariants(
variants.filter(
selectedVariant =>
!product.variants.find(getById(selectedVariant.id)),
),
)
: setVariants([
...variants,
...product.variants.filter(
productVariant => !variants.find(getById(productVariant.id)),
),
]);
export const handleVariantAssign = (
variant: SearchVariant,
variantIndex: number,
productIndex: number,
variants: SearchVariant[],
selectedVariantsToProductsMap: boolean[][],
setVariants: SetVariantsAction,
) =>
selectedVariantsToProductsMap[productIndex][variantIndex]
? setVariants(variants.filter(getByUnmatchingId(variant.id)))
: setVariants([...variants, variant]);
export function hasAllVariantsSelected(
productVariants: SearchVariant[],
selectedVariantsToProductsMap: SearchVariant[],
): boolean {
return productVariants.reduce(
(acc, productVariant) =>
acc && !!selectedVariantsToProductsMap.find(getById(productVariant.id)),
true,
);
}

View file

@ -1,21 +1,20 @@
import { Table } from "@material-ui/core";
import { makeStyles } from "@saleor/macaw-ui";
import classNames from "classnames";
import React from "react";
const useStyles = makeStyles(
theme => ({
root: {
[theme.breakpoints.up("md")]: {
"&& table": {
tableLayout: "fixed",
},
},
"& table": {
tableLayout: "auto",
},
overflowX: "auto",
width: "100%",
},
table: {
[theme.breakpoints.up("md")]: {
tableLayout: "fixed",
},
tableLayout: "auto",
},
}),
{
name: "ResponsiveTable",
@ -35,7 +34,7 @@ const ResponsiveTable: React.FC<ResponsiveTableProps> = props => {
return (
<div className={classes.root}>
<Table className={className}>{children}</Table>
<Table className={classNames(classes.table, className)}>{children}</Table>
</div>
);
};

View file

@ -73,6 +73,7 @@ export const orderErrorFragment = gql`
field
addressType
message
orderLines
}
`;

View file

@ -91,6 +91,17 @@ export const fragmentOrderLine = gql`
stocks {
...Stock
}
product {
id
channelListings {
id
isPublished
isAvailableForPurchase
channel {
id
}
}
}
}
productName
productSku

View file

@ -587,6 +587,7 @@ export const OrderErrorFragmentDoc = gql`
field
addressType
message
orderLines
}
`;
export const OrderSettingsErrorFragmentDoc = gql`
@ -1239,6 +1240,17 @@ export const OrderLineFragmentDoc = gql`
stocks {
...Stock
}
product {
id
channelListings {
id
isPublished
isAvailableForPurchase
channel {
id
}
}
}
}
productName
productSku
@ -8873,7 +8885,6 @@ export const FulfillOrderDocument = gql`
errors {
...OrderError
warehouse
orderLines
}
order {
...OrderDetails
@ -9423,6 +9434,41 @@ export function useOrderRefundDataLazyQuery(baseOptions?: ApolloReactHooks.LazyQ
export type OrderRefundDataQueryHookResult = ReturnType<typeof useOrderRefundDataQuery>;
export type OrderRefundDataLazyQueryHookResult = ReturnType<typeof useOrderRefundDataLazyQuery>;
export type OrderRefundDataQueryResult = Apollo.QueryResult<Types.OrderRefundDataQuery, Types.OrderRefundDataQueryVariables>;
export const ChannelUsabilityDataDocument = gql`
query ChannelUsabilityData($channel: String!) {
products(channel: $channel) {
totalCount
}
}
`;
/**
* __useChannelUsabilityDataQuery__
*
* To run a query within a React component, call `useChannelUsabilityDataQuery` and pass it any options that fit your needs.
* When your component renders, `useChannelUsabilityDataQuery` 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 } = useChannelUsabilityDataQuery({
* variables: {
* channel: // value for 'channel'
* },
* });
*/
export function useChannelUsabilityDataQuery(baseOptions: ApolloReactHooks.QueryHookOptions<Types.ChannelUsabilityDataQuery, Types.ChannelUsabilityDataQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return ApolloReactHooks.useQuery<Types.ChannelUsabilityDataQuery, Types.ChannelUsabilityDataQueryVariables>(ChannelUsabilityDataDocument, options);
}
export function useChannelUsabilityDataLazyQuery(baseOptions?: ApolloReactHooks.LazyQueryHookOptions<Types.ChannelUsabilityDataQuery, Types.ChannelUsabilityDataQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return ApolloReactHooks.useLazyQuery<Types.ChannelUsabilityDataQuery, Types.ChannelUsabilityDataQueryVariables>(ChannelUsabilityDataDocument, options);
}
export type ChannelUsabilityDataQueryHookResult = ReturnType<typeof useChannelUsabilityDataQuery>;
export type ChannelUsabilityDataLazyQueryHookResult = ReturnType<typeof useChannelUsabilityDataLazyQuery>;
export type ChannelUsabilityDataQueryResult = Apollo.QueryResult<Types.ChannelUsabilityDataQuery, Types.ChannelUsabilityDataQueryVariables>;
export const PageTypeUpdateDocument = gql`
mutation PageTypeUpdate($id: ID!, $input: PageTypeUpdateInput!) {
pageTypeUpdate(id: $id, input: $input) {

File diff suppressed because one or more lines are too long

View file

@ -1,20 +0,0 @@
import CardDecorator from "@saleor/storybook/CardDecorator";
import Decorator from "@saleor/storybook/Decorator";
import { storiesOf } from "@storybook/react";
import React from "react";
import DraftOrderChannelSectionCard, {
DraftOrderChannelSectionCardProps,
} from ".";
const props: DraftOrderChannelSectionCardProps = {
channelName: "Default Channel",
};
storiesOf("Orders / Draft order channel section", module)
.addDecorator(CardDecorator)
.addDecorator(Decorator)
.add("default", () => <DraftOrderChannelSectionCard {...props} />)
.add("loading", () => (
<DraftOrderChannelSectionCard {...props} channelName={undefined} />
));

View file

@ -1,32 +0,0 @@
import { Card, CardContent, Typography } from "@material-ui/core";
import CardTitle from "@saleor/components/CardTitle";
import Skeleton from "@saleor/components/Skeleton";
import React from "react";
import { useIntl } from "react-intl";
export interface DraftOrderChannelSectionCardProps {
channelName: string;
}
export const DraftOrderChannelSectionCard: React.FC<DraftOrderChannelSectionCardProps> = ({
channelName,
}) => {
const intl = useIntl();
return (
<Card>
<CardTitle
title={intl.formatMessage({
id: "aY0HAT",
defaultMessage: "Sales channel",
description: "section header",
})}
/>
<CardContent data-test-id="sales-channel">
{!channelName ? <Skeleton /> : <Typography>{channelName}</Typography>}
</CardContent>
</Card>
);
};
DraftOrderChannelSectionCard.displayName = "DraftOrderChannelSectionCard";
export default DraftOrderChannelSectionCard;

View file

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

View file

@ -0,0 +1,45 @@
import React from "react";
import { MessageDescriptor, useIntl } from "react-intl";
interface OrderAlertsProps {
alertsHeader?: string;
alerts: Array<string | MessageDescriptor>;
}
export const OrderAlerts: React.FC<OrderAlertsProps> = ({
alertsHeader,
alerts,
}) => {
const intl = useIntl();
const formattedAlerts = alerts.map((alert, index) => {
if (typeof alert === "string") {
return { id: `${index}_${alert}`, text: alert };
}
return {
id: alert.id,
text: intl.formatMessage(alert),
};
});
if (!formattedAlerts.length) {
return null;
}
if (formattedAlerts.length === 1) {
return <>{formattedAlerts[0].text}</>;
}
return (
<>
{!!alertsHeader && alertsHeader}
<ul>
{formattedAlerts.map(alert => (
<li key={alert.id}>{alert.text}</li>
))}
</ul>
</>
);
};
OrderAlerts.displayName = "OrderAlerts";
export default OrderAlerts;

View file

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

View file

@ -6,7 +6,10 @@ import React from "react";
import OrderChannelSectionCard, { OrderChannelSectionCardProps } from ".";
const props: OrderChannelSectionCardProps = {
selectedChannelName: "International store",
channel: {
id: "dh87hf34hk8i",
name: "International store",
},
};
storiesOf("Orders / Order details channel section", module)
@ -14,5 +17,5 @@ storiesOf("Orders / Order details channel section", module)
.addDecorator(Decorator)
.add("default", () => <OrderChannelSectionCard {...props} />)
.add("loading", () => (
<OrderChannelSectionCard {...props} selectedChannelName={undefined} />
<OrderChannelSectionCard {...props} channel={undefined} />
));

View file

@ -1,15 +1,18 @@
import { Card, CardContent, Typography } from "@material-ui/core";
import { channelUrl } from "@saleor/channels/urls";
import CardTitle from "@saleor/components/CardTitle";
import Link from "@saleor/components/Link";
import Skeleton from "@saleor/components/Skeleton";
import { ChannelFragment } from "@saleor/graphql";
import React from "react";
import { useIntl } from "react-intl";
export interface OrderChannelSectionCardProps {
selectedChannelName: string;
channel?: Pick<ChannelFragment, "id" | "name">;
}
export const OrderChannelSectionCard: React.FC<OrderChannelSectionCardProps> = ({
selectedChannelName,
channel,
}) => {
const intl = useIntl();
@ -23,10 +26,14 @@ export const OrderChannelSectionCard: React.FC<OrderChannelSectionCardProps> = (
})}
/>
<CardContent>
{selectedChannelName === undefined ? (
{!channel ? (
<Skeleton />
) : (
<Typography>{selectedChannelName}</Typography>
<Typography>
<Link href={channelUrl(channel.id) ?? ""} disabled={!channel.id}>
{channel.name ?? "..."}
</Link>
</Typography>
)}
</CardContent>
</Card>

View file

@ -0,0 +1,28 @@
import { Typography } from "@material-ui/core";
import FormSpacer from "@saleor/components/FormSpacer";
import { OrderErrorFragment } from "@saleor/graphql";
import getOrderErrorMessage from "@saleor/utils/errors/order";
import React from "react";
import { useIntl } from "react-intl";
import { useAddressTextErrorStyles } from "./styles";
interface AddressTextErrorProps {
orderError: OrderErrorFragment;
}
export const AddressTextError: React.FC<AddressTextErrorProps> = ({
orderError,
}) => {
const intl = useIntl();
const classes = useAddressTextErrorStyles();
return (
<>
<Typography variant="body2" className={classes.textError}>
{getOrderErrorMessage(orderError, intl)}
</Typography>
<FormSpacer />
</>
);
};

View file

@ -1,9 +1,9 @@
import { Card, CardContent, Typography } from "@material-ui/core";
import AddressFormatter from "@saleor/components/AddressFormatter";
import { Button } from "@saleor/components/Button";
import CardTitle from "@saleor/components/CardTitle";
import ExternalLink from "@saleor/components/ExternalLink";
import Form from "@saleor/components/Form";
import FormSpacer from "@saleor/components/FormSpacer";
import Hr from "@saleor/components/Hr";
import Link from "@saleor/components/Link";
import RequirePermissions from "@saleor/components/RequirePermissions";
@ -11,13 +11,13 @@ import SingleAutocompleteSelectField from "@saleor/components/SingleAutocomplete
import Skeleton from "@saleor/components/Skeleton";
import {
OrderDetailsFragment,
OrderErrorCode,
OrderErrorFragment,
PermissionEnum,
SearchCustomersQuery,
WarehouseClickAndCollectOptionEnum,
} from "@saleor/graphql";
import useStateFromProps from "@saleor/hooks/useStateFromProps";
import { buttonMessages } from "@saleor/intl";
import { makeStyles } from "@saleor/macaw-ui";
import { FetchMoreProps, RelayToFlat } from "@saleor/types";
import createSingleAutocompleteSelectHandler from "@saleor/utils/handlers/singleAutocompleteSelectChangeHandler";
import React from "react";
@ -25,31 +25,9 @@ import { FormattedMessage, useIntl } from "react-intl";
import { customerUrl } from "../../../customers/urls";
import { maybe } from "../../../misc";
import messages from "./messages";
const useStyles = makeStyles(
theme => ({
sectionHeader: {
alignItems: "center",
display: "flex",
marginBottom: theme.spacing(3),
},
sectionHeaderTitle: {
flex: 1,
fontWeight: 600 as 600,
lineHeight: 1,
textTransform: "uppercase",
},
sectionHeaderToolbar: {
marginRight: theme.spacing(-2),
},
userEmail: {
fontWeight: 600 as 600,
marginBottom: theme.spacing(1),
},
}),
{ name: "OrderCustomer" },
);
import { AddressTextError } from "./AddrssTextError";
import { PickupAnnotation } from "./PickupAnnotation";
import { useStyles } from "./styles";
export interface CustomerEditData {
user?: string;
@ -62,6 +40,7 @@ export interface OrderCustomerProps extends Partial<FetchMoreProps> {
order: OrderDetailsFragment;
users?: RelayToFlat<SearchCustomersQuery["search"]>;
loading?: boolean;
errors: OrderErrorFragment[];
canEditAddresses: boolean;
canEditCustomer: boolean;
fetchUsers?: (query: string) => void;
@ -78,6 +57,7 @@ const OrderCustomer: React.FC<OrderCustomerProps> = props => {
fetchUsers,
hasMore: hasMoreUsers,
loading,
errors = [],
order,
users,
onCustomerEdit,
@ -102,24 +82,12 @@ const OrderCustomer: React.FC<OrderCustomerProps> = props => {
const billingAddress = maybe(() => order.billingAddress);
const shippingAddress = maybe(() => order.shippingAddress);
const pickupAnnotation = order => {
if (order?.deliveryMethod?.__typename === "Warehouse") {
return (
<>
<FormSpacer />
<Typography variant="caption" color="textSecondary">
{order?.deliveryMethod?.clickAndCollectOption ===
WarehouseClickAndCollectOptionEnum.LOCAL ? (
<FormattedMessage {...messages.orderCustomerFulfillmentLocal} />
) : (
<FormattedMessage {...messages.orderCustomerFulfillmentAll} />
)}
</Typography>
</>
const noBillingAddressError = errors.find(
error => error.code === OrderErrorCode.BILLING_ADDRESS_NOT_SET,
);
const noShippingAddressError = errors.find(
error => error.code === OrderErrorCode.ORDER_NO_SHIPPING_ADDRESS,
);
}
return "";
};
return (
<Card>
@ -295,7 +263,12 @@ const OrderCustomer: React.FC<OrderCustomerProps> = props => {
</div>
{shippingAddress === undefined ? (
<Skeleton />
) : shippingAddress === null ? (
) : (
<>
{noShippingAddressError && (
<AddressTextError orderError={noShippingAddressError} />
)}
{shippingAddress === null ? (
<Typography>
<FormattedMessage
id="e7yOai"
@ -305,30 +278,10 @@ const OrderCustomer: React.FC<OrderCustomerProps> = props => {
</Typography>
) : (
<>
{shippingAddress.companyName && (
<Typography>{shippingAddress.companyName}</Typography>
<AddressFormatter address={shippingAddress} />
<PickupAnnotation order={order} />
</>
)}
<Typography>
{shippingAddress.firstName} {shippingAddress.lastName}
</Typography>
<Typography>
{shippingAddress.streetAddress1}
<br />
{shippingAddress.streetAddress2}
</Typography>
<Typography>
{shippingAddress.postalCode} {shippingAddress.city}
{shippingAddress.cityArea ? ", " + shippingAddress.cityArea : ""}
</Typography>
<Typography>
{shippingAddress.countryArea
? shippingAddress.countryArea +
", " +
shippingAddress.country.country
: shippingAddress.country.country}
</Typography>
<Typography>{shippingAddress.phone}</Typography>
{pickupAnnotation(order)}
</>
)}
</CardContent>
@ -353,7 +306,12 @@ const OrderCustomer: React.FC<OrderCustomerProps> = props => {
</div>
{billingAddress === undefined ? (
<Skeleton />
) : billingAddress === null ? (
) : (
<>
{noBillingAddressError && (
<AddressTextError orderError={noBillingAddressError} />
)}
{billingAddress === null ? (
<Typography>
<FormattedMessage
id="YI6Fhj"
@ -370,30 +328,8 @@ const OrderCustomer: React.FC<OrderCustomerProps> = props => {
/>
</Typography>
) : (
<>
{billingAddress.companyName && (
<Typography>{billingAddress.companyName}</Typography>
<AddressFormatter address={billingAddress} />
)}
<Typography>
{billingAddress.firstName} {billingAddress.lastName}
</Typography>
<Typography>
{billingAddress.streetAddress1}
<br />
{billingAddress.streetAddress2}
</Typography>
<Typography>
{billingAddress.postalCode} {billingAddress.city}
{billingAddress.cityArea ? ", " + billingAddress.cityArea : ""}
</Typography>
<Typography>
{billingAddress.countryArea
? billingAddress.countryArea +
", " +
billingAddress.country.country
: billingAddress.country.country}
</Typography>
<Typography>{billingAddress.phone}</Typography>
</>
)}
</CardContent>

View file

@ -0,0 +1,35 @@
import { Typography } from "@material-ui/core";
import FormSpacer from "@saleor/components/FormSpacer";
import {
OrderDetailsFragment,
WarehouseClickAndCollectOptionEnum,
} from "@saleor/graphql";
import React from "react";
import { FormattedMessage } from "react-intl";
import messages from "./messages";
interface PickupAnnotationProps {
order?: OrderDetailsFragment;
}
export const PickupAnnotation: React.FC<PickupAnnotationProps> = ({
order,
}) => {
if (order?.deliveryMethod?.__typename === "Warehouse") {
return (
<>
<FormSpacer />
<Typography variant="caption" color="textSecondary">
{order?.deliveryMethod?.clickAndCollectOption ===
WarehouseClickAndCollectOptionEnum.LOCAL ? (
<FormattedMessage {...messages.orderCustomerFulfillmentLocal} />
) : (
<FormattedMessage {...messages.orderCustomerFulfillmentAll} />
)}
</Typography>
</>
);
}
return null;
};

View file

@ -0,0 +1,34 @@
import { makeStyles } from "@saleor/macaw-ui";
export const useStyles = makeStyles(
theme => ({
sectionHeader: {
alignItems: "center",
display: "flex",
marginBottom: theme.spacing(3),
},
sectionHeaderTitle: {
flex: 1,
fontWeight: 600 as 600,
lineHeight: 1,
textTransform: "uppercase",
},
sectionHeaderToolbar: {
marginRight: theme.spacing(-2),
},
userEmail: {
fontWeight: 600 as 600,
marginBottom: theme.spacing(1),
},
}),
{ name: "OrderCustomer" },
);
export const useAddressTextErrorStyles = makeStyles(
theme => ({
textError: {
color: theme.palette.error.main,
},
}),
{ name: "AddrssTextError" },
);

View file

@ -18,6 +18,7 @@ import Skeleton from "@saleor/components/Skeleton";
import {
OrderDetailsFragment,
OrderDetailsQuery,
OrderErrorFragment,
OrderStatus,
} from "@saleor/graphql";
import { SubmitPromise } from "@saleor/hooks/useForm";
@ -69,6 +70,7 @@ export interface OrderDetailsPageProps {
}>;
disabled: boolean;
saveButtonBarState: ConfirmButtonTransitionState;
errors: OrderErrorFragment[];
onOrderLineAdd?: () => void;
onOrderLineChange?: (
id: string,
@ -121,6 +123,7 @@ const OrderDetailsPage: React.FC<OrderDetailsPageProps> = props => {
order,
shop,
saveButtonBarState,
errors,
onBillingAddressEdit,
onFulfillmentApprove,
onFulfillmentCancel,
@ -268,6 +271,7 @@ const OrderDetailsPage: React.FC<OrderDetailsPageProps> = props => {
<>
<OrderDraftDetails
order={order}
errors={errors}
onOrderLineAdd={onOrderLineAdd}
onOrderLineChange={onOrderLineChange}
onOrderLineRemove={onOrderLineRemove}
@ -319,14 +323,13 @@ const OrderDetailsPage: React.FC<OrderDetailsPageProps> = props => {
canEditAddresses={canEditAddresses}
canEditCustomer={false}
order={order}
errors={errors}
onBillingAddressEdit={onBillingAddressEdit}
onShippingAddressEdit={onShippingAddressEdit}
onProfileView={onProfileView}
/>
<CardSpacer />
<OrderChannelSectionCard
selectedChannelName={order?.channel?.name}
/>
<OrderChannelSectionCard channel={order?.channel} />
<CardSpacer />
{!isOrderUnconfirmed && (
<>

View file

@ -1,7 +1,12 @@
import { Card, CardContent } from "@material-ui/core";
import { Button } from "@saleor/components/Button";
import CardTitle from "@saleor/components/CardTitle";
import { OrderDetailsFragment, OrderLineInput } from "@saleor/graphql";
import {
ChannelUsabilityDataQuery,
OrderDetailsFragment,
OrderErrorFragment,
OrderLineInput,
} from "@saleor/graphql";
import {
OrderDiscountContext,
OrderDiscountContextConsumerProps,
@ -15,6 +20,8 @@ import OrderDraftDetailsSummary from "../OrderDraftDetailsSummary";
interface OrderDraftDetailsProps {
order: OrderDetailsFragment;
channelUsabilityData?: ChannelUsabilityDataQuery;
errors: OrderErrorFragment[];
onOrderLineAdd: () => void;
onOrderLineChange: (id: string, data: OrderLineInput) => void;
onOrderLineRemove: (id: string) => void;
@ -23,6 +30,8 @@ interface OrderDraftDetailsProps {
const OrderDraftDetails: React.FC<OrderDraftDetailsProps> = ({
order,
channelUsabilityData,
errors,
onOrderLineAdd,
onOrderLineChange,
onOrderLineRemove,
@ -30,6 +39,9 @@ const OrderDraftDetails: React.FC<OrderDraftDetailsProps> = ({
}) => {
const intl = useIntl();
const isChannelActive = order?.channel.isActive;
const areProductsInChannel = !!channelUsabilityData?.products.totalCount;
return (
<Card>
<CardTitle
@ -39,7 +51,8 @@ const OrderDraftDetails: React.FC<OrderDraftDetailsProps> = ({
description: "section header",
})}
toolbar={
order?.channel.isActive && (
isChannelActive &&
areProductsInChannel && (
<Button
variant="tertiary"
onClick={onOrderLineAdd}
@ -55,7 +68,8 @@ const OrderDraftDetails: React.FC<OrderDraftDetailsProps> = ({
}
/>
<OrderDraftDetailsProducts
lines={maybe(() => order.lines)}
order={order}
errors={errors}
onOrderLineChange={onOrderLineChange}
onOrderLineRemove={onOrderLineRemove}
/>
@ -65,6 +79,7 @@ const OrderDraftDetails: React.FC<OrderDraftDetailsProps> = ({
{(orderDiscountProps: OrderDiscountContextConsumerProps) => (
<OrderDraftDetailsSummary
order={order}
errors={errors}
onShippingMethodEdit={onShippingMethodEdit}
{...orderDiscountProps}
/>

View file

@ -1,16 +1,23 @@
import { TableBody, TableCell, TableHead, TableRow } from "@material-ui/core";
import {
TableBody,
TableCell,
TableHead,
TableRow,
Typography,
} from "@material-ui/core";
import ResponsiveTable from "@saleor/components/ResponsiveTable";
import { AVATAR_MARGIN } from "@saleor/components/TableCellAvatar/Avatar";
import { OrderLineFragment } from "@saleor/graphql";
import Skeleton from "@saleor/components/Skeleton";
import { OrderDetailsFragment, OrderErrorFragment } from "@saleor/graphql";
import { makeStyles } from "@saleor/macaw-ui";
import {
OrderLineDiscountConsumer,
OrderLineDiscountContextConsumerProps,
} from "@saleor/products/components/OrderDiscountProviders/OrderLineDiscountProvider";
import getOrderErrorMessage from "@saleor/utils/errors/order";
import React from "react";
import { FormattedMessage } from "react-intl";
import { FormattedMessage, useIntl } from "react-intl";
import { maybe, renderCollection } from "../../../misc";
import { renderCollection } from "../../../misc";
import TableLine from "./TableLine";
export interface FormData {
@ -20,60 +27,65 @@ export interface FormData {
const useStyles = makeStyles(
theme => ({
colAction: {
"&:last-child": {
paddingRight: 0,
},
width: theme.spacing(10),
},
colName: {
width: "auto",
},
colNameLabel: {
marginLeft: AVATAR_MARGIN,
},
colPrice: {
textAlign: "right",
},
colQuantity: {
textAlign: "right",
},
colTotal: {
textAlign: "right",
colNameLabel: {},
colPrice: {},
colQuantity: {},
colTotal: {},
skeleton: {
margin: theme.spacing(0, 4),
},
errorInfo: {
color: theme.palette.error.main,
marginLeft: theme.spacing(1.5),
display: "inline",
},
quantityField: {
"& input": {
padding: "12px 12px 10px",
textAlign: "right",
},
width: 60,
},
table: {
tableLayout: "fixed",
[theme.breakpoints.up("md")]: {
tableLayout: "auto",
},
tableLayout: "auto",
},
}),
{ name: "OrderDraftDetailsProducts" },
);
interface OrderDraftDetailsProductsProps {
lines: OrderLineFragment[];
order?: OrderDetailsFragment;
errors: OrderErrorFragment[];
onOrderLineChange: (id: string, data: FormData) => void;
onOrderLineRemove: (id: string) => void;
}
const OrderDraftDetailsProducts: React.FC<OrderDraftDetailsProductsProps> = props => {
const { lines, onOrderLineChange, onOrderLineRemove } = props;
const { order, errors, onOrderLineChange, onOrderLineRemove } = props;
const lines = order?.lines ?? [];
const intl = useIntl();
const classes = useStyles(props);
const formErrors = errors.filter(error => error.field === "lines");
if (order === undefined) {
return <Skeleton className={classes.skeleton} />;
}
return (
<ResponsiveTable className={classes.table}>
{maybe(() => !!lines.length) && (
{!!lines.length && (
<TableHead>
<TableRow>
<TableCell className={classes.colName}>
<TableCell className={classes.colName} colSpan={2}>
<span className={classes.colNameLabel}>
<FormattedMessage id="x/ZVlU" defaultMessage="Product" />
</span>
@ -104,7 +116,7 @@ const OrderDraftDetailsProducts: React.FC<OrderDraftDetailsProductsProps> = prop
</TableHead>
)}
<TableBody>
{!!lines?.length ? (
{!!lines.length ? (
renderCollection(lines, line => (
<OrderLineDiscountConsumer key={line.id} orderLineId={line.id}>
{(
@ -113,6 +125,10 @@ const OrderDraftDetailsProducts: React.FC<OrderDraftDetailsProductsProps> = prop
<TableLine
{...orderLineDiscountProps}
line={line}
channelId={order.channel.id}
error={formErrors.find(error =>
error.orderLines?.some(id => id === line.id),
)}
onOrderLineChange={onOrderLineChange}
onOrderLineRemove={onOrderLineRemove}
/>
@ -120,14 +136,21 @@ const OrderDraftDetailsProducts: React.FC<OrderDraftDetailsProductsProps> = prop
</OrderLineDiscountConsumer>
))
) : (
<>
<TableRow>
<TableCell colSpan={5}>
<FormattedMessage
id="UD7/q8"
defaultMessage="No Products added to Order"
/>
{!!formErrors.length && (
<Typography variant="body2" className={classes.errorInfo}>
{getOrderErrorMessage(formErrors[0], intl)}
</Typography>
)}
</TableCell>
</TableRow>
</>
)}
</TableBody>
</ResponsiveTable>

View file

@ -3,22 +3,31 @@ import Link from "@saleor/components/Link";
import Money from "@saleor/components/Money";
import TableCellAvatar from "@saleor/components/TableCellAvatar";
import { AVATAR_MARGIN } from "@saleor/components/TableCellAvatar/Avatar";
import { OrderLineFragment, OrderLineInput } from "@saleor/graphql";
import {
OrderErrorFragment,
OrderLineFragment,
OrderLineInput,
} from "@saleor/graphql";
import { DeleteIcon, IconButton, makeStyles } from "@saleor/macaw-ui";
import { OrderLineDiscountContextConsumerProps } from "@saleor/products/components/OrderDiscountProviders/OrderLineDiscountProvider";
import classNames from "classnames";
import React, { useRef } from "react";
import { maybe } from "../../../misc";
import OrderDiscountCommonModal from "../OrderDiscountCommonModal";
import { ORDER_LINE_DISCOUNT } from "../OrderDiscountCommonModal/types";
import TableLineAlert from "./TableLineAlert";
import TableLineForm from "./TableLineForm";
import useLineAlerts from "./useLineAlerts";
const useStyles = makeStyles(
theme => ({
colAction: {
"&:last-child": {
colStatusEmpty: {
"&:first-child:not(.MuiTableCell-paddingCheckbox)": {
paddingRight: 0,
},
},
colAction: {
width: `calc(76px + ${theme.spacing(0.5)})`,
},
colName: {
@ -40,24 +49,22 @@ const useStyles = makeStyles(
textDecoration: "line-through",
color: theme.palette.grey[400],
},
errorInfo: {
color: theme.palette.error.main,
},
table: {
tableLayout: "fixed",
},
}),
{ name: "OrderDraftDetailsProducts" },
);
interface TableLineProps extends OrderLineDiscountContextConsumerProps {
line: OrderLineFragment;
channelId: string;
error?: OrderErrorFragment;
onOrderLineChange: (id: string, data: OrderLineInput) => void;
onOrderLineRemove: (id: string) => void;
}
const TableLine: React.FC<TableLineProps> = ({
line,
channelId,
error,
onOrderLineChange,
onOrderLineRemove,
orderLineDiscount,
@ -71,10 +78,16 @@ const TableLine: React.FC<TableLineProps> = ({
discountedPrice,
orderLineDiscountUpdateStatus,
}) => {
const classes = useStyles({});
const classes = useStyles();
const popperAnchorRef = useRef<HTMLTableRowElement | null>(null);
const { id, thumbnail, productName, productSku, quantity } = line;
const alerts = useLineAlerts({
line,
channelId,
error,
});
const getUnitPriceLabel = () => {
const money = <Money money={undiscountedPrice} />;
@ -94,6 +107,18 @@ const TableLine: React.FC<TableLineProps> = ({
return (
<TableRow key={id}>
<TableCell
className={classNames({
[classes.colStatusEmpty]: !alerts.length,
})}
>
{!!alerts.length && (
<TableLineAlert
alerts={alerts}
variant={!!error ? "error" : "warning"}
/>
)}
</TableCell>
<TableCellAvatar
className={classes.colName}
thumbnail={maybe(() => thumbnail.url)}

View file

@ -0,0 +1,30 @@
import {
IndicatorOutlined,
Tooltip,
TooltipMountWrapper,
} from "@saleor/macaw-ui";
import React from "react";
import OrderAlerts from "../OrderAlerts";
interface TableLineAlertProps {
alerts?: string[];
variant: "warning" | "error";
}
const TableLineAlert: React.FC<TableLineAlertProps> = ({ alerts, variant }) => {
if (!alerts.length) {
return null;
}
const title = <OrderAlerts alerts={alerts} />;
return (
<Tooltip title={title} variant={variant}>
<TooltipMountWrapper>
<IndicatorOutlined icon={variant} />
</TooltipMountWrapper>
</Tooltip>
);
};
export default TableLineAlert;

View file

@ -0,0 +1,14 @@
import { defineMessages } from "react-intl";
export const lineAlertMessages = defineMessages({
notPublished: {
id: "Oad+ES",
defaultMessage: "This product is not published in this channel.",
description: "alert message",
},
notAvailable: {
id: "zO+l0L",
defaultMessage: "This product is not available for sale in this channel.",
description: "alert message",
},
});

View file

@ -0,0 +1,46 @@
import { OrderErrorFragment, OrderLineFragment } from "@saleor/graphql";
import getOrderErrorMessage from "@saleor/utils/errors/order";
import { useMemo } from "react";
import { useIntl } from "react-intl";
import { lineAlertMessages } from "./messages";
interface UseLineAlertsOpts {
line: OrderLineFragment;
channelId: string;
error?: OrderErrorFragment;
}
const useLineAlerts = ({ line, channelId, error }: UseLineAlertsOpts) => {
const intl = useIntl();
const alerts = useMemo(() => {
const {
variant: {
product: { channelListings },
},
} = line;
const channelListing = channelListings.find(
channelListing => channelListing.channel.id === channelId,
);
const isPublished = channelListing?.isPublished;
const isAvailable = channelListing?.isAvailableForPurchase;
const alerts: string[] = [];
if (error) {
alerts.push(getOrderErrorMessage(error, intl));
}
if (!isPublished) {
alerts.push(intl.formatMessage(lineAlertMessages.notPublished));
}
if (!isAvailable) {
alerts.push(intl.formatMessage(lineAlertMessages.notAvailable));
}
return alerts;
}, [line, channelId, error]);
return alerts;
};
export default useLineAlerts;

View file

@ -2,10 +2,16 @@ import { Typography } from "@material-ui/core";
import HorizontalSpacer from "@saleor/apps/components/HorizontalSpacer";
import Link from "@saleor/components/Link";
import Money from "@saleor/components/Money";
import { DiscountValueTypeEnum, OrderDetailsFragment } from "@saleor/graphql";
import {
DiscountValueTypeEnum,
OrderDetailsFragment,
OrderErrorFragment,
} from "@saleor/graphql";
import { makeStyles } from "@saleor/macaw-ui";
import { OrderDiscountContextConsumerProps } from "@saleor/products/components/OrderDiscountProviders/OrderDiscountProvider";
import { OrderDiscountData } from "@saleor/products/components/OrderDiscountProviders/types";
import { getFormErrors } from "@saleor/utils/errors";
import getOrderErrorMessage from "@saleor/utils/errors/order";
import React, { useRef } from "react";
import { useIntl } from "react-intl";
@ -23,6 +29,11 @@ const useStyles = makeStyles(
textRight: {
textAlign: "right",
},
textError: {
color: theme.palette.error.main,
marginLeft: theme.spacing(1.5),
display: "inline",
},
subtitle: {
color: theme.palette.grey[500],
paddingRight: theme.spacing(1),
@ -51,12 +62,14 @@ interface OrderDraftDetailsSummaryProps
extends OrderDiscountContextConsumerProps {
disabled?: boolean;
order: OrderDetailsFragment;
errors: OrderErrorFragment[];
onShippingMethodEdit: () => void;
}
const OrderDraftDetailsSummary: React.FC<OrderDraftDetailsSummaryProps> = props => {
const {
order,
errors,
onShippingMethodEdit,
orderDiscount,
addOrderDiscount,
@ -89,6 +102,8 @@ const OrderDraftDetailsSummary: React.FC<OrderDraftDetailsSummaryProps> = props
isShippingRequired,
} = order;
const formErrors = getFormErrors(["shipping"], errors);
const hasChosenShippingMethod =
shippingMethod !== null && shippingMethodName !== null;
@ -192,11 +207,18 @@ const OrderDraftDetailsSummary: React.FC<OrderDraftDetailsSummaryProps> = props
</td>
</tr>
<tr>
{hasShippingMethods && <td>{getShippingMethodComponent()}</td>}
<td>
{hasShippingMethods && getShippingMethodComponent()}
{!hasShippingMethods && (
<td>{intl.formatMessage(messages.noShippingCarriers)}</td>
{!hasShippingMethods &&
intl.formatMessage(messages.noShippingCarriers)}
{formErrors.shipping && (
<Typography variant="body2" className={classes.textError}>
{getOrderErrorMessage(formErrors.shipping, intl)}
</Typography>
)}
</td>
<td className={classes.textRight}>
{hasChosenShippingMethod ? (

View file

@ -0,0 +1,67 @@
import {
ChannelUsabilityDataQuery,
OrderDetailsFragment,
} from "@saleor/graphql";
import { Alert, AlertProps } from "@saleor/macaw-ui";
import React from "react";
import { MessageDescriptor, useIntl } from "react-intl";
import OrderAlerts from "../OrderAlerts";
import { alertMessages } from "./messages";
import { useAlertStyles } from "./styles";
const getAlerts = (
order?: OrderDetailsFragment,
channelUsabilityData?: ChannelUsabilityDataQuery,
) => {
const canDetermineShippingMethods =
order?.shippingAddress?.country.code && !!order?.lines?.length;
const isChannelInactive = order && !order.channel.isActive;
const noProductsInChannel = channelUsabilityData?.products.totalCount === 0;
const noShippingMethodsInChannel =
canDetermineShippingMethods && order?.shippingMethods.length === 0;
let alerts: MessageDescriptor[] = [];
if (isChannelInactive) {
alerts = [...alerts, alertMessages.inactiveChannel];
}
if (noProductsInChannel) {
alerts = [...alerts, alertMessages.noProductsInChannel];
}
if (noShippingMethodsInChannel) {
alerts = [...alerts, alertMessages.noShippingMethodsInChannel];
}
return alerts;
};
export type OrderDraftAlertProps = Omit<AlertProps, "variant" | "close"> & {
order?: OrderDetailsFragment;
channelUsabilityData?: ChannelUsabilityDataQuery;
};
const OrderDraftAlert: React.FC<OrderDraftAlertProps> = props => {
const { order, channelUsabilityData, ...alertProps } = props;
const classes = useAlertStyles();
const intl = useIntl();
const alerts = getAlerts(order, channelUsabilityData);
if (!alerts.length) {
return null;
}
return (
<Alert variant="warning" close className={classes.root} {...alertProps}>
<OrderAlerts
alerts={alerts}
alertsHeader={intl.formatMessage(alertMessages.manyAlerts)}
/>
</Alert>
);
};
OrderDraftAlert.displayName = "OrderDraftAlert";
export default OrderDraftAlert;

View file

@ -9,15 +9,17 @@ import PageHeader from "@saleor/components/PageHeader";
import Savebar from "@saleor/components/Savebar";
import Skeleton from "@saleor/components/Skeleton";
import {
ChannelUsabilityDataQuery,
OrderDetailsFragment,
OrderErrorFragment,
OrderLineInput,
SearchCustomersQuery,
} from "@saleor/graphql";
import { SubmitPromise } from "@saleor/hooks/useForm";
import useNavigator from "@saleor/hooks/useNavigator";
import { sectionNames } from "@saleor/intl";
import { ConfirmButtonTransitionState, makeStyles } from "@saleor/macaw-ui";
import DraftOrderChannelSectionCard from "@saleor/orders/components/DraftOrderChannelSectionCard";
import { ConfirmButtonTransitionState } from "@saleor/macaw-ui";
import OrderChannelSectionCard from "@saleor/orders/components/OrderChannelSectionCard";
import { orderDraftListUrl } from "@saleor/orders/urls";
import { FetchMoreProps, RelayToFlat } from "@saleor/types";
import React from "react";
@ -26,25 +28,16 @@ import { useIntl } from "react-intl";
import OrderCustomer, { CustomerEditData } from "../OrderCustomer";
import OrderDraftDetails from "../OrderDraftDetails/OrderDraftDetails";
import OrderHistory, { FormData as HistoryFormData } from "../OrderHistory";
const useStyles = makeStyles(
theme => ({
date: {
marginBottom: theme.spacing(3),
},
header: {
display: "flex",
marginBottom: 0,
},
}),
{ name: "OrderDraftPage" },
);
import OrderDraftAlert from "./OrderDraftAlert";
import { usePageStyles } from "./styles";
export interface OrderDraftPageProps extends FetchMoreProps {
disabled: boolean;
order: OrderDetailsFragment;
order?: OrderDetailsFragment;
channelUsabilityData?: ChannelUsabilityDataQuery;
users: RelayToFlat<SearchCustomersQuery["search"]>;
usersLoading: boolean;
errors: OrderErrorFragment[];
saveButtonBarState: ConfirmButtonTransitionState;
fetchUsers: (query: string) => void;
onBillingAddressEdit: () => void;
@ -80,10 +73,12 @@ const OrderDraftPage: React.FC<OrderDraftPageProps> = props => {
onShippingMethodEdit,
onProfileView,
order,
channelUsabilityData,
users,
usersLoading,
errors,
} = props;
const classes = useStyles(props);
const classes = usePageStyles(props);
const navigate = useNavigator();
const intl = useIntl();
@ -122,8 +117,14 @@ const OrderDraftPage: React.FC<OrderDraftPageProps> = props => {
</div>
<Grid>
<div>
<OrderDraftAlert
order={order}
channelUsabilityData={channelUsabilityData}
/>
<OrderDraftDetails
order={order}
channelUsabilityData={channelUsabilityData}
errors={errors}
onOrderLineAdd={onOrderLineAdd}
onOrderLineChange={onOrderLineChange}
onOrderLineRemove={onOrderLineRemove}
@ -136,12 +137,15 @@ const OrderDraftPage: React.FC<OrderDraftPageProps> = props => {
/>
</div>
<div>
<OrderChannelSectionCard channel={order?.channel} />
<CardSpacer />
<OrderCustomer
canEditAddresses={!!order?.user}
canEditCustomer={true}
fetchUsers={fetchUsers}
hasMore={hasMore}
loading={usersLoading}
errors={errors}
order={order}
users={users}
onBillingAddressEdit={onBillingAddressEdit}
@ -150,13 +154,11 @@ const OrderDraftPage: React.FC<OrderDraftPageProps> = props => {
onProfileView={onProfileView}
onShippingAddressEdit={onShippingAddressEdit}
/>
<CardSpacer />
<DraftOrderChannelSectionCard channelName={order?.channel?.name} />
</div>
</Grid>
<Savebar
state={saveButtonBarState}
disabled={disabled || !order?.canFinalize}
disabled={disabled}
onCancel={() => navigate(orderDraftListUrl())}
onSubmit={onDraftFinalize}
labels={{

View file

@ -0,0 +1,24 @@
import { defineMessages } from "react-intl";
export const alertMessages = defineMessages({
manyAlerts: {
id: "43AOvZ",
defaultMessage: "You will not be able to finalize this draft because:",
description: "alert group message",
},
inactiveChannel: {
id: "SPp3cx",
defaultMessage: "Orders cannot be placed in an inactive channel.",
description: "alert message",
},
noProductsInChannel: {
id: "O4QNFx",
defaultMessage: "There are no available products in this channel.",
description: "alert message",
},
noShippingMethodsInChannel: {
id: "BvRyoX",
defaultMessage: "There are no available shipping methods in this channel.",
description: "alert message",
},
});

View file

@ -0,0 +1,23 @@
import { makeStyles } from "@saleor/macaw-ui";
export const usePageStyles = makeStyles(
theme => ({
date: {
marginBottom: theme.spacing(3),
},
header: {
display: "flex",
marginBottom: 0,
},
}),
{ name: "OrderDraftPage" },
);
export const useAlertStyles = makeStyles(
theme => ({
root: {
marginBottom: theme.spacing(3),
},
}),
{ name: "OrderDraftAlert" },
);

View file

@ -9,6 +9,7 @@ import {
TableCell,
TableRow,
TextField,
Typography,
} from "@material-ui/core";
import BackButton from "@saleor/components/BackButton";
import Checkbox from "@saleor/components/Checkbox";
@ -21,7 +22,7 @@ import useModalDialogErrors from "@saleor/hooks/useModalDialogErrors";
import useModalDialogOpen from "@saleor/hooks/useModalDialogOpen";
import useSearchQuery from "@saleor/hooks/useSearchQuery";
import { buttonMessages } from "@saleor/intl";
import { ConfirmButtonTransitionState, makeStyles } from "@saleor/macaw-ui";
import { ConfirmButtonTransitionState } from "@saleor/macaw-ui";
import { maybe, renderCollection } from "@saleor/misc";
import { ChannelProps, FetchMoreProps, RelayToFlat } from "@saleor/types";
import getOrderErrorMessage from "@saleor/utils/errors/order";
@ -30,65 +31,14 @@ import InfiniteScroll from "react-infinite-scroll-component";
import { FormattedMessage, useIntl } from "react-intl";
import OrderPriceLabel from "../OrderPriceLabel/OrderPriceLabel";
const useStyles = makeStyles(
theme => ({
avatar: {
paddingLeft: 0,
width: 64,
},
colName: {
paddingLeft: 0,
},
colVariantCheckbox: {
padding: 0,
},
content: {
overflowY: "scroll",
paddingTop: 0,
marginBottom: theme.spacing(3),
},
grayText: {
color: theme.palette.text.disabled,
},
loadMoreLoaderContainer: {
alignItems: "center",
display: "flex",
height: theme.spacing(3),
justifyContent: "center",
marginTop: theme.spacing(3),
},
overflow: {
overflowY: "hidden",
},
topArea: {
overflowY: "hidden",
paddingBottom: theme.spacing(6),
margin: theme.spacing(0, 3, 3, 3),
},
productCheckboxCell: {
"&:first-child": {
paddingLeft: 0,
paddingRight: 0,
},
},
textRight: {
textAlign: "right",
},
variantCheckbox: {
left: theme.spacing(),
position: "relative",
},
wideCell: {
width: "100%",
},
}),
{ name: "OrderProductAddDialog" },
);
type SetVariantsAction = (
data: SearchOrderVariantQuery["search"]["edges"][0]["node"]["variants"],
) => void;
import { messages } from "./messages";
import { useStyles } from "./styles";
import {
hasAllVariantsSelected,
isVariantSelected,
onProductAdd,
onVariantAdd,
} from "./utils";
export interface OrderProductAddDialogProps
extends FetchMoreProps,
@ -104,69 +54,6 @@ export interface OrderProductAddDialogProps
) => void;
}
function hasAllVariantsSelected(
productVariants: SearchOrderVariantQuery["search"]["edges"][0]["node"]["variants"],
selectedVariantsToProductsMap: SearchOrderVariantQuery["search"]["edges"][0]["node"]["variants"],
): boolean {
return productVariants.reduce(
(acc, productVariant) =>
acc &&
!!selectedVariantsToProductsMap.find(
selectedVariant => selectedVariant.id === productVariant.id,
),
true,
);
}
function isVariantSelected(
variant: SearchOrderVariantQuery["search"]["edges"][0]["node"]["variants"][0],
selectedVariantsToProductsMap: SearchOrderVariantQuery["search"]["edges"][0]["node"]["variants"],
): boolean {
return !!selectedVariantsToProductsMap.find(
selectedVariant => selectedVariant.id === variant.id,
);
}
const onProductAdd = (
product: SearchOrderVariantQuery["search"]["edges"][0]["node"],
productIndex: number,
productsWithAllVariantsSelected: boolean[],
variants: SearchOrderVariantQuery["search"]["edges"][0]["node"]["variants"],
setVariants: SetVariantsAction,
) =>
productsWithAllVariantsSelected[productIndex]
? setVariants(
variants.filter(
selectedVariant =>
!product.variants.find(
productVariant => productVariant.id === selectedVariant.id,
),
),
)
: setVariants([
...variants,
...product.variants.filter(
productVariant =>
!variants.find(
selectedVariant => selectedVariant.id === productVariant.id,
),
),
]);
const onVariantAdd = (
variant: SearchOrderVariantQuery["search"]["edges"][0]["node"]["variants"][0],
variantIndex: number,
productIndex: number,
variants: SearchOrderVariantQuery["search"]["edges"][0]["node"]["variants"],
selectedVariantsToProductsMap: boolean[][],
setVariants: SetVariantsAction,
) =>
selectedVariantsToProductsMap[productIndex][variantIndex]
? setVariants(
variants.filter(selectedVariant => selectedVariant.id !== variant.id),
)
: setVariants([...variants, variant]);
const scrollableTargetId = "orderProductAddScrollableDialog";
const OrderProductAddDialog: React.FC<OrderProductAddDialogProps> = props => {
@ -249,26 +136,15 @@ const OrderProductAddDialog: React.FC<OrderProductAddDialogProps> = props => {
maxWidth="sm"
>
<DialogTitle>
<FormattedMessage
id="myyWNp"
defaultMessage="Add Product"
description="dialog header"
/>
<FormattedMessage {...messages.title} />
</DialogTitle>
<DialogContent className={classes.topArea} data-test-id="search-query">
<TextField
name="query"
value={query}
onChange={onQueryChange}
label={intl.formatMessage({
id: "/TF6BZ",
defaultMessage: "Search Products",
})}
placeholder={intl.formatMessage({
id: "SHm7ee",
defaultMessage:
"Search by product name, attribute, product type etc...",
})}
label={intl.formatMessage(messages.search)}
placeholder={intl.formatMessage(messages.searchPlaceholder)}
fullWidth
InputProps={{
autoComplete: "off",
@ -355,9 +231,7 @@ const OrderProductAddDialog: React.FC<OrderProductAddDialogProps> = props => {
{variant.sku && (
<div className={classes.grayText}>
<FormattedMessage
id="+HuipK"
defaultMessage="SKU {sku}"
description="variant sku"
{...messages.sku}
values={{
sku: variant.sku,
}}
@ -373,14 +247,11 @@ const OrderProductAddDialog: React.FC<OrderProductAddDialogProps> = props => {
</React.Fragment>
),
() => (
<TableRow>
<TableCell colSpan={4}>
<FormattedMessage
id="WQnltU"
defaultMessage="No products available in order channel matching given query"
/>
</TableCell>
</TableRow>
<Typography className={classes.noContentText}>
{!!query
? intl.formatMessage(messages.noProductsInQuery)
: intl.formatMessage(messages.noProductsInChannel)}
</Typography>
),
)}
</TableBody>

View file

@ -0,0 +1,36 @@
import { defineMessages } from "react-intl";
export const messages = defineMessages({
title: {
defaultMessage: "Add product",
description: "dialog header",
id: "9Y6vg+",
},
search: {
defaultMessage: "Search products",
description: "search label",
id: "s6oAC+",
},
searchPlaceholder: {
defaultMessage: "Search by product name, attribute, product type etc...",
description: "search placeholder",
id: "S2xLxV",
},
sku: {
defaultMessage: "SKU {sku}",
description: "variant sku",
id: "+HuipK",
},
noProductsInChannel: {
id: "shmSDX",
defaultMessage:
"No products are available in the channel assigned to this order.",
description: "no products placeholder",
},
noProductsInQuery: {
id: "9mrWKz",
defaultMessage:
"No products are available matching query in the channel assigned to this order.",
description: "no products placeholder",
},
});

View file

@ -0,0 +1,59 @@
import { makeStyles } from "@saleor/macaw-ui";
export const useStyles = makeStyles(
theme => ({
avatar: {
paddingLeft: 0,
width: 64,
},
colName: {
paddingLeft: 0,
},
colVariantCheckbox: {
padding: 0,
},
noContentText: {
marginBottom: theme.spacing(3),
},
content: {
overflowY: "scroll",
paddingTop: 0,
marginBottom: theme.spacing(3),
},
grayText: {
color: theme.palette.text.disabled,
},
loadMoreLoaderContainer: {
alignItems: "center",
display: "flex",
height: theme.spacing(3),
justifyContent: "center",
marginTop: theme.spacing(3),
},
overflow: {
overflowY: "hidden",
},
topArea: {
overflowY: "hidden",
paddingBottom: theme.spacing(6),
margin: theme.spacing(0, 3, 3, 3),
},
productCheckboxCell: {
"&:first-child": {
paddingLeft: 0,
paddingRight: 0,
},
},
textRight: {
textAlign: "right",
},
variantCheckbox: {
left: theme.spacing(),
position: "relative",
},
wideCell: {
width: "100%",
},
}),
{ name: "OrderProductAddDialog" },
);

View file

@ -0,0 +1,68 @@
import { SearchOrderVariantQuery } from "@saleor/graphql";
type SetVariantsAction = (
data: SearchOrderVariantQuery["search"]["edges"][0]["node"]["variants"],
) => void;
export function hasAllVariantsSelected(
productVariants: SearchOrderVariantQuery["search"]["edges"][0]["node"]["variants"],
selectedVariantsToProductsMap: SearchOrderVariantQuery["search"]["edges"][0]["node"]["variants"],
): boolean {
return productVariants.reduce(
(acc, productVariant) =>
acc &&
!!selectedVariantsToProductsMap.find(
selectedVariant => selectedVariant.id === productVariant.id,
),
true,
);
}
export function isVariantSelected(
variant: SearchOrderVariantQuery["search"]["edges"][0]["node"]["variants"][0],
selectedVariantsToProductsMap: SearchOrderVariantQuery["search"]["edges"][0]["node"]["variants"],
): boolean {
return !!selectedVariantsToProductsMap.find(
selectedVariant => selectedVariant.id === variant.id,
);
}
export const onProductAdd = (
product: SearchOrderVariantQuery["search"]["edges"][0]["node"],
productIndex: number,
productsWithAllVariantsSelected: boolean[],
variants: SearchOrderVariantQuery["search"]["edges"][0]["node"]["variants"],
setVariants: SetVariantsAction,
) =>
productsWithAllVariantsSelected[productIndex]
? setVariants(
variants.filter(
selectedVariant =>
!product.variants.find(
productVariant => productVariant.id === selectedVariant.id,
),
),
)
: setVariants([
...variants,
...product.variants.filter(
productVariant =>
!variants.find(
selectedVariant => selectedVariant.id === productVariant.id,
),
),
]);
export const onVariantAdd = (
variant: SearchOrderVariantQuery["search"]["edges"][0]["node"]["variants"][0],
variantIndex: number,
productIndex: number,
variants: SearchOrderVariantQuery["search"]["edges"][0]["node"]["variants"],
selectedVariantsToProductsMap: boolean[][],
setVariants: SetVariantsAction,
) =>
selectedVariantsToProductsMap[productIndex][variantIndex]
? setVariants(
variants.filter(selectedVariant => selectedVariant.id !== variant.id),
)
: setVariants([...variants, variant]);

View file

@ -1,4 +1,5 @@
import {
ChannelUsabilityDataQuery,
CountryWithCodeFragment,
FulfillmentStatus,
InvoiceFragment,
@ -1132,6 +1133,11 @@ export const order = (placeholder: string): OrderDetailsFragment => ({
id: "dsfsfuhb",
quantityAvailable: 10,
preorder: null,
product: {
__typename: "Product",
id: "UHJvZHVjdDo1",
channelListings: [],
},
stocks: [
{
id: "stock_test_id1",
@ -1241,6 +1247,11 @@ export const order = (placeholder: string): OrderDetailsFragment => ({
id: "dsfsfuhb",
quantityAvailable: 10,
preorder: null,
product: {
__typename: "Product",
id: "UHJvZHVjdDo1",
channelListings: [],
},
stocks: [
{
id: "stock_test_id1",
@ -1358,6 +1369,11 @@ export const order = (placeholder: string): OrderDetailsFragment => ({
id: "dsfsfuhb",
quantityAvailable: 10,
preorder: null,
product: {
__typename: "Product",
id: "UHJvZHVjdDo1",
channelListings: [],
},
stocks: [
{
id: "stock_test_id1",
@ -1453,6 +1469,11 @@ export const order = (placeholder: string): OrderDetailsFragment => ({
id: "dsfsfuhb",
quantityAvailable: 10,
preorder: null,
product: {
__typename: "Product",
id: "UHJvZHVjdDo1",
channelListings: [],
},
stocks: [
{
id: "stock_test_id1",
@ -1585,7 +1606,7 @@ export const draftOrder = (placeholder: string): OrderDetailsFragment => ({
__typename: "Order" as "Order",
giftCards: [],
actions: [OrderAction.CAPTURE],
shippingMethods: null,
shippingMethods: [],
billingAddress: null,
canFinalize: true,
channel: {
@ -1686,6 +1707,11 @@ export const draftOrder = (placeholder: string): OrderDetailsFragment => ({
id: "dsfsfuhb",
quantityAvailable: 10,
preorder: null,
product: {
__typename: "Product",
id: "UHJvZHVjdDo1",
channelListings: [],
},
stocks: [
{
id: "stock_test_id1",
@ -1780,6 +1806,11 @@ export const draftOrder = (placeholder: string): OrderDetailsFragment => ({
id: "dsfsfuhb",
quantityAvailable: 10,
preorder: null,
product: {
__typename: "Product",
id: "UHJvZHVjdDo1",
channelListings: [],
},
stocks: [
{
id: "stock_test_id1",
@ -2492,3 +2523,11 @@ export const warehouseSearch: SearchWarehousesQuery["search"] = {
},
__typename: "WarehouseCountableConnection",
};
export const channelUsabilityData: ChannelUsabilityDataQuery = {
__typename: "Query",
products: {
__typename: "ProductCountableConnection",
totalCount: 50,
},
};

View file

@ -422,7 +422,6 @@ export const fulfillOrder = gql`
errors {
...OrderError
warehouse
orderLines
}
order {
...OrderDetails

View file

@ -192,3 +192,11 @@ export const orderRefundData = gql`
}
}
`;
export const channelUsabilityData = gql`
query ChannelUsabilityData($channel: String!) {
products(channel: $channel) {
totalCount
}
}
`;

View file

@ -539,6 +539,11 @@ describe("Get the total value of all replaced products", () => {
quantityAvailable: 50,
preorder: null,
__typename: "ProductVariant",
product: {
__typename: "Product",
id: "UHJvZHVjdDo1",
channelListings: [],
},
stocks: [
{
id: "stock_test_id1",
@ -660,6 +665,11 @@ describe("Get the total value of all replaced products", () => {
},
],
__typename: "ProductVariant",
product: {
__typename: "Product",
id: "UHJvZHVjdDo1",
channelListings: [],
},
},
productName: "Lake Tunes",
productSku: "lake-tunes-mp3",
@ -755,6 +765,11 @@ describe("Get the total value of all replaced products", () => {
},
],
__typename: "ProductVariant",
product: {
__typename: "Product",
id: "UHJvZHVjdDo1",
channelListings: [],
},
},
productName: "T-shirt",
productSku: "29810068",
@ -856,6 +871,11 @@ describe("Get the total value of all replaced products", () => {
},
],
__typename: "ProductVariant",
product: {
__typename: "Product",
id: "UHJvZHVjdDo1",
channelListings: [],
},
},
productName: "Lake Tunes",
productSku: "lake-tunes-mp3",
@ -956,6 +976,11 @@ describe("Get the total value of all replaced products", () => {
},
],
__typename: "ProductVariant",
product: {
__typename: "Product",
id: "UHJvZHVjdDo1",
channelListings: [],
},
},
productName: "Lake Tunes",
productSku: "lake-tunes-mp3",
@ -1056,6 +1081,11 @@ describe("Get the total value of all replaced products", () => {
},
],
__typename: "ProductVariant",
product: {
__typename: "Product",
id: "UHJvZHVjdDo1",
channelListings: [],
},
},
productName: "T-shirt",
productSku: "29810068",
@ -1156,6 +1186,11 @@ describe("Get the total value of all replaced products", () => {
},
],
__typename: "ProductVariant",
product: {
__typename: "Product",
id: "UHJvZHVjdDo1",
channelListings: [],
},
},
productName: "Lake Tunes",
productSku: "lake-tunes-mp3",
@ -1256,6 +1291,11 @@ describe("Get the total value of all replaced products", () => {
},
],
__typename: "ProductVariant",
product: {
__typename: "Product",
id: "UHJvZHVjdDo1",
channelListings: [],
},
},
productName: "Lake Tunes",
productSku: "lake-tunes-mp3",
@ -1490,6 +1530,11 @@ describe("Get the total value of all selected products", () => {
},
],
__typename: "ProductVariant",
product: {
__typename: "Product",
id: "UHJvZHVjdDo1",
channelListings: [],
},
},
productName: "Lake Tunes",
productSku: "lake-tunes-mp3",
@ -1585,6 +1630,11 @@ describe("Get the total value of all selected products", () => {
},
],
__typename: "ProductVariant",
product: {
__typename: "Product",
id: "UHJvZHVjdDo1",
channelListings: [],
},
},
productName: "Lake Tunes",
productSku: "lake-tunes-mp3",
@ -1680,6 +1730,11 @@ describe("Get the total value of all selected products", () => {
},
],
__typename: "ProductVariant",
product: {
__typename: "Product",
id: "UHJvZHVjdDo1",
channelListings: [],
},
},
productName: "T-shirt",
productSku: "29810068",
@ -1781,6 +1836,11 @@ describe("Get the total value of all selected products", () => {
},
],
__typename: "ProductVariant",
product: {
__typename: "Product",
id: "UHJvZHVjdDo1",
channelListings: [],
},
},
productName: "Lake Tunes",
productSku: "lake-tunes-mp3",
@ -1881,6 +1941,11 @@ describe("Get the total value of all selected products", () => {
},
],
__typename: "ProductVariant",
product: {
__typename: "Product",
id: "UHJvZHVjdDo1",
channelListings: [],
},
},
productName: "Lake Tunes",
productSku: "lake-tunes-mp3",
@ -1981,6 +2046,11 @@ describe("Get the total value of all selected products", () => {
},
],
__typename: "ProductVariant",
product: {
__typename: "Product",
id: "UHJvZHVjdDo1",
channelListings: [],
},
},
productName: "T-shirt",
productSku: "29810068",
@ -2209,6 +2279,11 @@ describe("Merge repeated order lines of fulfillment lines", () => {
},
],
__typename: "ProductVariant",
product: {
__typename: "Product",
id: "UHJvZHVjdDo1",
channelListings: [],
},
},
productName: "Lake Tunes",
productSku: "lake-tunes-mp3",
@ -2309,6 +2384,11 @@ describe("Merge repeated order lines of fulfillment lines", () => {
},
],
__typename: "ProductVariant",
product: {
__typename: "Product",
id: "UHJvZHVjdDo1",
channelListings: [],
},
},
productName: "Lake Tunes",
productSku: "lake-tunes-mp3",
@ -2409,6 +2489,11 @@ describe("Merge repeated order lines of fulfillment lines", () => {
},
],
__typename: "ProductVariant",
product: {
__typename: "Product",
id: "UHJvZHVjdDo1",
channelListings: [],
},
},
productName: "T-shirt",
productSku: "29810068",

View file

@ -4,11 +4,14 @@ import {
OrderDetailsQuery,
OrderDraftCancelMutation,
OrderDraftCancelMutationVariables,
OrderDraftFinalizeMutation,
OrderDraftFinalizeMutationVariables,
OrderDraftUpdateMutation,
OrderDraftUpdateMutationVariables,
OrderLineUpdateMutation,
OrderLineUpdateMutationVariables,
StockAvailability,
useChannelUsabilityDataQuery,
useCustomerAddressesQuery,
} from "@saleor/graphql";
import useNavigator from "@saleor/hooks/useNavigator";
@ -67,7 +70,10 @@ interface OrderDraftDetailsProps {
OrderDraftCancelMutation,
OrderDraftCancelMutationVariables
>;
orderDraftFinalize: any;
orderDraftFinalize: PartialMutationProviderOutput<
OrderDraftFinalizeMutation,
OrderDraftFinalizeMutationVariables
>;
openModal: (action: OrderUrlDialog, newParams?: OrderUrlQueryParams) => void;
closeModal: any;
}
@ -98,6 +104,12 @@ export const OrderDraftDetails: React.FC<OrderDraftDetailsProps> = ({
const order = data.order;
const navigate = useNavigator();
const { data: channelUsabilityData } = useChannelUsabilityDataQuery({
variables: {
channel: order.channel.slug,
},
});
const {
loadMore,
search: variantSearch,
@ -184,6 +196,8 @@ export const OrderDraftDetails: React.FC<OrderDraftDetailsProps> = ({
return errors;
};
const errors = orderDraftFinalize.opts.data?.draftOrderComplete.errors || [];
return (
<>
<WindowTitle
@ -203,6 +217,7 @@ export const OrderDraftDetails: React.FC<OrderDraftDetailsProps> = ({
<OrderLineDiscountProvider order={order}>
<OrderDraftPage
disabled={loading}
errors={errors}
onNoteAdd={variables =>
extractMutationErrors(
orderAddNote.mutate({
@ -222,6 +237,7 @@ export const OrderDraftDetails: React.FC<OrderDraftDetailsProps> = ({
onDraftRemove={() => openModal("cancel")}
onOrderLineAdd={() => openModal("add-order-line")}
order={order}
channelUsabilityData={channelUsabilityData}
onProductClick={id => () =>
navigate(productUrl(encodeURIComponent(id)))}
onBillingAddressEdit={() => openModal("edit-billing-address")}

View file

@ -149,6 +149,8 @@ export const OrderNormalDetails: React.FC<OrderNormalDetailsProps> = ({
}
}, [approvalErrors]);
const errors = orderUpdate.opts.data?.orderUpdate.errors || [];
return (
<>
<WindowTitle
@ -168,6 +170,7 @@ export const OrderNormalDetails: React.FC<OrderNormalDetailsProps> = ({
disabled={
updateMetadataOpts.loading || updatePrivateMetadataOpts.loading
}
errors={errors}
onNoteAdd={variables =>
extractMutationErrors(
orderAddNote.mutate({

View file

@ -147,6 +147,8 @@ export const OrderUnconfirmedDetails: React.FC<OrderUnconfirmedDetailsProps> = (
const intl = useIntl();
const [transactionReference, setTransactionReference] = React.useState("");
const errors = orderUpdate.opts.data?.orderUpdate.errors || [];
return (
<>
<WindowTitle
@ -168,6 +170,7 @@ export const OrderUnconfirmedDetails: React.FC<OrderUnconfirmedDetailsProps> = (
disabled={
updateMetadataOpts.loading || updatePrivateMetadataOpts.loading
}
errors={errors}
onNoteAdd={variables =>
extractMutationErrors(
orderAddNote.mutate({

File diff suppressed because it is too large Load diff

View file

@ -38,6 +38,7 @@ storiesOf("Orders / OrderCancelDialog", module)
field: null,
addressType: null,
message: "Cannot cancel order",
orderLines: null,
},
]}
/>

View file

@ -20,6 +20,7 @@ const props: Omit<OrderCustomerProps, "classes"> = {
onShippingAddressEdit: undefined,
order,
users: clients,
errors: [],
};
const OrderCustomer = props => {

View file

@ -40,6 +40,7 @@ const props: Omit<OrderDetailsPageProps, "classes"> = {
onShippingAddressEdit: undefined,
onSubmit: () => undefined,
order,
errors: [],
shop: shopFixture,
saveButtonBarState: "default",
};

View file

@ -29,6 +29,7 @@ storiesOf("Orders / OrderDraftCancelDialog", module)
field: null,
addressType: null,
message: "Graphql Error",
orderLines: null,
},
]}
/>

View file

@ -1,16 +1,48 @@
import placeholderImage from "@assets/images/placeholder60x60.png";
import { fetchMoreProps } from "@saleor/fixtures";
import { OrderErrorCode, OrderErrorFragment } from "@saleor/graphql";
import { storiesOf } from "@storybook/react";
import React from "react";
import OrderDraftPageComponent, {
OrderDraftPageProps,
} from "../../../../orders/components/OrderDraftPage";
import { clients, draftOrder } from "../../../../orders/fixtures";
import {
channelUsabilityData,
clients,
draftOrder,
} from "../../../../orders/fixtures";
import Decorator from "../../../Decorator";
import { MockedUserProvider } from "../../customers/MockedUserProvider";
import { getDiscountsProvidersWrapper } from "./utils";
const finalizeErrors: OrderErrorFragment[] = [
{
code: OrderErrorCode.BILLING_ADDRESS_NOT_SET,
field: "order",
addressType: null,
message: "Can't finalize draft with no billing address.",
orderLines: null,
__typename: "OrderError",
},
{
code: OrderErrorCode.ORDER_NO_SHIPPING_ADDRESS,
field: "order",
addressType: null,
message: "Can't finalize draft with no shipping address.",
orderLines: null,
__typename: "OrderError",
},
{
code: OrderErrorCode.SHIPPING_METHOD_REQUIRED,
field: "shipping",
addressType: null,
message: "Shipping method is required.",
orderLines: null,
__typename: "OrderError",
},
];
const order = draftOrder(placeholderImage);
const props: Omit<OrderDraftPageProps, "classes"> = {
@ -30,7 +62,9 @@ const props: Omit<OrderDraftPageProps, "classes"> = {
onShippingAddressEdit: undefined,
onShippingMethodEdit: undefined,
order,
channelUsabilityData,
saveButtonBarState: "default",
errors: [],
users: clients,
usersLoading: false,
};
@ -52,11 +86,19 @@ storiesOf("Views / Orders / Order draft", module)
.addDecorator(DiscountsDecorator)
.add("default", () => <OrderDraftPage {...props} />)
.add("loading", () => (
<OrderDraftPage {...props} disabled={true} order={undefined} />
<OrderDraftPage
{...props}
disabled={true}
order={undefined}
channelUsabilityData={undefined}
/>
))
.add("without lines", () => (
<OrderDraftPage {...props} order={{ ...order, lines: [] }} />
))
.add("no user permissions", () => (
<OrderDraftPage {...props} customPermissions={[]} />
))
.add("with errors", () => (
<OrderDraftPage {...props} errors={finalizeErrors} />
));

View file

@ -30,6 +30,7 @@ storiesOf("Orders / OrderFulfillmentCancelDialog", module)
field: null,
addressType: null,
message: "Graphql Error",
orderLines: null,
},
]}
/>

View file

@ -29,6 +29,7 @@ storiesOf("Orders / OrderFulfillmentTrackingDialog", module)
field: null,
addressType: null,
message: "Graphql Error",
orderLines: null,
},
{
__typename: "OrderError",
@ -36,6 +37,7 @@ storiesOf("Orders / OrderFulfillmentTrackingDialog", module)
field: "trackingNumber",
addressType: null,
message: "Tracking number field invalid",
orderLines: null,
},
]}
/>

View file

@ -30,6 +30,7 @@ storiesOf("Orders / OrderMarkAsPaidDialog", module)
field: null,
addressType: null,
message: "Graphql error",
orderLines: null,
},
]}
/>

View file

@ -29,6 +29,7 @@ storiesOf("Orders / OrderPaymentDialog", module)
field: null,
addressType: null,
message: "Capture inactive payment",
orderLines: null,
},
{
__typename: "OrderError",
@ -36,6 +37,7 @@ storiesOf("Orders / OrderPaymentDialog", module)
field: "payment",
addressType: null,
message: "Payment field invalid",
orderLines: null,
},
]}
/>

View file

@ -28,6 +28,7 @@ storiesOf("Orders / OrderPaymentVoidDialog", module)
field: null,
addressType: null,
message: "Void inactive payment Error",
orderLines: null,
},
]}
/>

View file

@ -37,6 +37,7 @@ storiesOf("Orders / OrderProductAddDialog", module)
field: null,
addressType: null,
message: "Graphql Error",
orderLines: null,
},
]}
/>

View file

@ -32,6 +32,7 @@ storiesOf("Orders / OrderShippingMethodEditDialog", module)
field: "shippingMethod",
addressType: null,
message: "Shipping method not applicable",
orderLines: null,
},
{
__typename: "OrderError",
@ -39,6 +40,7 @@ storiesOf("Orders / OrderShippingMethodEditDialog", module)
field: null,
addressType: null,
message: "Graphql error",
orderLines: null,
},
]}
/>