Order confirmation (#840)

* Order confirmed webhook

* Add status chip component and update order details page header

* refactor

* refactor and update types

* Remove rebase leftovers

* Create order settings page

* Add order setting query

* Connct order settings view with api

* Show order settings update error message

* Popup menu routing to order settings

* Fix circular import error with order settings form types

* Update order settings story

* Update order settings messages

* wip

* Add order confirm mutation and types

* Add confirm order feature to order details

* Update global types

* refactor after review

* refactor after review

* update types

* Revert settings menu removal

* Add changelog entry

Co-authored-by: Tomasz Szymanski <lime129@gmail.com>
Co-authored-by: Magdalena Markusik <magdalena.markusik@mirumee.com>
Co-authored-by: Dawid Tarasiuk <tarasiukdawid@gmail.com>
This commit is contained in:
Krzysztof Wolski 2020-11-30 14:19:57 +01:00 committed by GitHub
parent 6456cd21d2
commit 07f8f4b0b8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
38 changed files with 2040 additions and 32 deletions

View file

@ -4,6 +4,7 @@ All notable, unreleased changes to this project will be documented in this file.
## [Unreleased] ## [Unreleased]
- Add Order Confirmation settings - #840 by @orzechdev and @mmarkusik
- Add Page Types - #807 by @orzechdev - Add Page Types - #807 by @orzechdev
- Add shipping methods to translation section - #864 by @marekchoinski - Add shipping methods to translation section - #864 by @marekchoinski

View file

@ -3093,6 +3093,10 @@
"context": "button", "context": "button",
"string": "Cancel order" "string": "Cancel order"
}, },
"src_dot_orders_dot_components_dot_OrderDetailsPage_dot_3086420445": {
"context": "save button",
"string": "confirm order"
},
"src_dot_orders_dot_components_dot_OrderDraftCancelDialog_dot_1961675716": { "src_dot_orders_dot_components_dot_OrderDraftCancelDialog_dot_1961675716": {
"context": "dialog header", "context": "dialog header",
"string": "Delete Daft Order" "string": "Delete Daft Order"
@ -3371,6 +3375,10 @@
"context": "order history message", "context": "order history message",
"string": "Order was fully paid" "string": "Order was fully paid"
}, },
"src_dot_orders_dot_components_dot_OrderHistory_dot_276183305": {
"context": "order history message",
"string": "Order was confirmed"
},
"src_dot_orders_dot_components_dot_OrderHistory_dot_2770854455": { "src_dot_orders_dot_components_dot_OrderHistory_dot_2770854455": {
"context": "order history message", "context": "order history message",
"string": "Payment was captured" "string": "Payment was captured"
@ -3472,6 +3480,10 @@
"context": "generate invoice button", "context": "generate invoice button",
"string": "Generate" "string": "Generate"
}, },
"src_dot_orders_dot_components_dot_OrderListPage_dot_2225897825": {
"context": "button",
"string": "Order Settings"
},
"src_dot_orders_dot_components_dot_OrderListPage_dot_2826235371": { "src_dot_orders_dot_components_dot_OrderListPage_dot_2826235371": {
"context": "button", "context": "button",
"string": "Create order" "string": "Create order"
@ -3621,6 +3633,25 @@
"src_dot_orders_dot_components_dot_OrderProductAddDialog_dot_353369701": { "src_dot_orders_dot_components_dot_OrderProductAddDialog_dot_353369701": {
"string": "No products matching given query" "string": "No products matching given query"
}, },
"src_dot_orders_dot_components_dot_OrderSettingsPage_dot_1149215359": {
"context": "header",
"string": "Order settings"
},
"src_dot_orders_dot_components_dot_OrderSettingsPage_dot_3782847217": {
"string": "General Settings"
},
"src_dot_orders_dot_components_dot_OrderSettings_dot_1391686013": {
"context": "section header",
"string": "Settings"
},
"src_dot_orders_dot_components_dot_OrderSettings_dot_3281882935": {
"context": "checkbox label description",
"string": "All orders will be automatically confirmed and all payments will be captured."
},
"src_dot_orders_dot_components_dot_OrderSettings_dot_513393066": {
"context": "checkbox label",
"string": "Automatically confirm all orders"
},
"src_dot_orders_dot_components_dot_OrderShippingMethodEditDialog_dot_3369240294": { "src_dot_orders_dot_components_dot_OrderShippingMethodEditDialog_dot_3369240294": {
"context": "dialog header", "context": "dialog header",
"string": "Edit Shipping Method" "string": "Edit Shipping Method"
@ -5934,6 +5965,10 @@
"src_dot_translations_dot_components_dot_TranslationsVouchersPage_dot_2599922713": { "src_dot_translations_dot_components_dot_TranslationsVouchersPage_dot_2599922713": {
"string": "Voucher Name" "string": "Voucher Name"
}, },
"src_dot_unconfirmed": {
"context": "order status",
"string": "Unconfirmed"
},
"src_dot_undo": { "src_dot_undo": {
"context": "button", "context": "button",
"string": "Undo" "string": "Undo"
@ -6229,6 +6264,10 @@
"context": "event", "context": "event",
"string": "Checkout updated" "string": "Checkout updated"
}, },
"src_dot_webhooks_dot_components_dot_WebhookEvents_dot_1366770851": {
"context": "event",
"string": "Order confirmed"
},
"src_dot_webhooks_dot_components_dot_WebhookEvents_dot_1368317066": { "src_dot_webhooks_dot_components_dot_WebhookEvents_dot_1368317066": {
"context": "event", "context": "event",
"string": "Invoice deleted" "string": "Invoice deleted"

View file

@ -2554,6 +2554,7 @@ type Mutation {
shopFetchTaxRates: ShopFetchTaxRates shopFetchTaxRates: ShopFetchTaxRates
shopSettingsTranslate(input: ShopSettingsTranslationInput!, languageCode: LanguageCodeEnum!): ShopSettingsTranslate shopSettingsTranslate(input: ShopSettingsTranslationInput!, languageCode: LanguageCodeEnum!): ShopSettingsTranslate
shopAddressUpdate(input: AddressInput): ShopAddressUpdate shopAddressUpdate(input: AddressInput): ShopAddressUpdate
orderSettingsUpdate(input: OrderSettingsUpdateInput!): OrderSettingsUpdate
shippingMethodChannelListingUpdate(id: ID!, input: ShippingMethodChannelListingInput!): ShippingMethodChannelListingUpdate shippingMethodChannelListingUpdate(id: ID!, input: ShippingMethodChannelListingInput!): ShippingMethodChannelListingUpdate
shippingPriceCreate(input: ShippingPriceInput!): ShippingPriceCreate shippingPriceCreate(input: ShippingPriceInput!): ShippingPriceCreate
shippingPriceDelete(id: ID!): ShippingPriceDelete shippingPriceDelete(id: ID!): ShippingPriceDelete
@ -2643,6 +2644,7 @@ type Mutation {
orderAddNote(order: ID!, input: OrderAddNoteInput!): OrderAddNote orderAddNote(order: ID!, input: OrderAddNoteInput!): OrderAddNote
orderCancel(id: ID!): OrderCancel orderCancel(id: ID!): OrderCancel
orderCapture(amount: PositiveDecimal!, id: ID!): OrderCapture orderCapture(amount: PositiveDecimal!, id: ID!): OrderCapture
orderConfirm(id: ID!): OrderConfirm
orderFulfill(input: OrderFulfillInput!, order: ID): OrderFulfill orderFulfill(input: OrderFulfillInput!, order: ID): OrderFulfill
orderFulfillmentCancel(id: ID!, input: FulfillmentCancelInput!): FulfillmentCancel orderFulfillmentCancel(id: ID!, input: FulfillmentCancelInput!): FulfillmentCancel
orderFulfillmentUpdateTracking(id: ID!, input: FulfillmentUpdateTrackingInput!): FulfillmentUpdateTracking orderFulfillmentUpdateTracking(id: ID!, input: FulfillmentUpdateTrackingInput!): FulfillmentUpdateTracking
@ -2882,6 +2884,12 @@ type OrderCapture {
orderErrors: [OrderError!]! orderErrors: [OrderError!]!
} }
type OrderConfirm {
errors: [Error!]! @deprecated(reason: "Use typed errors with error codes. This field will be removed after 2020-07-31.")
order: Order
orderErrors: [OrderError!]!
}
type OrderCountableConnection { type OrderCountableConnection {
pageInfo: PageInfo! pageInfo: PageInfo!
edges: [OrderCountableEdge!]! edges: [OrderCountableEdge!]!
@ -2984,6 +2992,7 @@ type OrderEventOrderLineObject {
enum OrderEventsEmailsEnum { enum OrderEventsEmailsEnum {
PAYMENT_CONFIRMATION PAYMENT_CONFIRMATION
CONFIRMED
SHIPPING_CONFIRMATION SHIPPING_CONFIRMATION
TRACKING_UPDATED TRACKING_UPDATED
ORDER_CONFIRMATION ORDER_CONFIRMATION
@ -3005,6 +3014,7 @@ enum OrderEventsEnum {
ORDER_FULLY_PAID ORDER_FULLY_PAID
UPDATED_ADDRESS UPDATED_ADDRESS
EMAIL_SENT EMAIL_SENT
CONFIRMED
PAYMENT_AUTHORIZED PAYMENT_AUTHORIZED
PAYMENT_CAPTURED PAYMENT_CAPTURED
EXTERNAL_SERVICE_NOTIFICATION EXTERNAL_SERVICE_NOTIFICATION
@ -3093,6 +3103,30 @@ type OrderRefund {
orderErrors: [OrderError!]! orderErrors: [OrderError!]!
} }
type OrderSettings {
automaticallyConfirmAllNewOrders: Boolean!
}
type OrderSettingsError {
field: String
message: String
code: OrderSettingsErrorCode!
}
enum OrderSettingsErrorCode {
INVALID
}
type OrderSettingsUpdate {
errors: [Error!]! @deprecated(reason: "Use typed errors with error codes. This field will be removed after 2020-07-31.")
orderSettings: OrderSettings
orderSettingsErrors: [OrderSettingsError!]!
}
input OrderSettingsUpdateInput {
automaticallyConfirmAllNewOrders: Boolean!
}
enum OrderSortField { enum OrderSortField {
NUMBER NUMBER
CREATION_DATE CREATION_DATE
@ -3108,6 +3142,7 @@ input OrderSortingInput {
enum OrderStatus { enum OrderStatus {
DRAFT DRAFT
UNCONFIRMED
UNFULFILLED UNFULFILLED
PARTIALLY_FULFILLED PARTIALLY_FULFILLED
FULFILLED FULFILLED
@ -3118,6 +3153,7 @@ enum OrderStatusFilter {
READY_TO_FULFILL READY_TO_FULFILL
READY_TO_CAPTURE READY_TO_CAPTURE
UNFULFILLED UNFULFILLED
UNCONFIRMED
PARTIALLY_FULFILLED PARTIALLY_FULFILLED
FULFILLED FULFILLED
CANCELED CANCELED
@ -4291,6 +4327,7 @@ type Query {
stock(id: ID!): Stock stock(id: ID!): Stock
stocks(filter: StockFilterInput, before: String, after: String, first: Int, last: Int): StockCountableConnection stocks(filter: StockFilterInput, before: String, after: String, first: Int, last: Int): StockCountableConnection
shop: Shop! shop: Shop!
orderSettings: OrderSettings
shippingZone(id: ID!, channel: String): ShippingZone shippingZone(id: ID!, channel: String): ShippingZone
shippingZones(channel: String, before: String, after: String, first: Int, last: Int): ShippingZoneCountableConnection shippingZones(channel: String, before: String, after: String, first: Int, last: Int): ShippingZoneCountableConnection
digitalContent(id: ID!): DigitalContent digitalContent(id: ID!): DigitalContent
@ -5598,6 +5635,7 @@ type WebhookEvent {
enum WebhookEventTypeEnum { enum WebhookEventTypeEnum {
ANY_EVENTS ANY_EVENTS
ORDER_CREATED ORDER_CREATED
ORDER_CONFIRMED
ORDER_FULLY_PAID ORDER_FULLY_PAID
ORDER_UPDATED ORDER_UPDATED
ORDER_CANCELLED ORDER_CANCELLED
@ -5616,6 +5654,7 @@ enum WebhookEventTypeEnum {
enum WebhookSampleEventTypeEnum { enum WebhookSampleEventTypeEnum {
ORDER_CREATED ORDER_CREATED
ORDER_CONFIRMED
ORDER_FULLY_PAID ORDER_FULLY_PAID
ORDER_UPDATED ORDER_UPDATED
ORDER_CANCELLED ORDER_CANCELLED

View file

@ -0,0 +1,13 @@
import Decorator from "@saleor/storybook/Decorator";
import { storiesOf } from "@storybook/react";
import React from "react";
import StatusChip from "./StatusChip";
import { StatusType } from "./types";
storiesOf("Generics / Status Chip", module)
.addDecorator(Decorator)
.add("neutral", () => <StatusChip label="label" type={StatusType.NEUTRAL} />)
.add("error", () => <StatusChip label="label" type={StatusType.ERROR} />)
.add("success", () => <StatusChip label="label" type={StatusType.SUCCESS} />)
.add("alert", () => <StatusChip label="label" type={StatusType.ALERT} />);

View file

@ -0,0 +1,76 @@
import { makeStyles } from "@material-ui/core/styles";
import Typography from "@material-ui/core/Typography";
import classNames from "classnames";
import React from "react";
import { StatusType } from "./types";
export interface StatusChipProps {
type?: StatusType;
label?: string;
}
const StatusChipStyles = {
alert: {
background: "#FFF4E4"
},
alertLabel: {
color: "#FFB84E"
},
error: {
backgroundColor: "rgba(254, 110, 118, 0.15)"
},
errorLabel: {
color: "#FE6E76"
},
neutral: {
background: "rgba(40, 35, 74, 0.1)"
},
neutralLabel: {
color: "#28234A"
},
success: {
background: "rgba(93, 194, 146, 0.3)"
},
successLabel: {
color: "#5DC292"
}
};
const useStyles = makeStyles(
theme => ({
label: {
fontSize: "1rem",
fontWeight: theme.typography.fontWeightBold,
textTransform: "uppercase"
},
root: {
borderRadius: 22,
display: "inline-block",
padding: "8px 24px"
},
...StatusChipStyles
}),
{ name: "StatusChip" }
);
const StatusChip: React.FC<StatusChipProps> = props => {
const { type = StatusType.NEUTRAL, label } = props;
const classes = useStyles(props);
if (!label) {
return null;
}
return (
<div className={classNames(classes.root, classes[type])}>
<Typography
className={classNames(classes.label, classes[`${type}Label`])}
>
{label}
</Typography>
</div>
);
};
export default StatusChip;

View file

@ -0,0 +1 @@
export { default } from "./StatusChip";

View file

@ -0,0 +1,6 @@
export enum StatusType {
NEUTRAL = "neutral",
ERROR = "error",
ALERT = "alert",
SUCCESS = "success"
}

View file

@ -69,6 +69,13 @@ export const orderErrorFragment = gql`
} }
`; `;
export const orderSettingsErrorFragment = gql`
fragment OrderSettingsErrorFragment on OrderSettingsError {
code
field
}
`;
export const pageErrorFragment = gql` export const pageErrorFragment = gql`
fragment PageErrorFragment on PageError { fragment PageErrorFragment on PageError {
code code

View file

@ -175,3 +175,9 @@ export const fragmentOrderDetails = gql`
isPaid isPaid
} }
`; `;
export const fragmentOrderSettings = gql`
fragment OrderSettingsFragment on OrderSettings {
automaticallyConfirmAllNewOrders
}
`;

View file

@ -0,0 +1,15 @@
/* tslint:disable */
/* eslint-disable */
// This file was automatically generated and should not be edited.
import { OrderSettingsErrorCode } from "./../../types/globalTypes";
// ====================================================
// GraphQL fragment: OrderSettingsErrorFragment
// ====================================================
export interface OrderSettingsErrorFragment {
__typename: "OrderSettingsError";
code: OrderSettingsErrorCode;
field: string | null;
}

View file

@ -0,0 +1,12 @@
/* tslint:disable */
/* eslint-disable */
// This file was automatically generated and should not be edited.
// ====================================================
// GraphQL fragment: OrderSettingsFragment
// ====================================================
export interface OrderSettingsFragment {
__typename: "OrderSettings";
automaticallyConfirmAllNewOrders: boolean;
}

View file

@ -4,6 +4,7 @@ import { defineMessages, IntlShape } from "react-intl";
import urlJoin from "url-join"; import urlJoin from "url-join";
import { ConfirmButtonTransitionState } from "./components/ConfirmButton/ConfirmButton"; import { ConfirmButtonTransitionState } from "./components/ConfirmButton/ConfirmButton";
import { StatusType } from "./components/StatusChip/types";
import { APP_MOUNT_URI } from "./config"; import { APP_MOUNT_URI } from "./config";
import { AddressType, AddressTypeInput } from "./customers/types"; import { AddressType, AddressTypeInput } from "./customers/types";
import { import {
@ -144,43 +145,55 @@ export const orderStatusMessages = defineMessages({
defaultMessage: "Ready to fulfill", defaultMessage: "Ready to fulfill",
description: "order status" description: "order status"
}, },
unconfirmed: {
defaultMessage: "Unconfirmed",
description: "order status"
},
unfulfilled: { unfulfilled: {
defaultMessage: "Unfulfilled", defaultMessage: "Unfulfilled",
description: "order status" description: "order status"
} }
}); });
export const transformOrderStatus = (status: string, intl: IntlShape) => { export const transformOrderStatus = (
status: string,
intl: IntlShape
): { localized: string; status: StatusType } => {
switch (status) { switch (status) {
case OrderStatus.FULFILLED: case OrderStatus.FULFILLED:
return { return {
localized: intl.formatMessage(orderStatusMessages.fulfilled), localized: intl.formatMessage(orderStatusMessages.fulfilled),
status: "success" status: StatusType.SUCCESS
}; };
case OrderStatus.PARTIALLY_FULFILLED: case OrderStatus.PARTIALLY_FULFILLED:
return { return {
localized: intl.formatMessage(orderStatusMessages.partiallyFulfilled), localized: intl.formatMessage(orderStatusMessages.partiallyFulfilled),
status: "neutral" status: StatusType.NEUTRAL
}; };
case OrderStatus.UNFULFILLED: case OrderStatus.UNFULFILLED:
return { return {
localized: intl.formatMessage(orderStatusMessages.unfulfilled), localized: intl.formatMessage(orderStatusMessages.unfulfilled),
status: "error" status: StatusType.ERROR
}; };
case OrderStatus.CANCELED: case OrderStatus.CANCELED:
return { return {
localized: intl.formatMessage(orderStatusMessages.cancelled), localized: intl.formatMessage(orderStatusMessages.cancelled),
status: "error" status: StatusType.ERROR
}; };
case OrderStatus.DRAFT: case OrderStatus.DRAFT:
return { return {
localized: intl.formatMessage(orderStatusMessages.draft), localized: intl.formatMessage(orderStatusMessages.draft),
status: "error" status: StatusType.ERROR
};
case OrderStatus.UNCONFIRMED:
return {
localized: intl.formatMessage(orderStatusMessages.unconfirmed),
status: StatusType.NEUTRAL
}; };
} }
return { return {
localized: status, localized: status,
status: "error" status: StatusType.ERROR
}; };
}; };

View file

@ -31,6 +31,7 @@ import OrderHistory, { FormData as HistoryFormData } from "../OrderHistory";
import OrderInvoiceList from "../OrderInvoiceList"; import OrderInvoiceList from "../OrderInvoiceList";
import OrderPayment from "../OrderPayment/OrderPayment"; import OrderPayment from "../OrderPayment/OrderPayment";
import OrderUnfulfilledItems from "../OrderUnfulfilledItems/OrderUnfulfilledItems"; import OrderUnfulfilledItems from "../OrderUnfulfilledItems/OrderUnfulfilledItems";
import Title from "./Title";
const useStyles = makeStyles( const useStyles = makeStyles(
theme => ({ theme => ({
@ -39,6 +40,7 @@ const useStyles = makeStyles(
}, },
header: { header: {
display: "flex", display: "flex",
justifyContent: "space-between",
marginBottom: 0 marginBottom: 0
} }
}), }),
@ -136,6 +138,22 @@ const OrderDetailsPage: React.FC<OrderDetailsPageProps> = props => {
privateMetadata: order?.privateMetadata.map(mapMetadataItemToInput) privateMetadata: order?.privateMetadata.map(mapMetadataItemToInput)
}; };
const saveLabel =
order?.status === OrderStatus.UNCONFIRMED
? intl.formatMessage({
defaultMessage: "confirm order",
description: "save button"
})
: undefined;
const allowSave = (hasChanged: boolean) => {
if (order?.status !== OrderStatus.UNCONFIRMED) {
return disabled || !hasChanged;
}
return disabled;
};
return ( return (
<Form initial={initial} onSubmit={handleSubmit}> <Form initial={initial} onSubmit={handleSubmit}>
{({ change, data, hasChanged, submit }) => { {({ change, data, hasChanged, submit }) => {
@ -149,7 +167,7 @@ const OrderDetailsPage: React.FC<OrderDetailsPageProps> = props => {
<PageHeader <PageHeader
className={classes.header} className={classes.header}
inline inline
title={maybe(() => order.number) ? "#" + order.number : undefined} title={<Title order={order} />}
> >
{canCancel && ( {canCancel && (
<CardMenu <CardMenu
@ -243,10 +261,11 @@ const OrderDetailsPage: React.FC<OrderDetailsPageProps> = props => {
</div> </div>
</Grid> </Grid>
<SaveButtonBar <SaveButtonBar
labels={{ save: saveLabel }}
onCancel={onBack} onCancel={onBack}
onSave={submit} onSave={submit}
state={saveButtonBarState} state={saveButtonBarState}
disabled={disabled || !hasChanged} disabled={allowSave(hasChanged)}
/> />
</Container> </Container>
); );

View file

@ -0,0 +1,46 @@
import { makeStyles } from "@material-ui/core/styles";
import StatusChip from "@saleor/components/StatusChip";
import { transformOrderStatus } from "@saleor/misc";
import { OrderDetails_order } from "@saleor/orders/types/OrderDetails";
import React from "react";
import { useIntl } from "react-intl";
export interface TitleProps {
order?: OrderDetails_order;
}
const useStyles = makeStyles(
theme => ({
container: {
alignItems: "center",
display: "flex"
},
statusContainer: {
marginLeft: theme.spacing(2)
}
}),
{ name: "OrderDetailsTitle" }
);
const Title: React.FC<TitleProps> = props => {
const intl = useIntl();
const classes = useStyles(props);
const { order } = props;
if (!order) {
return null;
}
const { localized, status } = transformOrderStatus(order.status, intl);
return (
<div className={classes.container}>
{`#${order.number}`}
<div className={classes.statusContainer}>
<StatusChip label={localized} type={status} />
</div>
</div>
);
};
export default Title;

View file

@ -226,6 +226,11 @@ const getEventMessage = (event: OrderDetails_order_events, intl: IntlShape) => {
defaultMessage: "Payment was authorized", defaultMessage: "Payment was authorized",
description: "order history message" description: "order history message"
}); });
case OrderEventsEnum.CONFIRMED:
return intl.formatMessage({
defaultMessage: "Order was confirmed",
description: "order history message"
});
case OrderEventsEnum.EXTERNAL_SERVICE_NOTIFICATION: case OrderEventsEnum.EXTERNAL_SERVICE_NOTIFICATION:
return event.message; return event.message;
} }

View file

@ -1,5 +1,7 @@
import Button from "@material-ui/core/Button"; import Button from "@material-ui/core/Button";
import Card from "@material-ui/core/Card"; import Card from "@material-ui/core/Card";
import makeStyles from "@material-ui/core/styles/makeStyles";
import CardMenu from "@saleor/components/CardMenu";
import Container from "@saleor/components/Container"; import Container from "@saleor/components/Container";
import FilterBar from "@saleor/components/FilterBar"; import FilterBar from "@saleor/components/FilterBar";
import PageHeader from "@saleor/components/PageHeader"; import PageHeader from "@saleor/components/PageHeader";
@ -22,8 +24,18 @@ export interface OrderListPageProps
FilterPageProps<OrderFilterKeys, OrderListFilterOpts>, FilterPageProps<OrderFilterKeys, OrderListFilterOpts>,
SortPage<OrderListUrlSortField> { SortPage<OrderListUrlSortField> {
orders: OrderList_orders_edges_node[]; orders: OrderList_orders_edges_node[];
onSettingsOpen: () => void;
} }
const useStyles = makeStyles(
theme => ({
settings: {
marginRight: theme.spacing(2)
}
}),
{ name: "OrderListPage" }
);
const OrderListPage: React.FC<OrderListPageProps> = ({ const OrderListPage: React.FC<OrderListPageProps> = ({
currentTab, currentTab,
initialSearch, initialSearch,
@ -32,6 +44,7 @@ const OrderListPage: React.FC<OrderListPageProps> = ({
onAdd, onAdd,
onAll, onAll,
onSearchChange, onSearchChange,
onSettingsOpen,
onFilterChange, onFilterChange,
onTabChange, onTabChange,
onTabDelete, onTabDelete,
@ -39,11 +52,26 @@ const OrderListPage: React.FC<OrderListPageProps> = ({
...listProps ...listProps
}) => { }) => {
const intl = useIntl(); const intl = useIntl();
const classes = useStyles({});
const filterStructure = createFilterStructure(intl, filterOpts); const filterStructure = createFilterStructure(intl, filterOpts);
return ( return (
<Container> <Container>
<PageHeader title={intl.formatMessage(sectionNames.orders)}> <PageHeader title={intl.formatMessage(sectionNames.orders)}>
{!!onSettingsOpen && (
<CardMenu
className={classes.settings}
menuItems={[
{
label: intl.formatMessage({
defaultMessage: "Order Settings",
description: "button"
}),
onSelect: onSettingsOpen
}
]}
/>
)}
<Button color="primary" variant="contained" onClick={onAdd}> <Button color="primary" variant="contained" onClick={onAdd}>
<FormattedMessage <FormattedMessage
defaultMessage="Create order" defaultMessage="Create order"

View file

@ -0,0 +1,61 @@
import Card from "@material-ui/core/Card";
import CardContent from "@material-ui/core/CardContent";
import Typography from "@material-ui/core/Typography";
import CardTitle from "@saleor/components/CardTitle";
import ControlledCheckbox from "@saleor/components/ControlledCheckbox";
import React from "react";
import { FormattedMessage, useIntl } from "react-intl";
import { OrderSettingsFormData } from "../OrderSettingsPage/form";
export interface OrderSettingsProps {
data: OrderSettingsFormData;
disabled: boolean;
onChange: (event: React.ChangeEvent<any>) => void;
}
const OrderSettings: React.FC<OrderSettingsProps> = ({
data,
disabled,
onChange
}) => {
const intl = useIntl();
return (
<Card data-test="orderSettings">
<CardTitle
title={intl.formatMessage({
defaultMessage: "Settings",
description: "section header"
})}
/>
<CardContent>
<ControlledCheckbox
name={
"automaticallyConfirmAllNewOrders" as keyof OrderSettingsFormData
}
label={
<>
<FormattedMessage
defaultMessage="Automatically confirm all orders"
description="checkbox label"
/>
<Typography variant="caption">
<FormattedMessage
defaultMessage="All orders will be automatically confirmed and all payments will be captured."
description="checkbox label description"
/>
</Typography>
</>
}
checked={data.automaticallyConfirmAllNewOrders}
onChange={onChange}
disabled={disabled}
data-test="automaticallyConfirmAllNewOrdersCheckbox"
/>
</CardContent>
</Card>
);
};
OrderSettings.displayName = "OrderSettings";
export default OrderSettings;

View file

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

View file

@ -0,0 +1,21 @@
import { orderSettings as orderSettingsFixture } from "@saleor/orders/fixtures";
import { storiesOf } from "@storybook/react";
import React from "react";
import Decorator from "../../../storybook/Decorator";
import OrderSettings, { OrderSettingsPageProps } from ".";
const props: OrderSettingsPageProps = {
data: orderSettingsFixture,
disabled: false,
onBack: () => undefined,
onSubmit: () => undefined,
saveButtonBarState: "default"
};
storiesOf("Views / Orders / Order settings", module)
.addDecorator(Decorator)
.add("default", () => <OrderSettings {...props} />)
.add("loading", () => (
<OrderSettings {...props} disabled={true} data={undefined} />
));

View file

@ -0,0 +1,62 @@
import Typography from "@material-ui/core/Typography";
import AppHeader from "@saleor/components/AppHeader";
import { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton";
import Container from "@saleor/components/Container";
import Grid from "@saleor/components/Grid";
import PageHeader from "@saleor/components/PageHeader";
import SaveButtonBar from "@saleor/components/SaveButtonBar";
import { OrderSettingsFragment } from "@saleor/fragments/types/OrderSettingsFragment";
import { SubmitPromise } from "@saleor/hooks/useForm";
import { sectionNames } from "@saleor/intl";
import React from "react";
import { FormattedMessage, useIntl } from "react-intl";
import OrderSettings from "../OrderSettings/OrderSettings";
import OrderSettingsForm, { OrderSettingsFormData } from "./form";
export interface OrderSettingsPageProps {
data: OrderSettingsFragment;
disabled: boolean;
saveButtonBarState: ConfirmButtonTransitionState;
onBack: () => void;
onSubmit: (data: OrderSettingsFormData) => SubmitPromise;
}
const OrderSettingsPage: React.FC<OrderSettingsPageProps> = props => {
const { data, disabled, saveButtonBarState, onBack, onSubmit } = props;
const intl = useIntl();
return (
<OrderSettingsForm orderSettings={data} onSubmit={onSubmit}>
{({ data, submit, hasChanged, change }) => (
<Container>
<AppHeader onBack={onBack}>
{intl.formatMessage(sectionNames.orders)}
</AppHeader>
<PageHeader
title={intl.formatMessage({
defaultMessage: "Order settings",
description: "header"
})}
/>
<Grid variant="inverted">
<div>
<Typography>
<FormattedMessage defaultMessage="General Settings" />
</Typography>
</div>
<OrderSettings data={data} disabled={disabled} onChange={change} />
</Grid>
<SaveButtonBar
onCancel={onBack}
onSave={submit}
disabled={disabled || !hasChanged}
state={saveButtonBarState}
/>
</Container>
)}
</OrderSettingsForm>
);
};
OrderSettingsPage.displayName = "OrderSettingsPage";
export default OrderSettingsPage;

View file

@ -0,0 +1,71 @@
import { OrderSettingsFragment } from "@saleor/fragments/types/OrderSettingsFragment";
import useForm, { FormChange, SubmitPromise } from "@saleor/hooks/useForm";
import handleFormSubmit from "@saleor/utils/handlers/handleFormSubmit";
import React from "react";
export interface OrderSettingsFormData {
automaticallyConfirmAllNewOrders: boolean;
}
export interface UseOrderSettingsFormResult {
change: FormChange;
data: OrderSettingsFormData;
hasChanged: boolean;
submit: () => Promise<boolean>;
}
export interface OrderSettingsFormProps {
children: (props: UseOrderSettingsFormResult) => React.ReactNode;
orderSettings: OrderSettingsFragment;
onSubmit: (data: OrderSettingsFormData) => SubmitPromise;
}
function getOrderSeettingsFormData(
orderSettings: OrderSettingsFragment
): OrderSettingsFormData {
return {
automaticallyConfirmAllNewOrders:
orderSettings?.automaticallyConfirmAllNewOrders
};
}
function useOrderSettingsForm(
orderSettings: OrderSettingsFragment,
onSubmit: (data: OrderSettingsFormData) => SubmitPromise
): UseOrderSettingsFormResult {
const [changed, setChanged] = React.useState(false);
const triggerChange = () => setChanged(true);
const form = useForm(getOrderSeettingsFormData(orderSettings));
const handleChange: FormChange = (event, cb) => {
form.change(event, cb);
triggerChange();
};
const data: OrderSettingsFormData = {
...form.data
};
const submit = () => handleFormSubmit(form.data, onSubmit, setChanged);
return {
change: handleChange,
data,
hasChanged: changed,
submit
};
}
const OrderSettingsForm: React.FC<OrderSettingsFormProps> = ({
children,
orderSettings,
onSubmit
}) => {
const props = useOrderSettingsForm(orderSettings, onSubmit);
return <form onSubmit={props.submit}>{children(props)}</form>;
};
OrderSettingsForm.displayName = "OrderSettingsForm";
export default OrderSettingsForm;

View file

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

View file

@ -1,4 +1,5 @@
import { InvoiceFragment } from "@saleor/fragments/types/InvoiceFragment"; import { InvoiceFragment } from "@saleor/fragments/types/InvoiceFragment";
import { OrderSettingsFragment } from "@saleor/fragments/types/OrderSettingsFragment";
import { SearchCustomers_search_edges_node } from "@saleor/searches/types/SearchCustomers"; import { SearchCustomers_search_edges_node } from "@saleor/searches/types/SearchCustomers";
import { warehouseList } from "@saleor/warehouses/fixtures"; import { warehouseList } from "@saleor/warehouses/fixtures";
import { MessageDescriptor } from "react-intl"; import { MessageDescriptor } from "react-intl";
@ -1628,3 +1629,8 @@ export const invoices: InvoiceFragment[] = [
"http://localhost:8000/media/invoices/invoice-1/07/2020-order-20-0e449e10-ef4b-4066-bebe-361f670b6820.pdf" "http://localhost:8000/media/invoices/invoice-1/07/2020-order-20-0e449e10-ef4b-4066-bebe-361f670b6820.pdf"
} }
]; ];
export const orderSettings: OrderSettingsFragment = {
__typename: "OrderSettings",
automaticallyConfirmAllNewOrders: true
};

View file

@ -15,12 +15,14 @@ import {
OrderListUrlQueryParams, OrderListUrlQueryParams,
OrderListUrlSortField, OrderListUrlSortField,
orderPath, orderPath,
orderSettingsPath,
OrderUrlQueryParams OrderUrlQueryParams
} from "./urls"; } from "./urls";
import OrderDetailsComponent from "./views/OrderDetails"; import OrderDetailsComponent from "./views/OrderDetails";
import OrderDraftListComponent from "./views/OrderDraftList"; import OrderDraftListComponent from "./views/OrderDraftList";
import OrderFulfillComponent from "./views/OrderFulfill"; import OrderFulfillComponent from "./views/OrderFulfill";
import OrderListComponent from "./views/OrderList"; import OrderListComponent from "./views/OrderList";
import OrderSettings from "./views/OrderSettings";
const OrderList: React.FC<RouteComponentProps<any>> = ({ location }) => { const OrderList: React.FC<RouteComponentProps<any>> = ({ location }) => {
const qs = parseQs(location.search.substr(1)); const qs = parseQs(location.search.substr(1));
@ -70,6 +72,7 @@ const Component = () => {
<> <>
<WindowTitle title={intl.formatMessage(sectionNames.orders)} /> <WindowTitle title={intl.formatMessage(sectionNames.orders)} />
<Switch> <Switch>
<Route exact path={orderSettingsPath} component={OrderSettings} />
<Route exact path={orderDraftListPath} component={OrderDraftList} /> <Route exact path={orderDraftListPath} component={OrderDraftList} />
<Route exact path={orderListPath} component={OrderList} /> <Route exact path={orderListPath} component={OrderList} />
<Route path={orderFulfillPath(":id")} component={OrderFulfill} /> <Route path={orderFulfillPath(":id")} component={OrderFulfill} />

View file

@ -1,10 +1,12 @@
import { import {
invoiceErrorFragment, invoiceErrorFragment,
orderErrorFragment orderErrorFragment,
orderSettingsErrorFragment
} from "@saleor/fragments/errors"; } from "@saleor/fragments/errors";
import { import {
fragmentOrderDetails, fragmentOrderDetails,
fragmentOrderEvent, fragmentOrderEvent,
fragmentOrderSettings,
invoiceFragment invoiceFragment
} from "@saleor/fragments/orders"; } from "@saleor/fragments/orders";
import makeMutation from "@saleor/hooks/makeMutation"; import makeMutation from "@saleor/hooks/makeMutation";
@ -23,6 +25,7 @@ import {
import { OrderAddNote, OrderAddNoteVariables } from "./types/OrderAddNote"; import { OrderAddNote, OrderAddNoteVariables } from "./types/OrderAddNote";
import { OrderCancel, OrderCancelVariables } from "./types/OrderCancel"; import { OrderCancel, OrderCancelVariables } from "./types/OrderCancel";
import { OrderCapture, OrderCaptureVariables } from "./types/OrderCapture"; import { OrderCapture, OrderCaptureVariables } from "./types/OrderCapture";
import { OrderConfirm, OrderConfirmVariables } from "./types/OrderConfirm";
import { import {
OrderDraftBulkCancel, OrderDraftBulkCancel,
OrderDraftBulkCancelVariables OrderDraftBulkCancelVariables
@ -65,6 +68,10 @@ import {
OrderMarkAsPaidVariables OrderMarkAsPaidVariables
} from "./types/OrderMarkAsPaid"; } from "./types/OrderMarkAsPaid";
import { OrderRefund, OrderRefundVariables } from "./types/OrderRefund"; import { OrderRefund, OrderRefundVariables } from "./types/OrderRefund";
import {
OrderSettingsUpdate,
OrderSettingsUpdateVariables
} from "./types/OrderSettingsUpdate";
import { import {
OrderShippingMethodUpdate, OrderShippingMethodUpdate,
OrderShippingMethodUpdateVariables OrderShippingMethodUpdateVariables
@ -120,6 +127,27 @@ const orderDraftBulkCancelMutation = gql`
} }
} }
`; `;
export const orderConfirmMutation = gql`
${fragmentOrderDetails}
${orderErrorFragment}
mutation OrderConfirm($id: ID!) {
orderConfirm(id: $id) {
errors: orderErrors {
...OrderErrorFragment
}
order {
...OrderDetailsFragment
}
}
}
`;
export const useOrderConfirmMutation = makeMutation<
OrderConfirm,
OrderConfirmVariables
>(orderConfirmMutation);
export const TypedOrderDraftBulkCancelMutation = TypedMutation< export const TypedOrderDraftBulkCancelMutation = TypedMutation<
OrderDraftBulkCancel, OrderDraftBulkCancel,
OrderDraftBulkCancelVariables OrderDraftBulkCancelVariables
@ -500,3 +528,22 @@ export const TypedInvoiceEmailSendMutation = TypedMutation<
InvoiceEmailSend, InvoiceEmailSend,
InvoiceEmailSendVariables InvoiceEmailSendVariables
>(invoiceEmailSendMutation); >(invoiceEmailSendMutation);
const orderSettingsUpdateMutation = gql`
${fragmentOrderSettings}
${orderSettingsErrorFragment}
mutation OrderSettingsUpdate($input: OrderSettingsUpdateInput!) {
orderSettingsUpdate(input: $input) {
errors: orderSettingsErrors {
...OrderSettingsErrorFragment
}
orderSettings {
...OrderSettingsFragment
}
}
}
`;
export const useOrderSettingsUpdateMutation = makeMutation<
OrderSettingsUpdate,
OrderSettingsUpdateVariables
>(orderSettingsUpdateMutation);

View file

@ -1,5 +1,8 @@
import { fragmentAddress } from "@saleor/fragments/address"; import { fragmentAddress } from "@saleor/fragments/address";
import { fragmentOrderDetails } from "@saleor/fragments/orders"; import {
fragmentOrderDetails,
fragmentOrderSettings
} from "@saleor/fragments/orders";
import makeQuery from "@saleor/hooks/makeQuery"; import makeQuery from "@saleor/hooks/makeQuery";
import makeTopLevelSearch from "@saleor/hooks/makeTopLevelSearch"; import makeTopLevelSearch from "@saleor/hooks/makeTopLevelSearch";
import gql from "graphql-tag"; import gql from "graphql-tag";
@ -15,6 +18,7 @@ import {
OrderFulfillDataVariables OrderFulfillDataVariables
} from "./types/OrderFulfillData"; } from "./types/OrderFulfillData";
import { OrderList, OrderListVariables } from "./types/OrderList"; import { OrderList, OrderListVariables } from "./types/OrderList";
import { OrderSettings } from "./types/OrderSettings";
import { import {
SearchOrderVariant as SearchOrderVariantType, SearchOrderVariant as SearchOrderVariantType,
SearchOrderVariantVariables SearchOrderVariantVariables
@ -238,3 +242,15 @@ export const useOrderFulfillData = makeQuery<
OrderFulfillData, OrderFulfillData,
OrderFulfillDataVariables OrderFulfillDataVariables
>(orderFulfillData); >(orderFulfillData);
export const orderSettingsQuery = gql`
${fragmentOrderSettings}
query OrderSettings {
orderSettings {
...OrderSettingsFragment
}
}
`;
export const useOrderSettingsQuery = makeQuery<OrderSettings, never>(
orderSettingsQuery
);

View file

@ -0,0 +1,343 @@
/* tslint:disable */
/* eslint-disable */
// This file was automatically generated and should not be edited.
import { OrderErrorCode, OrderEventsEmailsEnum, OrderEventsEnum, FulfillmentStatus, PaymentChargeStatusEnum, OrderStatus, OrderAction, JobStatusEnum } from "./../../types/globalTypes";
// ====================================================
// GraphQL mutation operation: OrderConfirm
// ====================================================
export interface OrderConfirm_orderConfirm_errors {
__typename: "OrderError";
code: OrderErrorCode;
field: string | null;
}
export interface OrderConfirm_orderConfirm_order_metadata {
__typename: "MetadataItem";
key: string;
value: string;
}
export interface OrderConfirm_orderConfirm_order_privateMetadata {
__typename: "MetadataItem";
key: string;
value: string;
}
export interface OrderConfirm_orderConfirm_order_billingAddress_country {
__typename: "CountryDisplay";
code: string;
country: string;
}
export interface OrderConfirm_orderConfirm_order_billingAddress {
__typename: "Address";
city: string;
cityArea: string;
companyName: string;
country: OrderConfirm_orderConfirm_order_billingAddress_country;
countryArea: string;
firstName: string;
id: string;
lastName: string;
phone: string | null;
postalCode: string;
streetAddress1: string;
streetAddress2: string;
}
export interface OrderConfirm_orderConfirm_order_events_user {
__typename: "User";
id: string;
email: string;
}
export interface OrderConfirm_orderConfirm_order_events {
__typename: "OrderEvent";
id: string;
amount: number | null;
date: any | null;
email: string | null;
emailType: OrderEventsEmailsEnum | null;
invoiceNumber: string | null;
message: string | null;
quantity: number | null;
type: OrderEventsEnum | null;
user: OrderConfirm_orderConfirm_order_events_user | null;
}
export interface OrderConfirm_orderConfirm_order_fulfillments_lines_orderLine_variant {
__typename: "ProductVariant";
id: string;
quantityAvailable: number;
}
export interface OrderConfirm_orderConfirm_order_fulfillments_lines_orderLine_unitPrice_gross {
__typename: "Money";
amount: number;
currency: string;
}
export interface OrderConfirm_orderConfirm_order_fulfillments_lines_orderLine_unitPrice_net {
__typename: "Money";
amount: number;
currency: string;
}
export interface OrderConfirm_orderConfirm_order_fulfillments_lines_orderLine_unitPrice {
__typename: "TaxedMoney";
gross: OrderConfirm_orderConfirm_order_fulfillments_lines_orderLine_unitPrice_gross;
net: OrderConfirm_orderConfirm_order_fulfillments_lines_orderLine_unitPrice_net;
}
export interface OrderConfirm_orderConfirm_order_fulfillments_lines_orderLine_thumbnail {
__typename: "Image";
url: string;
}
export interface OrderConfirm_orderConfirm_order_fulfillments_lines_orderLine {
__typename: "OrderLine";
id: string;
isShippingRequired: boolean;
variant: OrderConfirm_orderConfirm_order_fulfillments_lines_orderLine_variant | null;
productName: string;
productSku: string;
quantity: number;
quantityFulfilled: number;
unitPrice: OrderConfirm_orderConfirm_order_fulfillments_lines_orderLine_unitPrice | null;
thumbnail: OrderConfirm_orderConfirm_order_fulfillments_lines_orderLine_thumbnail | null;
}
export interface OrderConfirm_orderConfirm_order_fulfillments_lines {
__typename: "FulfillmentLine";
id: string;
quantity: number;
orderLine: OrderConfirm_orderConfirm_order_fulfillments_lines_orderLine | null;
}
export interface OrderConfirm_orderConfirm_order_fulfillments_warehouse {
__typename: "Warehouse";
id: string;
name: string;
}
export interface OrderConfirm_orderConfirm_order_fulfillments {
__typename: "Fulfillment";
id: string;
lines: (OrderConfirm_orderConfirm_order_fulfillments_lines | null)[] | null;
fulfillmentOrder: number;
status: FulfillmentStatus;
trackingNumber: string;
warehouse: OrderConfirm_orderConfirm_order_fulfillments_warehouse | null;
}
export interface OrderConfirm_orderConfirm_order_lines_variant {
__typename: "ProductVariant";
id: string;
quantityAvailable: number;
}
export interface OrderConfirm_orderConfirm_order_lines_unitPrice_gross {
__typename: "Money";
amount: number;
currency: string;
}
export interface OrderConfirm_orderConfirm_order_lines_unitPrice_net {
__typename: "Money";
amount: number;
currency: string;
}
export interface OrderConfirm_orderConfirm_order_lines_unitPrice {
__typename: "TaxedMoney";
gross: OrderConfirm_orderConfirm_order_lines_unitPrice_gross;
net: OrderConfirm_orderConfirm_order_lines_unitPrice_net;
}
export interface OrderConfirm_orderConfirm_order_lines_thumbnail {
__typename: "Image";
url: string;
}
export interface OrderConfirm_orderConfirm_order_lines {
__typename: "OrderLine";
id: string;
isShippingRequired: boolean;
variant: OrderConfirm_orderConfirm_order_lines_variant | null;
productName: string;
productSku: string;
quantity: number;
quantityFulfilled: number;
unitPrice: OrderConfirm_orderConfirm_order_lines_unitPrice | null;
thumbnail: OrderConfirm_orderConfirm_order_lines_thumbnail | null;
}
export interface OrderConfirm_orderConfirm_order_shippingAddress_country {
__typename: "CountryDisplay";
code: string;
country: string;
}
export interface OrderConfirm_orderConfirm_order_shippingAddress {
__typename: "Address";
city: string;
cityArea: string;
companyName: string;
country: OrderConfirm_orderConfirm_order_shippingAddress_country;
countryArea: string;
firstName: string;
id: string;
lastName: string;
phone: string | null;
postalCode: string;
streetAddress1: string;
streetAddress2: string;
}
export interface OrderConfirm_orderConfirm_order_shippingMethod {
__typename: "ShippingMethod";
id: string;
}
export interface OrderConfirm_orderConfirm_order_shippingPrice_gross {
__typename: "Money";
amount: number;
currency: string;
}
export interface OrderConfirm_orderConfirm_order_shippingPrice {
__typename: "TaxedMoney";
gross: OrderConfirm_orderConfirm_order_shippingPrice_gross;
}
export interface OrderConfirm_orderConfirm_order_subtotal_gross {
__typename: "Money";
amount: number;
currency: string;
}
export interface OrderConfirm_orderConfirm_order_subtotal {
__typename: "TaxedMoney";
gross: OrderConfirm_orderConfirm_order_subtotal_gross;
}
export interface OrderConfirm_orderConfirm_order_total_gross {
__typename: "Money";
amount: number;
currency: string;
}
export interface OrderConfirm_orderConfirm_order_total_tax {
__typename: "Money";
amount: number;
currency: string;
}
export interface OrderConfirm_orderConfirm_order_total {
__typename: "TaxedMoney";
gross: OrderConfirm_orderConfirm_order_total_gross;
tax: OrderConfirm_orderConfirm_order_total_tax;
}
export interface OrderConfirm_orderConfirm_order_totalAuthorized {
__typename: "Money";
amount: number;
currency: string;
}
export interface OrderConfirm_orderConfirm_order_totalCaptured {
__typename: "Money";
amount: number;
currency: string;
}
export interface OrderConfirm_orderConfirm_order_user {
__typename: "User";
id: string;
email: string;
}
export interface OrderConfirm_orderConfirm_order_availableShippingMethods_price {
__typename: "Money";
amount: number;
currency: string;
}
export interface OrderConfirm_orderConfirm_order_availableShippingMethods {
__typename: "ShippingMethod";
id: string;
name: string;
price: OrderConfirm_orderConfirm_order_availableShippingMethods_price | null;
}
export interface OrderConfirm_orderConfirm_order_discount {
__typename: "Money";
amount: number;
currency: string;
}
export interface OrderConfirm_orderConfirm_order_invoices {
__typename: "Invoice";
id: string;
number: string | null;
createdAt: any;
url: string | null;
status: JobStatusEnum;
}
export interface OrderConfirm_orderConfirm_order_channel {
__typename: "Channel";
isActive: boolean;
id: string;
name: string;
currencyCode: string;
}
export interface OrderConfirm_orderConfirm_order {
__typename: "Order";
id: string;
metadata: (OrderConfirm_orderConfirm_order_metadata | null)[];
privateMetadata: (OrderConfirm_orderConfirm_order_privateMetadata | null)[];
billingAddress: OrderConfirm_orderConfirm_order_billingAddress | null;
canFinalize: boolean;
created: any;
customerNote: string;
events: (OrderConfirm_orderConfirm_order_events | null)[] | null;
fulfillments: (OrderConfirm_orderConfirm_order_fulfillments | null)[];
lines: (OrderConfirm_orderConfirm_order_lines | null)[];
number: string | null;
paymentStatus: PaymentChargeStatusEnum | null;
shippingAddress: OrderConfirm_orderConfirm_order_shippingAddress | null;
shippingMethod: OrderConfirm_orderConfirm_order_shippingMethod | null;
shippingMethodName: string | null;
shippingPrice: OrderConfirm_orderConfirm_order_shippingPrice | null;
status: OrderStatus;
subtotal: OrderConfirm_orderConfirm_order_subtotal | null;
total: OrderConfirm_orderConfirm_order_total | null;
actions: (OrderAction | null)[];
totalAuthorized: OrderConfirm_orderConfirm_order_totalAuthorized | null;
totalCaptured: OrderConfirm_orderConfirm_order_totalCaptured | null;
user: OrderConfirm_orderConfirm_order_user | null;
userEmail: string | null;
availableShippingMethods: (OrderConfirm_orderConfirm_order_availableShippingMethods | null)[] | null;
discount: OrderConfirm_orderConfirm_order_discount | null;
invoices: (OrderConfirm_orderConfirm_order_invoices | null)[] | null;
channel: OrderConfirm_orderConfirm_order_channel;
}
export interface OrderConfirm_orderConfirm {
__typename: "OrderConfirm";
errors: OrderConfirm_orderConfirm_errors[];
order: OrderConfirm_orderConfirm_order | null;
}
export interface OrderConfirm {
orderConfirm: OrderConfirm_orderConfirm | null;
}
export interface OrderConfirmVariables {
id: string;
}

View file

@ -0,0 +1,16 @@
/* tslint:disable */
/* eslint-disable */
// This file was automatically generated and should not be edited.
// ====================================================
// GraphQL query operation: OrderSettings
// ====================================================
export interface OrderSettings_orderSettings {
__typename: "OrderSettings";
automaticallyConfirmAllNewOrders: boolean;
}
export interface OrderSettings {
orderSettings: OrderSettings_orderSettings | null;
}

View file

@ -0,0 +1,34 @@
/* tslint:disable */
/* eslint-disable */
// This file was automatically generated and should not be edited.
import { OrderSettingsUpdateInput, OrderSettingsErrorCode } from "./../../types/globalTypes";
// ====================================================
// GraphQL mutation operation: OrderSettingsUpdate
// ====================================================
export interface OrderSettingsUpdate_orderSettingsUpdate_errors {
__typename: "OrderSettingsError";
code: OrderSettingsErrorCode;
field: string | null;
}
export interface OrderSettingsUpdate_orderSettingsUpdate_orderSettings {
__typename: "OrderSettings";
automaticallyConfirmAllNewOrders: boolean;
}
export interface OrderSettingsUpdate_orderSettingsUpdate {
__typename: "OrderSettingsUpdate";
errors: OrderSettingsUpdate_orderSettingsUpdate_errors[];
orderSettings: OrderSettingsUpdate_orderSettingsUpdate_orderSettings | null;
}
export interface OrderSettingsUpdate {
orderSettingsUpdate: OrderSettingsUpdate_orderSettingsUpdate | null;
}
export interface OrderSettingsUpdateVariables {
input: OrderSettingsUpdateInput;
}

View file

@ -114,3 +114,5 @@ export const orderFulfillPath = (id: string) =>
urlJoin(orderPath(id), "fulfill"); urlJoin(orderPath(id), "fulfill");
export const orderFulfillUrl = (id: string) => export const orderFulfillUrl = (id: string) =>
orderFulfillPath(encodeURIComponent(id)); orderFulfillPath(encodeURIComponent(id));
export const orderSettingsPath = urlJoin(orderSectionUrl, "settings");

View file

@ -10,8 +10,10 @@ import useUser from "@saleor/hooks/useUser";
import { commonMessages } from "@saleor/intl"; import { commonMessages } from "@saleor/intl";
import OrderCannotCancelOrderDialog from "@saleor/orders/components/OrderCannotCancelOrderDialog"; import OrderCannotCancelOrderDialog from "@saleor/orders/components/OrderCannotCancelOrderDialog";
import OrderInvoiceEmailSendDialog from "@saleor/orders/components/OrderInvoiceEmailSendDialog"; import OrderInvoiceEmailSendDialog from "@saleor/orders/components/OrderInvoiceEmailSendDialog";
import { useOrderConfirmMutation } from "@saleor/orders/mutations";
import { InvoiceRequest } from "@saleor/orders/types/InvoiceRequest"; import { InvoiceRequest } from "@saleor/orders/types/InvoiceRequest";
import useCustomerSearch from "@saleor/searches/useCustomerSearch"; import useCustomerSearch from "@saleor/searches/useCustomerSearch";
import getOrderErrorMessage from "@saleor/utils/errors/order";
import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers"; import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers";
import createMetadataUpdateHandler from "@saleor/utils/handlers/metadataUpdateHandler"; import createMetadataUpdateHandler from "@saleor/utils/handlers/metadataUpdateHandler";
import { import {
@ -70,6 +72,7 @@ export const OrderDetails: React.FC<OrderDetailsProps> = ({ id, params }) => {
} = useCustomerSearch({ } = useCustomerSearch({
variables: DEFAULT_INITIAL_SEARCH_DATA variables: DEFAULT_INITIAL_SEARCH_DATA
}); });
const { const {
loadMore, loadMore,
search: variantSearch, search: variantSearch,
@ -100,6 +103,19 @@ export const OrderDetails: React.FC<OrderDetailsProps> = ({ id, params }) => {
const handleBack = () => navigate(orderListUrl()); const handleBack = () => navigate(orderListUrl());
const [orderConfirm] = useOrderConfirmMutation({
onCompleted: ({ orderConfirm: { errors } }) => {
const isError = !!errors.length;
notify({
status: isError ? "error" : "success",
text: isError
? getOrderErrorMessage(errors[0], intl)
: "Confirmed Order"
});
}
});
return ( return (
<TypedOrderDetailsQuery displayLoader variables={{ id }}> <TypedOrderDetailsQuery displayLoader variables={{ id }}>
{({ data, loading }) => { {({ data, loading }) => {
@ -109,6 +125,10 @@ export const OrderDetails: React.FC<OrderDetailsProps> = ({ id, params }) => {
} }
const handleSubmit = async (data: MetadataFormData) => { const handleSubmit = async (data: MetadataFormData) => {
if (order?.status === OrderStatus.UNCONFIRMED) {
await orderConfirm({ variables: { id: order?.id } });
}
const update = createMetadataUpdateHandler( const update = createMetadataUpdateHandler(
order, order,
() => Promise.resolve([]), () => Promise.resolve([]),

View file

@ -27,6 +27,7 @@ import {
orderListUrl, orderListUrl,
OrderListUrlDialog, OrderListUrlDialog,
OrderListUrlQueryParams, OrderListUrlQueryParams,
orderSettingsPath,
orderUrl orderUrl
} from "../../urls"; } from "../../urls";
import { import {
@ -160,6 +161,7 @@ export const OrderList: React.FC<OrderListProps> = ({ params }) => {
initialSearch={params.query || ""} initialSearch={params.query || ""}
tabs={getFilterTabs().map(tab => tab.name)} tabs={getFilterTabs().map(tab => tab.name)}
onAll={resetFilters} onAll={resetFilters}
onSettingsOpen={() => navigate(orderSettingsPath)}
/> />
<SaveFilterTabDialog <SaveFilterTabDialog
open={params.action === "save-search"} open={params.action === "save-search"}

View file

@ -0,0 +1,64 @@
import useNavigator from "@saleor/hooks/useNavigator";
import useNotifier from "@saleor/hooks/useNotifier";
import { commonMessages } from "@saleor/intl";
import { getMutationState } from "@saleor/misc";
import OrderSettingsPage from "@saleor/orders/components/OrderSettingsPage";
import { OrderSettingsFormData } from "@saleor/orders/components/OrderSettingsPage/form";
import { useOrderSettingsUpdateMutation } from "@saleor/orders/mutations";
import { useOrderSettingsQuery } from "@saleor/orders/queries";
import { orderListUrl } from "@saleor/orders/urls";
import React from "react";
import { useIntl } from "react-intl";
export const OrderSettings: React.FC = () => {
const intl = useIntl();
const navigate = useNavigator();
const notify = useNotifier();
const { data, loading } = useOrderSettingsQuery({});
const [
orderSettingsUpdate,
orderSettingsUpdateOpts
] = useOrderSettingsUpdateMutation({});
const handleSubmit = async (data: OrderSettingsFormData) => {
const result = await orderSettingsUpdate({
variables: {
input: data
}
});
const errors = result.data?.orderSettingsUpdate.errors;
if (errors.length) {
notify({
status: "error",
text: intl.formatMessage(commonMessages.somethingWentWrong)
});
return errors;
}
notify({
status: "success",
text: intl.formatMessage(commonMessages.savedChanges)
});
return [];
};
const handleBack = () => navigate(orderListUrl());
return (
<OrderSettingsPage
data={data?.orderSettings}
disabled={loading || orderSettingsUpdateOpts.loading}
onSubmit={handleSubmit}
onBack={handleBack}
saveButtonBarState={getMutationState(
orderSettingsUpdateOpts.called,
orderSettingsUpdateOpts.loading,
[...(orderSettingsUpdateOpts.data?.orderSettingsUpdate.errors || [])]
)}
/>
);
};
export default OrderSettings;

View file

@ -67,6 +67,7 @@ export interface PageCreate_pageCreate_page_pageType {
id: string; id: string;
name: string; name: string;
attributes: (PageCreate_pageCreate_page_pageType_attributes | null)[] | null; attributes: (PageCreate_pageCreate_page_pageType_attributes | null)[] | null;
message: string | null;
} }
export interface PageCreate_pageCreate_page_metadata { export interface PageCreate_pageCreate_page_metadata {

File diff suppressed because it is too large Load diff

View file

@ -37,6 +37,7 @@ const props: OrderListPageProps = {
value: [OrderStatusFilter.CANCELED, OrderStatusFilter.FULFILLED] value: [OrderStatusFilter.CANCELED, OrderStatusFilter.FULFILLED]
} }
}, },
onSettingsOpen: () => undefined,
orders, orders,
sort: { sort: {
...sortPageProps.sort, ...sortPageProps.sort,

View file

@ -592,6 +592,7 @@ export enum OrderErrorCode {
} }
export enum OrderEventsEmailsEnum { export enum OrderEventsEmailsEnum {
CONFIRMED = "CONFIRMED",
DIGITAL_LINKS = "DIGITAL_LINKS", DIGITAL_LINKS = "DIGITAL_LINKS",
FULFILLMENT_CONFIRMATION = "FULFILLMENT_CONFIRMATION", FULFILLMENT_CONFIRMATION = "FULFILLMENT_CONFIRMATION",
ORDER_CANCEL = "ORDER_CANCEL", ORDER_CANCEL = "ORDER_CANCEL",
@ -604,6 +605,7 @@ export enum OrderEventsEmailsEnum {
export enum OrderEventsEnum { export enum OrderEventsEnum {
CANCELED = "CANCELED", CANCELED = "CANCELED",
CONFIRMED = "CONFIRMED",
DRAFT_ADDED_PRODUCTS = "DRAFT_ADDED_PRODUCTS", DRAFT_ADDED_PRODUCTS = "DRAFT_ADDED_PRODUCTS",
DRAFT_CREATED = "DRAFT_CREATED", DRAFT_CREATED = "DRAFT_CREATED",
DRAFT_REMOVED_PRODUCTS = "DRAFT_REMOVED_PRODUCTS", DRAFT_REMOVED_PRODUCTS = "DRAFT_REMOVED_PRODUCTS",
@ -632,6 +634,10 @@ export enum OrderEventsEnum {
UPDATED_ADDRESS = "UPDATED_ADDRESS", UPDATED_ADDRESS = "UPDATED_ADDRESS",
} }
export enum OrderSettingsErrorCode {
INVALID = "INVALID",
}
export enum OrderSortField { export enum OrderSortField {
CREATION_DATE = "CREATION_DATE", CREATION_DATE = "CREATION_DATE",
CUSTOMER = "CUSTOMER", CUSTOMER = "CUSTOMER",
@ -645,6 +651,7 @@ export enum OrderStatus {
DRAFT = "DRAFT", DRAFT = "DRAFT",
FULFILLED = "FULFILLED", FULFILLED = "FULFILLED",
PARTIALLY_FULFILLED = "PARTIALLY_FULFILLED", PARTIALLY_FULFILLED = "PARTIALLY_FULFILLED",
UNCONFIRMED = "UNCONFIRMED",
UNFULFILLED = "UNFULFILLED", UNFULFILLED = "UNFULFILLED",
} }
@ -654,6 +661,7 @@ export enum OrderStatusFilter {
PARTIALLY_FULFILLED = "PARTIALLY_FULFILLED", PARTIALLY_FULFILLED = "PARTIALLY_FULFILLED",
READY_TO_CAPTURE = "READY_TO_CAPTURE", READY_TO_CAPTURE = "READY_TO_CAPTURE",
READY_TO_FULFILL = "READY_TO_FULFILL", READY_TO_FULFILL = "READY_TO_FULFILL",
UNCONFIRMED = "UNCONFIRMED",
UNFULFILLED = "UNFULFILLED", UNFULFILLED = "UNFULFILLED",
} }
@ -954,6 +962,7 @@ export enum WebhookEventTypeEnum {
INVOICE_REQUESTED = "INVOICE_REQUESTED", INVOICE_REQUESTED = "INVOICE_REQUESTED",
INVOICE_SENT = "INVOICE_SENT", INVOICE_SENT = "INVOICE_SENT",
ORDER_CANCELLED = "ORDER_CANCELLED", ORDER_CANCELLED = "ORDER_CANCELLED",
ORDER_CONFIRMED = "ORDER_CONFIRMED",
ORDER_CREATED = "ORDER_CREATED", ORDER_CREATED = "ORDER_CREATED",
ORDER_FULFILLED = "ORDER_FULFILLED", ORDER_FULFILLED = "ORDER_FULFILLED",
ORDER_FULLY_PAID = "ORDER_FULLY_PAID", ORDER_FULLY_PAID = "ORDER_FULLY_PAID",
@ -1342,6 +1351,10 @@ export interface OrderLineInput {
quantity: number; quantity: number;
} }
export interface OrderSettingsUpdateInput {
automaticallyConfirmAllNewOrders: boolean;
}
export interface OrderSortingInput { export interface OrderSortingInput {
direction: OrderDirection; direction: OrderDirection;
field: OrderSortField; field: OrderSortField;

View file

@ -60,6 +60,10 @@ const WebhookEvents: React.FC<WebhookEventsProps> = ({
defaultMessage: "Order created", defaultMessage: "Order created",
description: "event" description: "event"
}), }),
[WebhookEventTypeEnum.ORDER_CONFIRMED]: intl.formatMessage({
defaultMessage: "Order confirmed",
description: "event"
}),
[WebhookEventTypeEnum.ORDER_FULFILLED]: intl.formatMessage({ [WebhookEventTypeEnum.ORDER_FULFILLED]: intl.formatMessage({
defaultMessage: "Order fulfilled", defaultMessage: "Order fulfilled",
description: "event" description: "event"