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:
parent
fc15227b4c
commit
f01966f0d4
60 changed files with 3138 additions and 1427 deletions
|
@ -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"]',
|
||||
};
|
||||
|
|
|
@ -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"]',
|
||||
};
|
||||
|
|
|
@ -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
4
package-lock.json
generated
|
@ -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",
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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",
|
||||
},
|
||||
});
|
||||
|
|
|
@ -12,6 +12,9 @@ export const useStyles = makeStyles(
|
|||
colVariantCheckbox: {
|
||||
padding: 0,
|
||||
},
|
||||
noContentText: {
|
||||
marginBottom: theme.spacing(3),
|
||||
},
|
||||
content: {
|
||||
overflowY: "scroll",
|
||||
paddingTop: 0,
|
||||
|
|
63
src/components/AssignVariantDialog/utils.ts
Normal file
63
src/components/AssignVariantDialog/utils.ts
Normal 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,
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -73,6 +73,7 @@ export const orderErrorFragment = gql`
|
|||
field
|
||||
addressType
|
||||
message
|
||||
orderLines
|
||||
}
|
||||
`;
|
||||
|
||||
|
|
|
@ -91,6 +91,17 @@ export const fragmentOrderLine = gql`
|
|||
stocks {
|
||||
...Stock
|
||||
}
|
||||
product {
|
||||
id
|
||||
channelListings {
|
||||
id
|
||||
isPublished
|
||||
isAvailableForPurchase
|
||||
channel {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
productName
|
||||
productSku
|
||||
|
|
|
@ -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
|
@ -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} />
|
||||
));
|
|
@ -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;
|
|
@ -1,2 +0,0 @@
|
|||
export { default } from "./DraftOrderChannelSectionCard";
|
||||
export * from "./DraftOrderChannelSectionCard";
|
45
src/orders/components/OrderAlerts/OrderAlerts.tsx
Normal file
45
src/orders/components/OrderAlerts/OrderAlerts.tsx
Normal 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;
|
2
src/orders/components/OrderAlerts/index.ts
Normal file
2
src/orders/components/OrderAlerts/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export { default } from "./OrderAlerts";
|
||||
export * from "./OrderAlerts";
|
|
@ -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} />
|
||||
));
|
||||
|
|
|
@ -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>
|
||||
|
|
28
src/orders/components/OrderCustomer/AddrssTextError.tsx
Normal file
28
src/orders/components/OrderCustomer/AddrssTextError.tsx
Normal 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 />
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
return "";
|
||||
};
|
||||
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 (
|
||||
<Card>
|
||||
|
@ -295,40 +263,25 @@ const OrderCustomer: React.FC<OrderCustomerProps> = props => {
|
|||
</div>
|
||||
{shippingAddress === undefined ? (
|
||||
<Skeleton />
|
||||
) : shippingAddress === null ? (
|
||||
<Typography>
|
||||
<FormattedMessage
|
||||
id="e7yOai"
|
||||
defaultMessage="Not set"
|
||||
description="shipping address is not set in draft order"
|
||||
/>
|
||||
</Typography>
|
||||
) : (
|
||||
<>
|
||||
{shippingAddress.companyName && (
|
||||
<Typography>{shippingAddress.companyName}</Typography>
|
||||
{noShippingAddressError && (
|
||||
<AddressTextError orderError={noShippingAddressError} />
|
||||
)}
|
||||
{shippingAddress === null ? (
|
||||
<Typography>
|
||||
<FormattedMessage
|
||||
id="e7yOai"
|
||||
defaultMessage="Not set"
|
||||
description="shipping address is not set in draft order"
|
||||
/>
|
||||
</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,47 +306,30 @@ const OrderCustomer: React.FC<OrderCustomerProps> = props => {
|
|||
</div>
|
||||
{billingAddress === undefined ? (
|
||||
<Skeleton />
|
||||
) : billingAddress === null ? (
|
||||
<Typography>
|
||||
<FormattedMessage
|
||||
id="YI6Fhj"
|
||||
defaultMessage="Not set"
|
||||
description="no address is set in draft order"
|
||||
/>
|
||||
</Typography>
|
||||
) : maybe(() => shippingAddress.id) === billingAddress.id ? (
|
||||
<Typography>
|
||||
<FormattedMessage
|
||||
id="GLX9II"
|
||||
defaultMessage="Same as shipping address"
|
||||
description="billing address"
|
||||
/>
|
||||
</Typography>
|
||||
) : (
|
||||
<>
|
||||
{billingAddress.companyName && (
|
||||
<Typography>{billingAddress.companyName}</Typography>
|
||||
{noBillingAddressError && (
|
||||
<AddressTextError orderError={noBillingAddressError} />
|
||||
)}
|
||||
{billingAddress === null ? (
|
||||
<Typography>
|
||||
<FormattedMessage
|
||||
id="YI6Fhj"
|
||||
defaultMessage="Not set"
|
||||
description="no address is set in draft order"
|
||||
/>
|
||||
</Typography>
|
||||
) : maybe(() => shippingAddress.id) === billingAddress.id ? (
|
||||
<Typography>
|
||||
<FormattedMessage
|
||||
id="GLX9II"
|
||||
defaultMessage="Same as shipping address"
|
||||
description="billing address"
|
||||
/>
|
||||
</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>
|
||||
|
|
35
src/orders/components/OrderCustomer/PickupAnnotation.tsx
Normal file
35
src/orders/components/OrderCustomer/PickupAnnotation.tsx
Normal 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;
|
||||
};
|
34
src/orders/components/OrderCustomer/styles.ts
Normal file
34
src/orders/components/OrderCustomer/styles.ts
Normal 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" },
|
||||
);
|
|
@ -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 && (
|
||||
<>
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
@ -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"
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<>
|
||||
<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>
|
||||
|
|
|
@ -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)}
|
||||
|
|
|
@ -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;
|
14
src/orders/components/OrderDraftDetailsProducts/messages.ts
Normal file
14
src/orders/components/OrderDraftDetailsProducts/messages.ts
Normal 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",
|
||||
},
|
||||
});
|
|
@ -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;
|
|
@ -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 ? (
|
||||
|
|
67
src/orders/components/OrderDraftPage/OrderDraftAlert.tsx
Normal file
67
src/orders/components/OrderDraftPage/OrderDraftAlert.tsx
Normal 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;
|
|
@ -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={{
|
||||
|
|
24
src/orders/components/OrderDraftPage/messages.ts
Normal file
24
src/orders/components/OrderDraftPage/messages.ts
Normal 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",
|
||||
},
|
||||
});
|
23
src/orders/components/OrderDraftPage/styles.ts
Normal file
23
src/orders/components/OrderDraftPage/styles.ts
Normal 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" },
|
||||
);
|
|
@ -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>
|
||||
|
|
36
src/orders/components/OrderProductAddDialog/messages.ts
Normal file
36
src/orders/components/OrderProductAddDialog/messages.ts
Normal 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",
|
||||
},
|
||||
});
|
59
src/orders/components/OrderProductAddDialog/styles.ts
Normal file
59
src/orders/components/OrderProductAddDialog/styles.ts
Normal 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" },
|
||||
);
|
68
src/orders/components/OrderProductAddDialog/utils.ts
Normal file
68
src/orders/components/OrderProductAddDialog/utils.ts
Normal 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]);
|
|
@ -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,
|
||||
},
|
||||
};
|
||||
|
|
|
@ -422,7 +422,6 @@ export const fulfillOrder = gql`
|
|||
errors {
|
||||
...OrderError
|
||||
warehouse
|
||||
orderLines
|
||||
}
|
||||
order {
|
||||
...OrderDetails
|
||||
|
|
|
@ -192,3 +192,11 @@ export const orderRefundData = gql`
|
|||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const channelUsabilityData = gql`
|
||||
query ChannelUsabilityData($channel: String!) {
|
||||
products(channel: $channel) {
|
||||
totalCount
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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")}
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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
|
@ -38,6 +38,7 @@ storiesOf("Orders / OrderCancelDialog", module)
|
|||
field: null,
|
||||
addressType: null,
|
||||
message: "Cannot cancel order",
|
||||
orderLines: null,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
|
|
@ -20,6 +20,7 @@ const props: Omit<OrderCustomerProps, "classes"> = {
|
|||
onShippingAddressEdit: undefined,
|
||||
order,
|
||||
users: clients,
|
||||
errors: [],
|
||||
};
|
||||
|
||||
const OrderCustomer = props => {
|
||||
|
|
|
@ -40,6 +40,7 @@ const props: Omit<OrderDetailsPageProps, "classes"> = {
|
|||
onShippingAddressEdit: undefined,
|
||||
onSubmit: () => undefined,
|
||||
order,
|
||||
errors: [],
|
||||
shop: shopFixture,
|
||||
saveButtonBarState: "default",
|
||||
};
|
||||
|
|
|
@ -29,6 +29,7 @@ storiesOf("Orders / OrderDraftCancelDialog", module)
|
|||
field: null,
|
||||
addressType: null,
|
||||
message: "Graphql Error",
|
||||
orderLines: null,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
|
|
@ -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} />
|
||||
));
|
||||
|
|
|
@ -30,6 +30,7 @@ storiesOf("Orders / OrderFulfillmentCancelDialog", module)
|
|||
field: null,
|
||||
addressType: null,
|
||||
message: "Graphql Error",
|
||||
orderLines: null,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
|
|
@ -30,6 +30,7 @@ storiesOf("Orders / OrderMarkAsPaidDialog", module)
|
|||
field: null,
|
||||
addressType: null,
|
||||
message: "Graphql error",
|
||||
orderLines: null,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
|
|
@ -28,6 +28,7 @@ storiesOf("Orders / OrderPaymentVoidDialog", module)
|
|||
field: null,
|
||||
addressType: null,
|
||||
message: "Void inactive payment Error",
|
||||
orderLines: null,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
|
|
@ -37,6 +37,7 @@ storiesOf("Orders / OrderProductAddDialog", module)
|
|||
field: null,
|
||||
addressType: null,
|
||||
message: "Graphql Error",
|
||||
orderLines: null,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
|
Loading…
Reference in a new issue