From 07f8f4b0b8da036a2cd33e0f4423e1d04bb62935 Mon Sep 17 00:00:00 2001 From: Krzysztof Wolski Date: Mon, 30 Nov 2020 14:19:57 +0100 Subject: [PATCH] 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 Co-authored-by: Magdalena Markusik Co-authored-by: Dawid Tarasiuk --- CHANGELOG.md | 1 + locale/defaultMessages.json | 39 + schema.graphql | 39 + .../StatusChip/StatusChip.stories.tsx | 13 + src/components/StatusChip/StatusChip.tsx | 76 ++ src/components/StatusChip/index.ts | 1 + src/components/StatusChip/types.ts | 6 + src/fragments/errors.ts | 7 + src/fragments/orders.ts | 6 + .../types/OrderSettingsErrorFragment.ts | 15 + src/fragments/types/OrderSettingsFragment.ts | 12 + src/misc.ts | 27 +- .../OrderDetailsPage/OrderDetailsPage.tsx | 23 +- .../components/OrderDetailsPage/Title.tsx | 46 + .../components/OrderHistory/OrderHistory.tsx | 5 + .../OrderListPage/OrderListPage.tsx | 28 + .../OrderSettings/OrderSettings.tsx | 61 ++ src/orders/components/OrderSettings/index.ts | 2 + .../OrderSettingsPage.stories.tsx | 21 + .../OrderSettingsPage/OrderSettingsPage.tsx | 62 ++ .../components/OrderSettingsPage/form.tsx | 71 ++ .../components/OrderSettingsPage/index.ts | 2 + src/orders/fixtures.ts | 6 + src/orders/index.tsx | 3 + src/orders/mutations.ts | 49 +- src/orders/queries.ts | 18 +- src/orders/types/OrderConfirm.ts | 343 +++++++ src/orders/types/OrderSettings.ts | 16 + src/orders/types/OrderSettingsUpdate.ts | 34 + src/orders/urls.ts | 2 + src/orders/views/OrderDetails/index.tsx | 20 + src/orders/views/OrderList/OrderList.tsx | 2 + src/orders/views/OrderSettings.tsx | 64 ++ src/pages/types/PageCreate.ts | 1 + .../__snapshots__/Stories.test.ts.snap | 933 +++++++++++++++++- .../stories/orders/OrderListPage.tsx | 1 + src/types/globalTypes.ts | 13 + .../WebhookEvents/WebhookEvents.tsx | 4 + 38 files changed, 2040 insertions(+), 32 deletions(-) create mode 100644 src/components/StatusChip/StatusChip.stories.tsx create mode 100644 src/components/StatusChip/StatusChip.tsx create mode 100644 src/components/StatusChip/index.ts create mode 100644 src/components/StatusChip/types.ts create mode 100644 src/fragments/types/OrderSettingsErrorFragment.ts create mode 100644 src/fragments/types/OrderSettingsFragment.ts create mode 100644 src/orders/components/OrderDetailsPage/Title.tsx create mode 100644 src/orders/components/OrderSettings/OrderSettings.tsx create mode 100644 src/orders/components/OrderSettings/index.ts create mode 100644 src/orders/components/OrderSettingsPage/OrderSettingsPage.stories.tsx create mode 100644 src/orders/components/OrderSettingsPage/OrderSettingsPage.tsx create mode 100644 src/orders/components/OrderSettingsPage/form.tsx create mode 100644 src/orders/components/OrderSettingsPage/index.ts create mode 100644 src/orders/types/OrderConfirm.ts create mode 100644 src/orders/types/OrderSettings.ts create mode 100644 src/orders/types/OrderSettingsUpdate.ts create mode 100644 src/orders/views/OrderSettings.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index aefbcbb44..887a18556 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ All notable, unreleased changes to this project will be documented in this file. ## [Unreleased] +- Add Order Confirmation settings - #840 by @orzechdev and @mmarkusik - Add Page Types - #807 by @orzechdev - Add shipping methods to translation section - #864 by @marekchoinski diff --git a/locale/defaultMessages.json b/locale/defaultMessages.json index 1967bf70a..79ef21b79 100644 --- a/locale/defaultMessages.json +++ b/locale/defaultMessages.json @@ -3093,6 +3093,10 @@ "context": "button", "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": { "context": "dialog header", "string": "Delete Daft Order" @@ -3371,6 +3375,10 @@ "context": "order history message", "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": { "context": "order history message", "string": "Payment was captured" @@ -3472,6 +3480,10 @@ "context": "generate invoice button", "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": { "context": "button", "string": "Create order" @@ -3621,6 +3633,25 @@ "src_dot_orders_dot_components_dot_OrderProductAddDialog_dot_353369701": { "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": { "context": "dialog header", "string": "Edit Shipping Method" @@ -5934,6 +5965,10 @@ "src_dot_translations_dot_components_dot_TranslationsVouchersPage_dot_2599922713": { "string": "Voucher Name" }, + "src_dot_unconfirmed": { + "context": "order status", + "string": "Unconfirmed" + }, "src_dot_undo": { "context": "button", "string": "Undo" @@ -6229,6 +6264,10 @@ "context": "event", "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": { "context": "event", "string": "Invoice deleted" diff --git a/schema.graphql b/schema.graphql index 95946c486..85e21ee84 100644 --- a/schema.graphql +++ b/schema.graphql @@ -2554,6 +2554,7 @@ type Mutation { shopFetchTaxRates: ShopFetchTaxRates shopSettingsTranslate(input: ShopSettingsTranslationInput!, languageCode: LanguageCodeEnum!): ShopSettingsTranslate shopAddressUpdate(input: AddressInput): ShopAddressUpdate + orderSettingsUpdate(input: OrderSettingsUpdateInput!): OrderSettingsUpdate shippingMethodChannelListingUpdate(id: ID!, input: ShippingMethodChannelListingInput!): ShippingMethodChannelListingUpdate shippingPriceCreate(input: ShippingPriceInput!): ShippingPriceCreate shippingPriceDelete(id: ID!): ShippingPriceDelete @@ -2643,6 +2644,7 @@ type Mutation { orderAddNote(order: ID!, input: OrderAddNoteInput!): OrderAddNote orderCancel(id: ID!): OrderCancel orderCapture(amount: PositiveDecimal!, id: ID!): OrderCapture + orderConfirm(id: ID!): OrderConfirm orderFulfill(input: OrderFulfillInput!, order: ID): OrderFulfill orderFulfillmentCancel(id: ID!, input: FulfillmentCancelInput!): FulfillmentCancel orderFulfillmentUpdateTracking(id: ID!, input: FulfillmentUpdateTrackingInput!): FulfillmentUpdateTracking @@ -2882,6 +2884,12 @@ type OrderCapture { 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 { pageInfo: PageInfo! edges: [OrderCountableEdge!]! @@ -2984,6 +2992,7 @@ type OrderEventOrderLineObject { enum OrderEventsEmailsEnum { PAYMENT_CONFIRMATION + CONFIRMED SHIPPING_CONFIRMATION TRACKING_UPDATED ORDER_CONFIRMATION @@ -3005,6 +3014,7 @@ enum OrderEventsEnum { ORDER_FULLY_PAID UPDATED_ADDRESS EMAIL_SENT + CONFIRMED PAYMENT_AUTHORIZED PAYMENT_CAPTURED EXTERNAL_SERVICE_NOTIFICATION @@ -3093,6 +3103,30 @@ type OrderRefund { 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 { NUMBER CREATION_DATE @@ -3108,6 +3142,7 @@ input OrderSortingInput { enum OrderStatus { DRAFT + UNCONFIRMED UNFULFILLED PARTIALLY_FULFILLED FULFILLED @@ -3118,6 +3153,7 @@ enum OrderStatusFilter { READY_TO_FULFILL READY_TO_CAPTURE UNFULFILLED + UNCONFIRMED PARTIALLY_FULFILLED FULFILLED CANCELED @@ -4291,6 +4327,7 @@ type Query { stock(id: ID!): Stock stocks(filter: StockFilterInput, before: String, after: String, first: Int, last: Int): StockCountableConnection shop: Shop! + orderSettings: OrderSettings shippingZone(id: ID!, channel: String): ShippingZone shippingZones(channel: String, before: String, after: String, first: Int, last: Int): ShippingZoneCountableConnection digitalContent(id: ID!): DigitalContent @@ -5598,6 +5635,7 @@ type WebhookEvent { enum WebhookEventTypeEnum { ANY_EVENTS ORDER_CREATED + ORDER_CONFIRMED ORDER_FULLY_PAID ORDER_UPDATED ORDER_CANCELLED @@ -5616,6 +5654,7 @@ enum WebhookEventTypeEnum { enum WebhookSampleEventTypeEnum { ORDER_CREATED + ORDER_CONFIRMED ORDER_FULLY_PAID ORDER_UPDATED ORDER_CANCELLED diff --git a/src/components/StatusChip/StatusChip.stories.tsx b/src/components/StatusChip/StatusChip.stories.tsx new file mode 100644 index 000000000..07a4e5800 --- /dev/null +++ b/src/components/StatusChip/StatusChip.stories.tsx @@ -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", () => ) + .add("error", () => ) + .add("success", () => ) + .add("alert", () => ); diff --git a/src/components/StatusChip/StatusChip.tsx b/src/components/StatusChip/StatusChip.tsx new file mode 100644 index 000000000..899c40ae8 --- /dev/null +++ b/src/components/StatusChip/StatusChip.tsx @@ -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 = props => { + const { type = StatusType.NEUTRAL, label } = props; + const classes = useStyles(props); + + if (!label) { + return null; + } + + return ( +
+ + {label} + +
+ ); +}; + +export default StatusChip; diff --git a/src/components/StatusChip/index.ts b/src/components/StatusChip/index.ts new file mode 100644 index 000000000..d3563e4fb --- /dev/null +++ b/src/components/StatusChip/index.ts @@ -0,0 +1 @@ +export { default } from "./StatusChip"; diff --git a/src/components/StatusChip/types.ts b/src/components/StatusChip/types.ts new file mode 100644 index 000000000..1f826401b --- /dev/null +++ b/src/components/StatusChip/types.ts @@ -0,0 +1,6 @@ +export enum StatusType { + NEUTRAL = "neutral", + ERROR = "error", + ALERT = "alert", + SUCCESS = "success" +} diff --git a/src/fragments/errors.ts b/src/fragments/errors.ts index 92dbdfa80..15989c329 100644 --- a/src/fragments/errors.ts +++ b/src/fragments/errors.ts @@ -69,6 +69,13 @@ export const orderErrorFragment = gql` } `; +export const orderSettingsErrorFragment = gql` + fragment OrderSettingsErrorFragment on OrderSettingsError { + code + field + } +`; + export const pageErrorFragment = gql` fragment PageErrorFragment on PageError { code diff --git a/src/fragments/orders.ts b/src/fragments/orders.ts index 719bd3bda..2701ac70c 100644 --- a/src/fragments/orders.ts +++ b/src/fragments/orders.ts @@ -175,3 +175,9 @@ export const fragmentOrderDetails = gql` isPaid } `; + +export const fragmentOrderSettings = gql` + fragment OrderSettingsFragment on OrderSettings { + automaticallyConfirmAllNewOrders + } +`; diff --git a/src/fragments/types/OrderSettingsErrorFragment.ts b/src/fragments/types/OrderSettingsErrorFragment.ts new file mode 100644 index 000000000..abdd5d28f --- /dev/null +++ b/src/fragments/types/OrderSettingsErrorFragment.ts @@ -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; +} diff --git a/src/fragments/types/OrderSettingsFragment.ts b/src/fragments/types/OrderSettingsFragment.ts new file mode 100644 index 000000000..58895e0bd --- /dev/null +++ b/src/fragments/types/OrderSettingsFragment.ts @@ -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; +} diff --git a/src/misc.ts b/src/misc.ts index 2e661000a..1abbd8bce 100644 --- a/src/misc.ts +++ b/src/misc.ts @@ -4,6 +4,7 @@ import { defineMessages, IntlShape } from "react-intl"; import urlJoin from "url-join"; import { ConfirmButtonTransitionState } from "./components/ConfirmButton/ConfirmButton"; +import { StatusType } from "./components/StatusChip/types"; import { APP_MOUNT_URI } from "./config"; import { AddressType, AddressTypeInput } from "./customers/types"; import { @@ -144,43 +145,55 @@ export const orderStatusMessages = defineMessages({ defaultMessage: "Ready to fulfill", description: "order status" }, + unconfirmed: { + defaultMessage: "Unconfirmed", + description: "order status" + }, unfulfilled: { defaultMessage: "Unfulfilled", description: "order status" } }); -export const transformOrderStatus = (status: string, intl: IntlShape) => { +export const transformOrderStatus = ( + status: string, + intl: IntlShape +): { localized: string; status: StatusType } => { switch (status) { case OrderStatus.FULFILLED: return { localized: intl.formatMessage(orderStatusMessages.fulfilled), - status: "success" + status: StatusType.SUCCESS }; case OrderStatus.PARTIALLY_FULFILLED: return { localized: intl.formatMessage(orderStatusMessages.partiallyFulfilled), - status: "neutral" + status: StatusType.NEUTRAL }; case OrderStatus.UNFULFILLED: return { localized: intl.formatMessage(orderStatusMessages.unfulfilled), - status: "error" + status: StatusType.ERROR }; case OrderStatus.CANCELED: return { localized: intl.formatMessage(orderStatusMessages.cancelled), - status: "error" + status: StatusType.ERROR }; case OrderStatus.DRAFT: return { localized: intl.formatMessage(orderStatusMessages.draft), - status: "error" + status: StatusType.ERROR + }; + case OrderStatus.UNCONFIRMED: + return { + localized: intl.formatMessage(orderStatusMessages.unconfirmed), + status: StatusType.NEUTRAL }; } return { localized: status, - status: "error" + status: StatusType.ERROR }; }; diff --git a/src/orders/components/OrderDetailsPage/OrderDetailsPage.tsx b/src/orders/components/OrderDetailsPage/OrderDetailsPage.tsx index 2d3fdd7ff..9d1016e85 100644 --- a/src/orders/components/OrderDetailsPage/OrderDetailsPage.tsx +++ b/src/orders/components/OrderDetailsPage/OrderDetailsPage.tsx @@ -31,6 +31,7 @@ import OrderHistory, { FormData as HistoryFormData } from "../OrderHistory"; import OrderInvoiceList from "../OrderInvoiceList"; import OrderPayment from "../OrderPayment/OrderPayment"; import OrderUnfulfilledItems from "../OrderUnfulfilledItems/OrderUnfulfilledItems"; +import Title from "./Title"; const useStyles = makeStyles( theme => ({ @@ -39,6 +40,7 @@ const useStyles = makeStyles( }, header: { display: "flex", + justifyContent: "space-between", marginBottom: 0 } }), @@ -136,6 +138,22 @@ const OrderDetailsPage: React.FC = props => { 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 (
{({ change, data, hasChanged, submit }) => { @@ -149,7 +167,7 @@ const OrderDetailsPage: React.FC = props => { order.number) ? "#" + order.number : undefined} + title={} > {canCancel && ( <CardMenu @@ -243,10 +261,11 @@ const OrderDetailsPage: React.FC<OrderDetailsPageProps> = props => { </div> </Grid> <SaveButtonBar + labels={{ save: saveLabel }} onCancel={onBack} onSave={submit} state={saveButtonBarState} - disabled={disabled || !hasChanged} + disabled={allowSave(hasChanged)} /> </Container> ); diff --git a/src/orders/components/OrderDetailsPage/Title.tsx b/src/orders/components/OrderDetailsPage/Title.tsx new file mode 100644 index 000000000..3a71af776 --- /dev/null +++ b/src/orders/components/OrderDetailsPage/Title.tsx @@ -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; diff --git a/src/orders/components/OrderHistory/OrderHistory.tsx b/src/orders/components/OrderHistory/OrderHistory.tsx index ca10f8956..4bd735351 100644 --- a/src/orders/components/OrderHistory/OrderHistory.tsx +++ b/src/orders/components/OrderHistory/OrderHistory.tsx @@ -226,6 +226,11 @@ const getEventMessage = (event: OrderDetails_order_events, intl: IntlShape) => { defaultMessage: "Payment was authorized", description: "order history message" }); + case OrderEventsEnum.CONFIRMED: + return intl.formatMessage({ + defaultMessage: "Order was confirmed", + description: "order history message" + }); case OrderEventsEnum.EXTERNAL_SERVICE_NOTIFICATION: return event.message; } diff --git a/src/orders/components/OrderListPage/OrderListPage.tsx b/src/orders/components/OrderListPage/OrderListPage.tsx index 9ed91dfd0..555be172e 100644 --- a/src/orders/components/OrderListPage/OrderListPage.tsx +++ b/src/orders/components/OrderListPage/OrderListPage.tsx @@ -1,5 +1,7 @@ import Button from "@material-ui/core/Button"; 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 FilterBar from "@saleor/components/FilterBar"; import PageHeader from "@saleor/components/PageHeader"; @@ -22,8 +24,18 @@ export interface OrderListPageProps FilterPageProps<OrderFilterKeys, OrderListFilterOpts>, SortPage<OrderListUrlSortField> { orders: OrderList_orders_edges_node[]; + onSettingsOpen: () => void; } +const useStyles = makeStyles( + theme => ({ + settings: { + marginRight: theme.spacing(2) + } + }), + { name: "OrderListPage" } +); + const OrderListPage: React.FC<OrderListPageProps> = ({ currentTab, initialSearch, @@ -32,6 +44,7 @@ const OrderListPage: React.FC<OrderListPageProps> = ({ onAdd, onAll, onSearchChange, + onSettingsOpen, onFilterChange, onTabChange, onTabDelete, @@ -39,11 +52,26 @@ const OrderListPage: React.FC<OrderListPageProps> = ({ ...listProps }) => { const intl = useIntl(); + const classes = useStyles({}); const filterStructure = createFilterStructure(intl, filterOpts); return ( <Container> <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}> <FormattedMessage defaultMessage="Create order" diff --git a/src/orders/components/OrderSettings/OrderSettings.tsx b/src/orders/components/OrderSettings/OrderSettings.tsx new file mode 100644 index 000000000..6c1ebe1f2 --- /dev/null +++ b/src/orders/components/OrderSettings/OrderSettings.tsx @@ -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; diff --git a/src/orders/components/OrderSettings/index.ts b/src/orders/components/OrderSettings/index.ts new file mode 100644 index 000000000..2956066c8 --- /dev/null +++ b/src/orders/components/OrderSettings/index.ts @@ -0,0 +1,2 @@ +export { default } from "./OrderSettings"; +export * from "./OrderSettings"; diff --git a/src/orders/components/OrderSettingsPage/OrderSettingsPage.stories.tsx b/src/orders/components/OrderSettingsPage/OrderSettingsPage.stories.tsx new file mode 100644 index 000000000..503d08325 --- /dev/null +++ b/src/orders/components/OrderSettingsPage/OrderSettingsPage.stories.tsx @@ -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} /> + )); diff --git a/src/orders/components/OrderSettingsPage/OrderSettingsPage.tsx b/src/orders/components/OrderSettingsPage/OrderSettingsPage.tsx new file mode 100644 index 000000000..577a1d150 --- /dev/null +++ b/src/orders/components/OrderSettingsPage/OrderSettingsPage.tsx @@ -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; diff --git a/src/orders/components/OrderSettingsPage/form.tsx b/src/orders/components/OrderSettingsPage/form.tsx new file mode 100644 index 000000000..4cefed89c --- /dev/null +++ b/src/orders/components/OrderSettingsPage/form.tsx @@ -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; diff --git a/src/orders/components/OrderSettingsPage/index.ts b/src/orders/components/OrderSettingsPage/index.ts new file mode 100644 index 000000000..b9606de0f --- /dev/null +++ b/src/orders/components/OrderSettingsPage/index.ts @@ -0,0 +1,2 @@ +export { default } from "./OrderSettingsPage"; +export * from "./OrderSettingsPage"; diff --git a/src/orders/fixtures.ts b/src/orders/fixtures.ts index bee42f8a8..9b407165f 100644 --- a/src/orders/fixtures.ts +++ b/src/orders/fixtures.ts @@ -1,4 +1,5 @@ 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 { warehouseList } from "@saleor/warehouses/fixtures"; 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" } ]; + +export const orderSettings: OrderSettingsFragment = { + __typename: "OrderSettings", + automaticallyConfirmAllNewOrders: true +}; diff --git a/src/orders/index.tsx b/src/orders/index.tsx index 410c0d17b..3983b91ba 100644 --- a/src/orders/index.tsx +++ b/src/orders/index.tsx @@ -15,12 +15,14 @@ import { OrderListUrlQueryParams, OrderListUrlSortField, orderPath, + orderSettingsPath, OrderUrlQueryParams } from "./urls"; import OrderDetailsComponent from "./views/OrderDetails"; import OrderDraftListComponent from "./views/OrderDraftList"; import OrderFulfillComponent from "./views/OrderFulfill"; import OrderListComponent from "./views/OrderList"; +import OrderSettings from "./views/OrderSettings"; const OrderList: React.FC<RouteComponentProps<any>> = ({ location }) => { const qs = parseQs(location.search.substr(1)); @@ -70,6 +72,7 @@ const Component = () => { <> <WindowTitle title={intl.formatMessage(sectionNames.orders)} /> <Switch> + <Route exact path={orderSettingsPath} component={OrderSettings} /> <Route exact path={orderDraftListPath} component={OrderDraftList} /> <Route exact path={orderListPath} component={OrderList} /> <Route path={orderFulfillPath(":id")} component={OrderFulfill} /> diff --git a/src/orders/mutations.ts b/src/orders/mutations.ts index 6dbc41418..acc65aff4 100644 --- a/src/orders/mutations.ts +++ b/src/orders/mutations.ts @@ -1,10 +1,12 @@ import { invoiceErrorFragment, - orderErrorFragment + orderErrorFragment, + orderSettingsErrorFragment } from "@saleor/fragments/errors"; import { fragmentOrderDetails, fragmentOrderEvent, + fragmentOrderSettings, invoiceFragment } from "@saleor/fragments/orders"; import makeMutation from "@saleor/hooks/makeMutation"; @@ -23,6 +25,7 @@ import { import { OrderAddNote, OrderAddNoteVariables } from "./types/OrderAddNote"; import { OrderCancel, OrderCancelVariables } from "./types/OrderCancel"; import { OrderCapture, OrderCaptureVariables } from "./types/OrderCapture"; +import { OrderConfirm, OrderConfirmVariables } from "./types/OrderConfirm"; import { OrderDraftBulkCancel, OrderDraftBulkCancelVariables @@ -65,6 +68,10 @@ import { OrderMarkAsPaidVariables } from "./types/OrderMarkAsPaid"; import { OrderRefund, OrderRefundVariables } from "./types/OrderRefund"; +import { + OrderSettingsUpdate, + OrderSettingsUpdateVariables +} from "./types/OrderSettingsUpdate"; import { OrderShippingMethodUpdate, 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< OrderDraftBulkCancel, OrderDraftBulkCancelVariables @@ -500,3 +528,22 @@ export const TypedInvoiceEmailSendMutation = TypedMutation< InvoiceEmailSend, InvoiceEmailSendVariables >(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); diff --git a/src/orders/queries.ts b/src/orders/queries.ts index 4808d6315..fe7106d6b 100644 --- a/src/orders/queries.ts +++ b/src/orders/queries.ts @@ -1,5 +1,8 @@ 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 makeTopLevelSearch from "@saleor/hooks/makeTopLevelSearch"; import gql from "graphql-tag"; @@ -15,6 +18,7 @@ import { OrderFulfillDataVariables } from "./types/OrderFulfillData"; import { OrderList, OrderListVariables } from "./types/OrderList"; +import { OrderSettings } from "./types/OrderSettings"; import { SearchOrderVariant as SearchOrderVariantType, SearchOrderVariantVariables @@ -238,3 +242,15 @@ export const useOrderFulfillData = makeQuery< OrderFulfillData, OrderFulfillDataVariables >(orderFulfillData); + +export const orderSettingsQuery = gql` + ${fragmentOrderSettings} + query OrderSettings { + orderSettings { + ...OrderSettingsFragment + } + } +`; +export const useOrderSettingsQuery = makeQuery<OrderSettings, never>( + orderSettingsQuery +); diff --git a/src/orders/types/OrderConfirm.ts b/src/orders/types/OrderConfirm.ts new file mode 100644 index 000000000..ae7a8e772 --- /dev/null +++ b/src/orders/types/OrderConfirm.ts @@ -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; +} diff --git a/src/orders/types/OrderSettings.ts b/src/orders/types/OrderSettings.ts new file mode 100644 index 000000000..ef349b8de --- /dev/null +++ b/src/orders/types/OrderSettings.ts @@ -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; +} diff --git a/src/orders/types/OrderSettingsUpdate.ts b/src/orders/types/OrderSettingsUpdate.ts new file mode 100644 index 000000000..4f57a6c43 --- /dev/null +++ b/src/orders/types/OrderSettingsUpdate.ts @@ -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; +} diff --git a/src/orders/urls.ts b/src/orders/urls.ts index 4f5dc2128..1d7473e85 100644 --- a/src/orders/urls.ts +++ b/src/orders/urls.ts @@ -114,3 +114,5 @@ export const orderFulfillPath = (id: string) => urlJoin(orderPath(id), "fulfill"); export const orderFulfillUrl = (id: string) => orderFulfillPath(encodeURIComponent(id)); + +export const orderSettingsPath = urlJoin(orderSectionUrl, "settings"); diff --git a/src/orders/views/OrderDetails/index.tsx b/src/orders/views/OrderDetails/index.tsx index 15f173a2d..5bb46ff4d 100644 --- a/src/orders/views/OrderDetails/index.tsx +++ b/src/orders/views/OrderDetails/index.tsx @@ -10,8 +10,10 @@ import useUser from "@saleor/hooks/useUser"; import { commonMessages } from "@saleor/intl"; import OrderCannotCancelOrderDialog from "@saleor/orders/components/OrderCannotCancelOrderDialog"; import OrderInvoiceEmailSendDialog from "@saleor/orders/components/OrderInvoiceEmailSendDialog"; +import { useOrderConfirmMutation } from "@saleor/orders/mutations"; import { InvoiceRequest } from "@saleor/orders/types/InvoiceRequest"; import useCustomerSearch from "@saleor/searches/useCustomerSearch"; +import getOrderErrorMessage from "@saleor/utils/errors/order"; import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers"; import createMetadataUpdateHandler from "@saleor/utils/handlers/metadataUpdateHandler"; import { @@ -70,6 +72,7 @@ export const OrderDetails: React.FC<OrderDetailsProps> = ({ id, params }) => { } = useCustomerSearch({ variables: DEFAULT_INITIAL_SEARCH_DATA }); + const { loadMore, search: variantSearch, @@ -100,6 +103,19 @@ export const OrderDetails: React.FC<OrderDetailsProps> = ({ id, params }) => { 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 ( <TypedOrderDetailsQuery displayLoader variables={{ id }}> {({ data, loading }) => { @@ -109,6 +125,10 @@ export const OrderDetails: React.FC<OrderDetailsProps> = ({ id, params }) => { } const handleSubmit = async (data: MetadataFormData) => { + if (order?.status === OrderStatus.UNCONFIRMED) { + await orderConfirm({ variables: { id: order?.id } }); + } + const update = createMetadataUpdateHandler( order, () => Promise.resolve([]), diff --git a/src/orders/views/OrderList/OrderList.tsx b/src/orders/views/OrderList/OrderList.tsx index 4b8a5002f..8c48e961c 100644 --- a/src/orders/views/OrderList/OrderList.tsx +++ b/src/orders/views/OrderList/OrderList.tsx @@ -27,6 +27,7 @@ import { orderListUrl, OrderListUrlDialog, OrderListUrlQueryParams, + orderSettingsPath, orderUrl } from "../../urls"; import { @@ -160,6 +161,7 @@ export const OrderList: React.FC<OrderListProps> = ({ params }) => { initialSearch={params.query || ""} tabs={getFilterTabs().map(tab => tab.name)} onAll={resetFilters} + onSettingsOpen={() => navigate(orderSettingsPath)} /> <SaveFilterTabDialog open={params.action === "save-search"} diff --git a/src/orders/views/OrderSettings.tsx b/src/orders/views/OrderSettings.tsx new file mode 100644 index 000000000..76c484805 --- /dev/null +++ b/src/orders/views/OrderSettings.tsx @@ -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; diff --git a/src/pages/types/PageCreate.ts b/src/pages/types/PageCreate.ts index 320ba975b..abc82deaf 100644 --- a/src/pages/types/PageCreate.ts +++ b/src/pages/types/PageCreate.ts @@ -67,6 +67,7 @@ export interface PageCreate_pageCreate_page_pageType { id: string; name: string; attributes: (PageCreate_pageCreate_page_pageType_attributes | null)[] | null; + message: string | null; } export interface PageCreate_pageCreate_page_metadata { diff --git a/src/storybook/__snapshots__/Stories.test.ts.snap b/src/storybook/__snapshots__/Stories.test.ts.snap index 24c0a8ee2..ceebc185b 100644 --- a/src/storybook/__snapshots__/Stories.test.ts.snap +++ b/src/storybook/__snapshots__/Stories.test.ts.snap @@ -9244,6 +9244,70 @@ exports[`Storyshots Generics / Square Button default 1`] = ` </div> `; +exports[`Storyshots Generics / Status Chip alert 1`] = ` +<div + style="padding:24px" +> + <div + class="StatusChip-root-id StatusChip-alert-id" + > + <div + class="MuiTypography-root-id StatusChip-label-id StatusChip-alertLabel-id MuiTypography-body1-id" + > + label + </div> + </div> +</div> +`; + +exports[`Storyshots Generics / Status Chip error 1`] = ` +<div + style="padding:24px" +> + <div + class="StatusChip-root-id StatusChip-error-id" + > + <div + class="MuiTypography-root-id StatusChip-label-id StatusChip-errorLabel-id MuiTypography-body1-id" + > + label + </div> + </div> +</div> +`; + +exports[`Storyshots Generics / Status Chip neutral 1`] = ` +<div + style="padding:24px" +> + <div + class="StatusChip-root-id StatusChip-neutral-id" + > + <div + class="MuiTypography-root-id StatusChip-label-id StatusChip-neutralLabel-id MuiTypography-body1-id" + > + label + </div> + </div> +</div> +`; + +exports[`Storyshots Generics / Status Chip success 1`] = ` +<div + style="padding:24px" +> + <div + class="StatusChip-root-id StatusChip-success-id" + > + <div + class="MuiTypography-root-id StatusChip-label-id StatusChip-successLabel-id MuiTypography-body1-id" + > + label + </div> + </div> +</div> +`; + exports[`Storyshots Generics / StatusLabel when error 1`] = ` <div style="padding:24px" @@ -19062,6 +19126,48 @@ exports[`Storyshots Views / Apps / Webhooks / Create webhook default 1`] = ` </span> </label> </div> + <div> + <label + class="MuiFormControlLabel-root-id" + > + <span + aria-disabled="false" + class="MuiButtonBase-root-id MuiIconButton-root-id PrivateSwitchBase-root-id MuiCheckbox-root-id MuiCheckbox-colorPrimary-id MuiIconButton-colorPrimary-id" + > + <span + class="MuiIconButton-label-id" + > + <input + class="PrivateSwitchBase-input-id" + data-indeterminate="false" + name="ORDER_CONFIRMED" + type="checkbox" + /> + <svg + aria-hidden="true" + class="MuiSvgIcon-root-id" + focusable="false" + role="presentation" + viewBox="0 0 24 24" + > + <rect + fill="none" + height="14" + stroke="currentColor" + width="14" + x="5" + y="5" + /> + </svg> + </span> + </span> + <span + class="MuiTypography-root-id MuiFormControlLabel-label-id MuiTypography-body1-id" + > + Order confirmed + </span> + </label> + </div> <div> <label class="MuiFormControlLabel-root-id" @@ -20048,6 +20154,48 @@ exports[`Storyshots Views / Apps / Webhooks / Create webhook form errors 1`] = ` </span> </label> </div> + <div> + <label + class="MuiFormControlLabel-root-id" + > + <span + aria-disabled="false" + class="MuiButtonBase-root-id MuiIconButton-root-id PrivateSwitchBase-root-id MuiCheckbox-root-id MuiCheckbox-colorPrimary-id MuiIconButton-colorPrimary-id" + > + <span + class="MuiIconButton-label-id" + > + <input + class="PrivateSwitchBase-input-id" + data-indeterminate="false" + name="ORDER_CONFIRMED" + type="checkbox" + /> + <svg + aria-hidden="true" + class="MuiSvgIcon-root-id" + focusable="false" + role="presentation" + viewBox="0 0 24 24" + > + <rect + fill="none" + height="14" + stroke="currentColor" + width="14" + x="5" + y="5" + /> + </svg> + </span> + </span> + <span + class="MuiTypography-root-id MuiFormControlLabel-label-id MuiTypography-body1-id" + > + Order confirmed + </span> + </label> + </div> <div> <label class="MuiFormControlLabel-root-id" @@ -21052,6 +21200,50 @@ exports[`Storyshots Views / Apps / Webhooks / Create webhook loading 1`] = ` </span> </label> </div> + <div> + <label + class="MuiFormControlLabel-root-id MuiFormControlLabel-disabled-id" + > + <span + aria-disabled="true" + class="MuiButtonBase-root-id MuiIconButton-root-id PrivateSwitchBase-root-id MuiCheckbox-root-id MuiCheckbox-colorPrimary-id PrivateSwitchBase-disabled-id MuiCheckbox-disabled-id MuiIconButton-colorPrimary-id MuiIconButton-disabled-id MuiButtonBase-disabled-id" + tabindex="-1" + > + <span + class="MuiIconButton-label-id" + > + <input + class="PrivateSwitchBase-input-id" + data-indeterminate="false" + disabled="" + name="ORDER_CONFIRMED" + type="checkbox" + /> + <svg + aria-hidden="true" + class="MuiSvgIcon-root-id" + focusable="false" + role="presentation" + viewBox="0 0 24 24" + > + <rect + fill="none" + height="14" + stroke="currentColor" + width="14" + x="5" + y="5" + /> + </svg> + </span> + </span> + <span + class="MuiTypography-root-id MuiFormControlLabel-label-id MuiFormControlLabel-disabled-id MuiTypography-body1-id" + > + Order confirmed + </span> + </label> + </div> <div> <label class="MuiFormControlLabel-root-id MuiFormControlLabel-disabled-id" @@ -22059,6 +22251,48 @@ exports[`Storyshots Views / Apps / Webhooks / Webhook details default 1`] = ` </span> </label> </div> + <div> + <label + class="MuiFormControlLabel-root-id" + > + <span + aria-disabled="false" + class="MuiButtonBase-root-id MuiIconButton-root-id PrivateSwitchBase-root-id MuiCheckbox-root-id MuiCheckbox-colorPrimary-id MuiIconButton-colorPrimary-id" + > + <span + class="MuiIconButton-label-id" + > + <input + class="PrivateSwitchBase-input-id" + data-indeterminate="false" + name="ORDER_CONFIRMED" + type="checkbox" + /> + <svg + aria-hidden="true" + class="MuiSvgIcon-root-id" + focusable="false" + role="presentation" + viewBox="0 0 24 24" + > + <rect + fill="none" + height="14" + stroke="currentColor" + width="14" + x="5" + y="5" + /> + </svg> + </span> + </span> + <span + class="MuiTypography-root-id MuiFormControlLabel-label-id MuiTypography-body1-id" + > + Order confirmed + </span> + </label> + </div> <div> <label class="MuiFormControlLabel-root-id" @@ -23051,6 +23285,48 @@ exports[`Storyshots Views / Apps / Webhooks / Webhook details form errors 1`] = </span> </label> </div> + <div> + <label + class="MuiFormControlLabel-root-id" + > + <span + aria-disabled="false" + class="MuiButtonBase-root-id MuiIconButton-root-id PrivateSwitchBase-root-id MuiCheckbox-root-id MuiCheckbox-colorPrimary-id MuiIconButton-colorPrimary-id" + > + <span + class="MuiIconButton-label-id" + > + <input + class="PrivateSwitchBase-input-id" + data-indeterminate="false" + name="ORDER_CONFIRMED" + type="checkbox" + /> + <svg + aria-hidden="true" + class="MuiSvgIcon-root-id" + focusable="false" + role="presentation" + viewBox="0 0 24 24" + > + <rect + fill="none" + height="14" + stroke="currentColor" + width="14" + x="5" + y="5" + /> + </svg> + </span> + </span> + <span + class="MuiTypography-root-id MuiFormControlLabel-label-id MuiTypography-body1-id" + > + Order confirmed + </span> + </label> + </div> <div> <label class="MuiFormControlLabel-root-id" @@ -24061,6 +24337,50 @@ exports[`Storyshots Views / Apps / Webhooks / Webhook details loading 1`] = ` </span> </label> </div> + <div> + <label + class="MuiFormControlLabel-root-id MuiFormControlLabel-disabled-id" + > + <span + aria-disabled="true" + class="MuiButtonBase-root-id MuiIconButton-root-id PrivateSwitchBase-root-id MuiCheckbox-root-id MuiCheckbox-colorPrimary-id PrivateSwitchBase-disabled-id MuiCheckbox-disabled-id MuiIconButton-colorPrimary-id MuiIconButton-disabled-id MuiButtonBase-disabled-id" + tabindex="-1" + > + <span + class="MuiIconButton-label-id" + > + <input + class="PrivateSwitchBase-input-id" + data-indeterminate="false" + disabled="" + name="ORDER_CONFIRMED" + type="checkbox" + /> + <svg + aria-hidden="true" + class="MuiSvgIcon-root-id" + focusable="false" + role="presentation" + viewBox="0 0 24 24" + > + <rect + fill="none" + height="14" + stroke="currentColor" + width="14" + x="5" + y="5" + /> + </svg> + </span> + </span> + <span + class="MuiTypography-root-id MuiFormControlLabel-label-id MuiFormControlLabel-disabled-id MuiTypography-body1-id" + > + Order confirmed + </span> + </label> + </div> <div> <label class="MuiFormControlLabel-root-id MuiFormControlLabel-disabled-id" @@ -25056,6 +25376,48 @@ exports[`Storyshots Views / Apps / Webhooks / Webhook details unnamed 1`] = ` </span> </label> </div> + <div> + <label + class="MuiFormControlLabel-root-id" + > + <span + aria-disabled="false" + class="MuiButtonBase-root-id MuiIconButton-root-id PrivateSwitchBase-root-id MuiCheckbox-root-id MuiCheckbox-colorPrimary-id MuiIconButton-colorPrimary-id" + > + <span + class="MuiIconButton-label-id" + > + <input + class="PrivateSwitchBase-input-id" + data-indeterminate="false" + name="ORDER_CONFIRMED" + type="checkbox" + /> + <svg + aria-hidden="true" + class="MuiSvgIcon-root-id" + focusable="false" + role="presentation" + viewBox="0 0 24 24" + > + <rect + fill="none" + height="14" + stroke="currentColor" + width="14" + x="5" + y="5" + /> + </svg> + </span> + </span> + <span + class="MuiTypography-root-id MuiFormControlLabel-label-id MuiTypography-body1-id" + > + Order confirmed + </span> + </label> + </div> <div> <label class="MuiFormControlLabel-root-id" @@ -95901,7 +96263,24 @@ exports[`Storyshots Views / Orders / Order details cancelled 1`] = ` <div class="MuiTypography-root-id PageHeader-title-id MuiTypography-h5-id" > - #9 + <div + class="OrderDetailsTitle-container-id" + > + #9 + <div + class="OrderDetailsTitle-statusContainer-id" + > + <div + class="StatusChip-root-id StatusChip-error-id" + > + <div + class="MuiTypography-root-id StatusChip-label-id StatusChip-errorLabel-id MuiTypography-body1-id" + > + Cancelled + </div> + </div> + </div> + </div> </div> <div class="ExtendedPageHeader-action-id" @@ -97365,7 +97744,24 @@ exports[`Storyshots Views / Orders / Order details default 1`] = ` <div class="MuiTypography-root-id PageHeader-title-id MuiTypography-h5-id" > - #9 + <div + class="OrderDetailsTitle-container-id" + > + #9 + <div + class="OrderDetailsTitle-statusContainer-id" + > + <div + class="StatusChip-root-id StatusChip-neutral-id" + > + <div + class="MuiTypography-root-id StatusChip-label-id StatusChip-neutralLabel-id MuiTypography-body1-id" + > + Partially fulfilled + </div> + </div> + </div> + </div> </div> <div class="ExtendedPageHeader-action-id" @@ -99033,7 +99429,24 @@ exports[`Storyshots Views / Orders / Order details fulfilled 1`] = ` <div class="MuiTypography-root-id PageHeader-title-id MuiTypography-h5-id" > - #9 + <div + class="OrderDetailsTitle-container-id" + > + #9 + <div + class="OrderDetailsTitle-statusContainer-id" + > + <div + class="StatusChip-root-id StatusChip-success-id" + > + <div + class="MuiTypography-root-id StatusChip-label-id StatusChip-successLabel-id MuiTypography-body1-id" + > + Fulfilled + </div> + </div> + </div> + </div> </div> <div class="ExtendedPageHeader-action-id" @@ -100700,14 +101113,7 @@ exports[`Storyshots Views / Orders / Order details loading 1`] = ` > <div class="MuiTypography-root-id PageHeader-title-id MuiTypography-h5-id" - > - <span - class="Skeleton-skeleton-id" - style="width:10em" - > - ‌ - </span> - </div> + /> <div class="ExtendedPageHeader-action-id" > @@ -101442,7 +101848,24 @@ exports[`Storyshots Views / Orders / Order details no customer note 1`] = ` <div class="MuiTypography-root-id PageHeader-title-id MuiTypography-h5-id" > - #9 + <div + class="OrderDetailsTitle-container-id" + > + #9 + <div + class="OrderDetailsTitle-statusContainer-id" + > + <div + class="StatusChip-root-id StatusChip-neutral-id" + > + <div + class="MuiTypography-root-id StatusChip-label-id StatusChip-neutralLabel-id MuiTypography-body1-id" + > + Partially fulfilled + </div> + </div> + </div> + </div> </div> <div class="ExtendedPageHeader-action-id" @@ -103110,7 +103533,24 @@ exports[`Storyshots Views / Orders / Order details no payment 1`] = ` <div class="MuiTypography-root-id PageHeader-title-id MuiTypography-h5-id" > - #9 + <div + class="OrderDetailsTitle-container-id" + > + #9 + <div + class="OrderDetailsTitle-statusContainer-id" + > + <div + class="StatusChip-root-id StatusChip-neutral-id" + > + <div + class="MuiTypography-root-id StatusChip-label-id StatusChip-neutralLabel-id MuiTypography-body1-id" + > + Partially fulfilled + </div> + </div> + </div> + </div> </div> <div class="ExtendedPageHeader-action-id" @@ -104778,7 +105218,24 @@ exports[`Storyshots Views / Orders / Order details no shipping address 1`] = ` <div class="MuiTypography-root-id PageHeader-title-id MuiTypography-h5-id" > - #9 + <div + class="OrderDetailsTitle-container-id" + > + #9 + <div + class="OrderDetailsTitle-statusContainer-id" + > + <div + class="StatusChip-root-id StatusChip-neutral-id" + > + <div + class="MuiTypography-root-id StatusChip-label-id StatusChip-neutralLabel-id MuiTypography-body1-id" + > + Partially fulfilled + </div> + </div> + </div> + </div> </div> <div class="ExtendedPageHeader-action-id" @@ -106446,7 +106903,24 @@ exports[`Storyshots Views / Orders / Order details partially fulfilled 1`] = ` <div class="MuiTypography-root-id PageHeader-title-id MuiTypography-h5-id" > - #9 + <div + class="OrderDetailsTitle-container-id" + > + #9 + <div + class="OrderDetailsTitle-statusContainer-id" + > + <div + class="StatusChip-root-id StatusChip-neutral-id" + > + <div + class="MuiTypography-root-id StatusChip-label-id StatusChip-neutralLabel-id MuiTypography-body1-id" + > + Partially fulfilled + </div> + </div> + </div> + </div> </div> <div class="ExtendedPageHeader-action-id" @@ -108114,7 +108588,24 @@ exports[`Storyshots Views / Orders / Order details payment confirmed 1`] = ` <div class="MuiTypography-root-id PageHeader-title-id MuiTypography-h5-id" > - #9 + <div + class="OrderDetailsTitle-container-id" + > + #9 + <div + class="OrderDetailsTitle-statusContainer-id" + > + <div + class="StatusChip-root-id StatusChip-neutral-id" + > + <div + class="MuiTypography-root-id StatusChip-label-id StatusChip-neutralLabel-id MuiTypography-body1-id" + > + Partially fulfilled + </div> + </div> + </div> + </div> </div> <div class="ExtendedPageHeader-action-id" @@ -109782,7 +110273,24 @@ exports[`Storyshots Views / Orders / Order details payment error 1`] = ` <div class="MuiTypography-root-id PageHeader-title-id MuiTypography-h5-id" > - #9 + <div + class="OrderDetailsTitle-container-id" + > + #9 + <div + class="OrderDetailsTitle-statusContainer-id" + > + <div + class="StatusChip-root-id StatusChip-neutral-id" + > + <div + class="MuiTypography-root-id StatusChip-label-id StatusChip-neutralLabel-id MuiTypography-body1-id" + > + Partially fulfilled + </div> + </div> + </div> + </div> </div> <div class="ExtendedPageHeader-action-id" @@ -111450,7 +111958,24 @@ exports[`Storyshots Views / Orders / Order details pending payment 1`] = ` <div class="MuiTypography-root-id PageHeader-title-id MuiTypography-h5-id" > - #9 + <div + class="OrderDetailsTitle-container-id" + > + #9 + <div + class="OrderDetailsTitle-statusContainer-id" + > + <div + class="StatusChip-root-id StatusChip-neutral-id" + > + <div + class="MuiTypography-root-id StatusChip-label-id StatusChip-neutralLabel-id MuiTypography-body1-id" + > + Partially fulfilled + </div> + </div> + </div> + </div> </div> <div class="ExtendedPageHeader-action-id" @@ -113118,7 +113643,24 @@ exports[`Storyshots Views / Orders / Order details refunded payment 1`] = ` <div class="MuiTypography-root-id PageHeader-title-id MuiTypography-h5-id" > - #9 + <div + class="OrderDetailsTitle-container-id" + > + #9 + <div + class="OrderDetailsTitle-statusContainer-id" + > + <div + class="StatusChip-root-id StatusChip-neutral-id" + > + <div + class="MuiTypography-root-id StatusChip-label-id StatusChip-neutralLabel-id MuiTypography-body1-id" + > + Partially fulfilled + </div> + </div> + </div> + </div> </div> <div class="ExtendedPageHeader-action-id" @@ -114786,7 +115328,24 @@ exports[`Storyshots Views / Orders / Order details rejected payment 1`] = ` <div class="MuiTypography-root-id PageHeader-title-id MuiTypography-h5-id" > - #9 + <div + class="OrderDetailsTitle-container-id" + > + #9 + <div + class="OrderDetailsTitle-statusContainer-id" + > + <div + class="StatusChip-root-id StatusChip-neutral-id" + > + <div + class="MuiTypography-root-id StatusChip-label-id StatusChip-neutralLabel-id MuiTypography-body1-id" + > + Partially fulfilled + </div> + </div> + </div> + </div> </div> <div class="ExtendedPageHeader-action-id" @@ -116454,7 +117013,24 @@ exports[`Storyshots Views / Orders / Order details unfulfilled 1`] = ` <div class="MuiTypography-root-id PageHeader-title-id MuiTypography-h5-id" > - #9 + <div + class="OrderDetailsTitle-container-id" + > + #9 + <div + class="OrderDetailsTitle-statusContainer-id" + > + <div + class="StatusChip-root-id StatusChip-error-id" + > + <div + class="MuiTypography-root-id StatusChip-label-id StatusChip-errorLabel-id MuiTypography-body1-id" + > + Unfulfilled + </div> + </div> + </div> + </div> </div> <div class="ExtendedPageHeader-action-id" @@ -120217,6 +120793,33 @@ exports[`Storyshots Views / Orders / Order list default 1`] = ` <div class="PageHeader-root-id" > + <div + class="OrderListPage-settings-id" + > + <button + aria-haspopup="true" + aria-label="More" + class="MuiButtonBase-root-id MuiIconButton-root-id CardMenu-iconButton-id MuiIconButton-colorPrimary-id" + tabindex="0" + type="button" + > + <span + class="MuiIconButton-label-id" + > + <svg + aria-hidden="true" + class="MuiSvgIcon-root-id" + focusable="false" + role="presentation" + viewBox="0 0 24 24" + > + <path + d="M12 8c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z" + /> + </svg> + </span> + </button> + </div> <button class="MuiButtonBase-root-id MuiButton-root-id MuiButton-contained-id MuiButton-containedPrimary-id" tabindex="0" @@ -121538,6 +122141,33 @@ exports[`Storyshots Views / Orders / Order list loading 1`] = ` <div class="PageHeader-root-id" > + <div + class="OrderListPage-settings-id" + > + <button + aria-haspopup="true" + aria-label="More" + class="MuiButtonBase-root-id MuiIconButton-root-id CardMenu-iconButton-id MuiIconButton-colorPrimary-id" + tabindex="0" + type="button" + > + <span + class="MuiIconButton-label-id" + > + <svg + aria-hidden="true" + class="MuiSvgIcon-root-id" + focusable="false" + role="presentation" + viewBox="0 0 24 24" + > + <path + d="M12 8c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z" + /> + </svg> + </span> + </button> + </div> <button class="MuiButtonBase-root-id MuiButton-root-id MuiButton-contained-id MuiButton-containedPrimary-id" tabindex="0" @@ -121969,6 +122599,33 @@ exports[`Storyshots Views / Orders / Order list when no data 1`] = ` <div class="PageHeader-root-id" > + <div + class="OrderListPage-settings-id" + > + <button + aria-haspopup="true" + aria-label="More" + class="MuiButtonBase-root-id MuiIconButton-root-id CardMenu-iconButton-id MuiIconButton-colorPrimary-id" + tabindex="0" + type="button" + > + <span + class="MuiIconButton-label-id" + > + <svg + aria-hidden="true" + class="MuiSvgIcon-root-id" + focusable="false" + role="presentation" + viewBox="0 0 24 24" + > + <path + d="M12 8c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z" + /> + </svg> + </span> + </button> + </div> <button class="MuiButtonBase-root-id MuiButton-root-id MuiButton-contained-id MuiButton-containedPrimary-id" tabindex="0" @@ -122319,6 +122976,240 @@ exports[`Storyshots Views / Orders / Order list when no data 1`] = ` </div> `; +exports[`Storyshots Views / Orders / Order settings default 1`] = ` +<div + style="padding:24px" +> + <form> + <div + class="Container-root-id" + > + <div + class="ExtendedPageHeader-root-id ExtendedPageHeader-block-id" + > + <div + class="MuiTypography-root-id PageHeader-title-id MuiTypography-h5-id" + > + Order settings + </div> + <div + class="ExtendedPageHeader-action-id" + > + <div + class="PageHeader-root-id" + /> + </div> + </div> + <div + class="Grid-root-id Grid-inverted-id" + > + <div> + <div + class="MuiTypography-root-id MuiTypography-body1-id" + > + General Settings + </div> + </div> + <div + class="MuiPaper-root-id MuiPaper-elevation0-id MuiCard-root-id MuiPaper-rounded-id" + data-test="orderSettings" + > + <div + class="CardTitle-root-id" + > + <span + class="MuiTypography-root-id CardTitle-title-id MuiTypography-h5-id" + > + Settings + </span> + <div + class="CardTitle-toolbar-id" + /> + </div> + <div + class="CardTitle-children-id" + /> + <hr + class="CardTitle-hr-id" + /> + <div + class="MuiCardContent-root-id" + > + <label + class="MuiFormControlLabel-root-id" + data-test="automaticallyConfirmAllNewOrdersCheckbox" + > + <span + aria-disabled="false" + class="MuiButtonBase-root-id MuiIconButton-root-id PrivateSwitchBase-root-id MuiCheckbox-root-id MuiCheckbox-colorPrimary-id PrivateSwitchBase-checked-id MuiCheckbox-checked-id MuiIconButton-colorPrimary-id" + > + <span + class="MuiIconButton-label-id" + > + <input + checked="" + class="PrivateSwitchBase-input-id" + data-indeterminate="false" + name="automaticallyConfirmAllNewOrders" + type="checkbox" + /> + <svg + aria-hidden="true" + class="MuiSvgIcon-root-id" + focusable="false" + role="presentation" + viewBox="0 0 24 24" + > + <rect + fill="currentColor" + height="14" + width="14" + x="5" + y="5" + /> + <path + clip-rule="evenodd" + d="M 16.7527 9.33783 L 10.86618 15.7595 L 8 12.32006 L 8.76822 11.67988 L 10.90204 14.24046 L 16.0155 8.66211 L 16.7527 9.33783 Z" + fill="white" + fill-rule="evenodd" + /> + </svg> + </span> + </span> + <span + class="MuiTypography-root-id MuiFormControlLabel-label-id MuiTypography-body1-id" + > + Automatically confirm all orders + <div + class="MuiTypography-root-id MuiTypography-caption-id" + > + All orders will be automatically confirmed and all payments will be captured. + </div> + </span> + </label> + </div> + </div> + </div> + </div> + </form> +</div> +`; + +exports[`Storyshots Views / Orders / Order settings loading 1`] = ` +<div + style="padding:24px" +> + <form> + <div + class="Container-root-id" + > + <div + class="ExtendedPageHeader-root-id ExtendedPageHeader-block-id" + > + <div + class="MuiTypography-root-id PageHeader-title-id MuiTypography-h5-id" + > + Order settings + </div> + <div + class="ExtendedPageHeader-action-id" + > + <div + class="PageHeader-root-id" + /> + </div> + </div> + <div + class="Grid-root-id Grid-inverted-id" + > + <div> + <div + class="MuiTypography-root-id MuiTypography-body1-id" + > + General Settings + </div> + </div> + <div + class="MuiPaper-root-id MuiPaper-elevation0-id MuiCard-root-id MuiPaper-rounded-id" + data-test="orderSettings" + > + <div + class="CardTitle-root-id" + > + <span + class="MuiTypography-root-id CardTitle-title-id MuiTypography-h5-id" + > + Settings + </span> + <div + class="CardTitle-toolbar-id" + /> + </div> + <div + class="CardTitle-children-id" + /> + <hr + class="CardTitle-hr-id" + /> + <div + class="MuiCardContent-root-id" + > + <label + class="MuiFormControlLabel-root-id MuiFormControlLabel-disabled-id" + data-test="automaticallyConfirmAllNewOrdersCheckbox" + > + <span + aria-disabled="true" + class="MuiButtonBase-root-id MuiIconButton-root-id PrivateSwitchBase-root-id MuiCheckbox-root-id MuiCheckbox-colorPrimary-id PrivateSwitchBase-disabled-id MuiCheckbox-disabled-id MuiIconButton-colorPrimary-id MuiIconButton-disabled-id MuiButtonBase-disabled-id" + tabindex="-1" + > + <span + class="MuiIconButton-label-id" + > + <input + class="PrivateSwitchBase-input-id" + data-indeterminate="false" + disabled="" + name="automaticallyConfirmAllNewOrders" + type="checkbox" + /> + <svg + aria-hidden="true" + class="MuiSvgIcon-root-id" + focusable="false" + role="presentation" + viewBox="0 0 24 24" + > + <rect + fill="none" + height="14" + stroke="currentColor" + width="14" + x="5" + y="5" + /> + </svg> + </span> + </span> + <span + class="MuiTypography-root-id MuiFormControlLabel-label-id MuiFormControlLabel-disabled-id MuiTypography-body1-id" + > + Automatically confirm all orders + <div + class="MuiTypography-root-id MuiTypography-caption-id" + > + All orders will be automatically confirmed and all payments will be captured. + </div> + </span> + </label> + </div> + </div> + </div> + </div> + </form> +</div> +`; + exports[`Storyshots Views / Page types / Create page type default 1`] = ` <div style="padding:24px" diff --git a/src/storybook/stories/orders/OrderListPage.tsx b/src/storybook/stories/orders/OrderListPage.tsx index 4f4b3ab52..266b7aaba 100644 --- a/src/storybook/stories/orders/OrderListPage.tsx +++ b/src/storybook/stories/orders/OrderListPage.tsx @@ -37,6 +37,7 @@ const props: OrderListPageProps = { value: [OrderStatusFilter.CANCELED, OrderStatusFilter.FULFILLED] } }, + onSettingsOpen: () => undefined, orders, sort: { ...sortPageProps.sort, diff --git a/src/types/globalTypes.ts b/src/types/globalTypes.ts index 6c8982011..6e80d089a 100644 --- a/src/types/globalTypes.ts +++ b/src/types/globalTypes.ts @@ -592,6 +592,7 @@ export enum OrderErrorCode { } export enum OrderEventsEmailsEnum { + CONFIRMED = "CONFIRMED", DIGITAL_LINKS = "DIGITAL_LINKS", FULFILLMENT_CONFIRMATION = "FULFILLMENT_CONFIRMATION", ORDER_CANCEL = "ORDER_CANCEL", @@ -604,6 +605,7 @@ export enum OrderEventsEmailsEnum { export enum OrderEventsEnum { CANCELED = "CANCELED", + CONFIRMED = "CONFIRMED", DRAFT_ADDED_PRODUCTS = "DRAFT_ADDED_PRODUCTS", DRAFT_CREATED = "DRAFT_CREATED", DRAFT_REMOVED_PRODUCTS = "DRAFT_REMOVED_PRODUCTS", @@ -632,6 +634,10 @@ export enum OrderEventsEnum { UPDATED_ADDRESS = "UPDATED_ADDRESS", } +export enum OrderSettingsErrorCode { + INVALID = "INVALID", +} + export enum OrderSortField { CREATION_DATE = "CREATION_DATE", CUSTOMER = "CUSTOMER", @@ -645,6 +651,7 @@ export enum OrderStatus { DRAFT = "DRAFT", FULFILLED = "FULFILLED", PARTIALLY_FULFILLED = "PARTIALLY_FULFILLED", + UNCONFIRMED = "UNCONFIRMED", UNFULFILLED = "UNFULFILLED", } @@ -654,6 +661,7 @@ export enum OrderStatusFilter { PARTIALLY_FULFILLED = "PARTIALLY_FULFILLED", READY_TO_CAPTURE = "READY_TO_CAPTURE", READY_TO_FULFILL = "READY_TO_FULFILL", + UNCONFIRMED = "UNCONFIRMED", UNFULFILLED = "UNFULFILLED", } @@ -954,6 +962,7 @@ export enum WebhookEventTypeEnum { INVOICE_REQUESTED = "INVOICE_REQUESTED", INVOICE_SENT = "INVOICE_SENT", ORDER_CANCELLED = "ORDER_CANCELLED", + ORDER_CONFIRMED = "ORDER_CONFIRMED", ORDER_CREATED = "ORDER_CREATED", ORDER_FULFILLED = "ORDER_FULFILLED", ORDER_FULLY_PAID = "ORDER_FULLY_PAID", @@ -1342,6 +1351,10 @@ export interface OrderLineInput { quantity: number; } +export interface OrderSettingsUpdateInput { + automaticallyConfirmAllNewOrders: boolean; +} + export interface OrderSortingInput { direction: OrderDirection; field: OrderSortField; diff --git a/src/webhooks/components/WebhookEvents/WebhookEvents.tsx b/src/webhooks/components/WebhookEvents/WebhookEvents.tsx index 8f492dd78..bc41fe878 100644 --- a/src/webhooks/components/WebhookEvents/WebhookEvents.tsx +++ b/src/webhooks/components/WebhookEvents/WebhookEvents.tsx @@ -60,6 +60,10 @@ const WebhookEvents: React.FC<WebhookEventsProps> = ({ defaultMessage: "Order created", description: "event" }), + [WebhookEventTypeEnum.ORDER_CONFIRMED]: intl.formatMessage({ + defaultMessage: "Order confirmed", + description: "event" + }), [WebhookEventTypeEnum.ORDER_FULFILLED]: intl.formatMessage({ defaultMessage: "Order fulfilled", description: "event"