From aaa4c9d8dce6b51e929c59bbad599343f8dbbd9d Mon Sep 17 00:00:00 2001 From: Mirek Mencel Date: Fri, 5 Dec 2025 18:06:15 +0100 Subject: [PATCH 01/84] Add border to Order summary and remove bg --- src/orders/components/OrderSummary/OrderValue.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/orders/components/OrderSummary/OrderValue.tsx b/src/orders/components/OrderSummary/OrderValue.tsx index 4cc7cf4f0a1..8eb8cb12d64 100644 --- a/src/orders/components/OrderSummary/OrderValue.tsx +++ b/src/orders/components/OrderSummary/OrderValue.tsx @@ -32,11 +32,10 @@ export const OrderValue = ({ return ( From 7ce12ba3718af6767b32d51b93fa5e072e51c925 Mon Sep 17 00:00:00 2001 From: Mirek Mencel Date: Fri, 5 Dec 2025 18:28:06 +0100 Subject: [PATCH 02/84] Improve empty state for Payments summary card --- .../OrderSummary/PaymentsSummary.tsx | 10 ++---- .../PaymentsSummaryEmptyState.module.css | 26 ++++++++++++++++ .../PaymentsSummaryEmptyState.tsx | 31 +++++++++++++++++++ .../OrderSummary/PaymentsSummaryHeader.tsx | 10 +++--- 4 files changed, 66 insertions(+), 11 deletions(-) create mode 100644 src/orders/components/OrderSummary/PaymentsSummaryEmptyState.module.css create mode 100644 src/orders/components/OrderSummary/PaymentsSummaryEmptyState.tsx diff --git a/src/orders/components/OrderSummary/PaymentsSummary.tsx b/src/orders/components/OrderSummary/PaymentsSummary.tsx index 006adadae8d..1274d24df31 100644 --- a/src/orders/components/OrderSummary/PaymentsSummary.tsx +++ b/src/orders/components/OrderSummary/PaymentsSummary.tsx @@ -5,6 +5,7 @@ import { useIntl } from "react-intl"; import { OrderAuthorizeStatusBadge } from "./OrderAuthorizeStatusBadge"; import { OrderChargeStatusBadge } from "./OrderChargeStatusBadge"; import { OrderSummaryListItem } from "./OrderSummaryListItem"; +import { PaymentsSummaryEmptyState } from "./PaymentsSummaryEmptyState"; import { PaymentsSummaryHeader } from "./PaymentsSummaryHeader"; type Props = PropsWithBox<{ @@ -31,13 +32,8 @@ export const PaymentsSummary = ({ orderAmounts, order, hasNoPayment, ...props }: borderWidth={1} {...props} > - + + ); } diff --git a/src/orders/components/OrderSummary/PaymentsSummaryEmptyState.module.css b/src/orders/components/OrderSummary/PaymentsSummaryEmptyState.module.css new file mode 100644 index 00000000000..513aaa5996c --- /dev/null +++ b/src/orders/components/OrderSummary/PaymentsSummaryEmptyState.module.css @@ -0,0 +1,26 @@ +.emptyState { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 32px 16px; + gap: 0; +} + +.iconWrapper { + display: flex; + align-items: center; + justify-content: center; + width: 56px; + height: 56px; + border-radius: 50%; + background-color: var(--macaw-color-background-default2); +} + +.textContainer { + display: flex; + flex-direction: column; + align-items: center; + gap: 4px; + text-align: center; +} diff --git a/src/orders/components/OrderSummary/PaymentsSummaryEmptyState.tsx b/src/orders/components/OrderSummary/PaymentsSummaryEmptyState.tsx new file mode 100644 index 00000000000..4f80454f6a9 --- /dev/null +++ b/src/orders/components/OrderSummary/PaymentsSummaryEmptyState.tsx @@ -0,0 +1,31 @@ +import { Box, Text } from "@saleor/macaw-ui-next"; +import { CreditCard } from "lucide-react"; +import { useIntl } from "react-intl"; + +import styles from "./PaymentsSummaryEmptyState.module.css"; + +export const PaymentsSummaryEmptyState = () => { + const intl = useIntl(); + + return ( + + + + + + + {intl.formatMessage({ + defaultMessage: "No payment received", + id: "6Jgwpc", + })} + + + {intl.formatMessage({ + defaultMessage: "Mark as paid manually if the payment is confirmed", + id: "3Eyq0y", + })} + + + + ); +}; diff --git a/src/orders/components/OrderSummary/PaymentsSummaryHeader.tsx b/src/orders/components/OrderSummary/PaymentsSummaryHeader.tsx index 7b4902c3d1f..b0a88658242 100644 --- a/src/orders/components/OrderSummary/PaymentsSummaryHeader.tsx +++ b/src/orders/components/OrderSummary/PaymentsSummaryHeader.tsx @@ -4,7 +4,7 @@ import { useIntl } from "react-intl"; type Props = { order: OrderDetailsFragment; - description: string; + description?: string; }; export const PaymentsSummaryHeader = ({ description }: Props) => { @@ -19,9 +19,11 @@ export const PaymentsSummaryHeader = ({ description }: Props) => { id: "q7bXR4", })} - - {description} - + {description && ( + + {description} + + )} ); From 5c1b09d10908a93602a004f5d42f2daa90336191 Mon Sep 17 00:00:00 2001 From: Mirek Mencel Date: Fri, 5 Dec 2025 18:46:03 +0100 Subject: [PATCH 03/84] Match font size and add tabular nums to amounts --- .../components/OrderSummary/OrderSummaryListAmount.tsx | 7 ++++++- .../components/OrderSummary/OrderSummaryListItem.tsx | 6 +++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/orders/components/OrderSummary/OrderSummaryListAmount.tsx b/src/orders/components/OrderSummary/OrderSummaryListAmount.tsx index e3e2687477a..ccf1cc2e76a 100644 --- a/src/orders/components/OrderSummary/OrderSummaryListAmount.tsx +++ b/src/orders/components/OrderSummary/OrderSummaryListAmount.tsx @@ -1,4 +1,5 @@ import { Text, TextProps } from "@saleor/macaw-ui-next"; +import React from "react"; import { useIntl } from "react-intl"; type Props = Omit & { @@ -6,11 +7,15 @@ type Props = Omit & { showSign?: boolean; }; +const tabularNumsStyle: React.CSSProperties = { + fontVariantNumeric: "tabular-nums", +}; + export const OrderSummaryListAmount = ({ amount, showSign = false, ...props }: Props) => { const intl = useIntl(); return ( - + {intl.formatNumber(amount, { minimumFractionDigits: 2, signDisplay: showSign ? "exceptZero" : "auto", diff --git a/src/orders/components/OrderSummary/OrderSummaryListItem.tsx b/src/orders/components/OrderSummary/OrderSummaryListItem.tsx index 5cf407f0e8f..6b759963070 100644 --- a/src/orders/components/OrderSummary/OrderSummaryListItem.tsx +++ b/src/orders/components/OrderSummary/OrderSummaryListItem.tsx @@ -14,14 +14,14 @@ type Props = PropsWithBox<{ export const OrderSummaryListItem = ({ children, amount, showSign, currency, ...props }: Props) => { return ( - + {children} - + {currency} {" "} - + ); From 51ae25d58b353bc6208aa7fe7be7bdf01816d57b Mon Sep 17 00:00:00 2001 From: Mirek Mencel Date: Fri, 5 Dec 2025 18:49:34 +0100 Subject: [PATCH 04/84] Fix buttons stacking on each other --- .../OrderSummary/LegacyPaymentsApiButtons.tsx | 86 ++++++++++--------- 1 file changed, 44 insertions(+), 42 deletions(-) diff --git a/src/orders/components/OrderSummary/LegacyPaymentsApiButtons.tsx b/src/orders/components/OrderSummary/LegacyPaymentsApiButtons.tsx index b8ee281b768..0a974c3b9ca 100644 --- a/src/orders/components/OrderSummary/LegacyPaymentsApiButtons.tsx +++ b/src/orders/components/OrderSummary/LegacyPaymentsApiButtons.tsx @@ -27,49 +27,51 @@ export const LegacyPaymentsApiButtons = ({ }: Props) => { const intl = useIntl(); + const showButtons = + order?.status !== OrderStatus.CANCELED && (canCapture || canRefund || canVoid || canMarkAsPaid); + + if (!showButtons) { + return null; + } + return ( - - {order?.status !== OrderStatus.CANCELED && - (canCapture || canRefund || canVoid || canMarkAsPaid) && ( - <> - {canCapture && ( - - )} - {canRefund && ( - - )} - {canVoid && ( - - )} - {canMarkAsPaid && ( - - )} - - )} + + {canCapture && ( + + )} + {canRefund && ( + + )} + {canVoid && ( + + )} + {canMarkAsPaid && ( + + )} ); }; From 7c2b4d6ad9b405389d275dd8e2c3ca8bafa209bf Mon Sep 17 00:00:00 2001 From: Mirek Mencel Date: Fri, 5 Dec 2025 18:51:27 +0100 Subject: [PATCH 05/84] Extract messages --- locale/defaultMessages.json | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/locale/defaultMessages.json b/locale/defaultMessages.json index e4c59755964..6d645ebd4df 100644 --- a/locale/defaultMessages.json +++ b/locale/defaultMessages.json @@ -834,6 +834,9 @@ "3DGvA/": { "string": "Remember this will also unpin all products assigned to this category, making them unavailable in storefront." }, + "3Eyq0y": { + "string": "Mark as paid manually if the payment is confirmed" + }, "3PVGWj": { "string": "Filter preset" }, @@ -1314,6 +1317,9 @@ "6J1m2c": { "string": "Create a Model" }, + "6Jgwpc": { + "string": "No payment received" + }, "6QjMei": { "string": "Preorder end time needs to be set in the future" }, @@ -2694,9 +2700,6 @@ "context": "postal codes, header", "string": "Postal codes" }, - "Fcxl/G": { - "string": "This order has no payment yet" - }, "FemBUF": { "context": "header", "string": "Translations to {language}" From d1f52636369e6e4ad786a0a6113b2ff51e5a0a77 Mon Sep 17 00:00:00 2001 From: Mirek Mencel Date: Fri, 5 Dec 2025 18:52:35 +0100 Subject: [PATCH 06/84] Add changeset --- .changeset/deep-ducks-sniff.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/deep-ducks-sniff.md diff --git a/.changeset/deep-ducks-sniff.md b/.changeset/deep-ducks-sniff.md new file mode 100644 index 00000000000..642e6dffc9c --- /dev/null +++ b/.changeset/deep-ducks-sniff.md @@ -0,0 +1,5 @@ +--- +"saleor-dashboard": patch +--- + +Improve Order summary section From 3390702932cd8d2d6ceab765dc04235b6b5ff283 Mon Sep 17 00:00:00 2001 From: Mirek Mencel Date: Fri, 5 Dec 2025 18:53:06 +0100 Subject: [PATCH 07/84] Fix linting warning --- src/orders/components/OrderSummary/OrderSummaryListAmount.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/orders/components/OrderSummary/OrderSummaryListAmount.tsx b/src/orders/components/OrderSummary/OrderSummaryListAmount.tsx index ccf1cc2e76a..64a754526ea 100644 --- a/src/orders/components/OrderSummary/OrderSummaryListAmount.tsx +++ b/src/orders/components/OrderSummary/OrderSummaryListAmount.tsx @@ -1,5 +1,5 @@ import { Text, TextProps } from "@saleor/macaw-ui-next"; -import React from "react"; +import { CSSProperties } from "react"; import { useIntl } from "react-intl"; type Props = Omit & { @@ -7,7 +7,7 @@ type Props = Omit & { showSign?: boolean; }; -const tabularNumsStyle: React.CSSProperties = { +const tabularNumsStyle: CSSProperties = { fontVariantNumeric: "tabular-nums", }; From 5ba1c1577457022e7efbc88fa4b287a4a8f99f95 Mon Sep 17 00:00:00 2001 From: Mirek Mencel Date: Mon, 8 Dec 2025 14:11:53 +0100 Subject: [PATCH 08/84] Fix "No applicable shipping methods" bug --- locale/defaultMessages.json | 4 ++++ .../OrderDraftDetailsSummary.tsx | 21 +++++++++++++++++-- .../OrderDraftDetailsSummary/messages.ts | 5 +++++ 3 files changed, 28 insertions(+), 2 deletions(-) diff --git a/locale/defaultMessages.json b/locale/defaultMessages.json index e4c59755964..c839d3540c8 100644 --- a/locale/defaultMessages.json +++ b/locale/defaultMessages.json @@ -6576,6 +6576,10 @@ "context": "vat included in order price", "string": "VAT included" }, + "dJv+cZ": { + "context": "shown when shipping method is selected but no other options exist", + "string": "no alternatives available" + }, "dLIeRH": { "context": "charge status none", "string": "None" diff --git a/src/orders/components/OrderDraftDetailsSummary/OrderDraftDetailsSummary.tsx b/src/orders/components/OrderDraftDetailsSummary/OrderDraftDetailsSummary.tsx index 14a83701688..4c7c99a29d4 100644 --- a/src/orders/components/OrderDraftDetailsSummary/OrderDraftDetailsSummary.tsx +++ b/src/orders/components/OrderDraftDetailsSummary/OrderDraftDetailsSummary.tsx @@ -121,6 +121,21 @@ const OrderDraftDetailsSummary = (props: OrderDraftDetailsSummaryProps) => { }; const getShippingMethodComponent = () => { if (hasChosenShippingMethod) { + // Shipping method is selected but no alternatives available - show as plain text + if (!hasShippingMethods) { + return ( +
+ + {shippingMethodName} + + + + {`(${intl.formatMessage(messages.noAlternativeShippingMethods)})`} + +
+ ); + } + return {`${shippingMethodName}`}; } @@ -193,9 +208,11 @@ const OrderDraftDetailsSummary = (props: OrderDraftDetailsSummaryProps) => { - {hasShippingMethods && getShippingMethodComponent()} + {(hasShippingMethods || hasChosenShippingMethod) && getShippingMethodComponent()} - {!hasShippingMethods && intl.formatMessage(messages.noShippingCarriers)} + {!hasShippingMethods && + !hasChosenShippingMethod && + intl.formatMessage(messages.noShippingCarriers)} {formErrors.shipping && ( diff --git a/src/orders/components/OrderDraftDetailsSummary/messages.ts b/src/orders/components/OrderDraftDetailsSummary/messages.ts index 89b7bc07dba..0aaa018a85c 100644 --- a/src/orders/components/OrderDraftDetailsSummary/messages.ts +++ b/src/orders/components/OrderDraftDetailsSummary/messages.ts @@ -31,6 +31,11 @@ export const messages = defineMessages({ defaultMessage: "No applicable shipping carriers", description: "no shipping carriers title", }, + noAlternativeShippingMethods: { + id: "8EDRSz", + defaultMessage: "no alternatives available", + description: "shown when shipping method is selected but no other options exist", + }, total: { id: "S/yAtJ", defaultMessage: "Total", From 2f242bf28501c486992a55942a7a33e108a9f204 Mon Sep 17 00:00:00 2001 From: Mirek Mencel Date: Mon, 8 Dec 2025 15:39:15 +0100 Subject: [PATCH 09/84] Fix Edit Shipping Options modal to show unavailable method so user is not confused when the name matches --- .../OrderShippingMethodEditDialog.tsx | 175 ++++++++++++------ .../OrderDetails/OrderDraftDetails/index.tsx | 2 + .../OrderUnconfirmedDetails/index.tsx | 2 + 3 files changed, 118 insertions(+), 61 deletions(-) diff --git a/src/orders/components/OrderShippingMethodEditDialog/OrderShippingMethodEditDialog.tsx b/src/orders/components/OrderShippingMethodEditDialog/OrderShippingMethodEditDialog.tsx index 1d6456966b3..8c6f887f6bb 100644 --- a/src/orders/components/OrderShippingMethodEditDialog/OrderShippingMethodEditDialog.tsx +++ b/src/orders/components/OrderShippingMethodEditDialog/OrderShippingMethodEditDialog.tsx @@ -23,6 +23,8 @@ interface OrderShippingMethodEditDialogProps { errors: OrderErrorFragment[]; open: boolean; shippingMethod: string; + shippingMethodName?: string; + shippingPrice?: OrderDetailsFragment["shippingPrice"]; shippingMethods?: OrderDetailsFragment["shippingMethods"]; onClose: () => any; onSubmit?: (data: FormData) => any; @@ -34,6 +36,8 @@ const OrderShippingMethodEditDialog = (props: OrderShippingMethodEditDialogProps errors: apiErrors, open, shippingMethod, + shippingMethodName, + shippingPrice, shippingMethods, onClose, onSubmit, @@ -43,7 +47,7 @@ const OrderShippingMethodEditDialog = (props: OrderShippingMethodEditDialogProps const formFields = ["shippingMethod"]; const formErrors = getFormErrors(formFields, errors); const nonFieldErrors = errors.filter(err => !formFields.includes(err.field)); - const choices = shippingMethods + const availableChoices = shippingMethods ? shippingMethods .map(s => ({ label: ( @@ -68,73 +72,122 @@ const OrderShippingMethodEditDialog = (props: OrderShippingMethodEditDialogProps })) .sort((x, y) => (x.disabled === y.disabled ? 0 : x.disabled ? 1 : -1)) : []; + + const currentMethodInChoices = availableChoices.some(c => c.value === shippingMethod); + + const unavailableMethodOption = + shippingMethod && !currentMethodInChoices + ? [ + { + label: ( + + + + {shippingMethodName || ( + + )} + + + + {shippingPrice && ( + + + + + + )} + + + + + + ), + disabled: true, + value: shippingMethod, + }, + ] + : []; + + const choices = [...unavailableMethodOption, ...availableChoices]; + const initialForm: FormData = { - shippingMethod, + shippingMethod: currentMethodInChoices ? shippingMethod : "", }; return ( -
- {({ change, data, submit }) => ( - <> - - - + {({ change, data, submit }) => ( + <> + + + + + + { - const value = target.value; - const isDisabled = choices.find(({ value }) => value === value)?.disabled; - - if (isDisabled) { - return; - } - - change({ - target: { - name: "shippingMethod", - value: typeof value === "string" ? value : (value as Option)?.value, - }, - }); - }} - /> - {nonFieldErrors.length > 0 && ( - <> - - {nonFieldErrors.map((err, index) => ( - - {getOrderErrorMessage(err, intl)} - - ))} - - )} + {nonFieldErrors.length > 0 && ( + <> + + {nonFieldErrors.map((err, index) => ( + + {getOrderErrorMessage(err, intl)} + + ))} + + )} - - - - - - - - - )} - + + + + + + +
+ + )} + + )}
); }; diff --git a/src/orders/views/OrderDetails/OrderDraftDetails/index.tsx b/src/orders/views/OrderDetails/OrderDraftDetails/index.tsx index de5f28c4a75..ddcd9a51680 100644 --- a/src/orders/views/OrderDetails/OrderDraftDetails/index.tsx +++ b/src/orders/views/OrderDetails/OrderDraftDetails/index.tsx @@ -259,6 +259,8 @@ export const OrderDraftDetails = ({ errors={orderShippingMethodUpdate.opts.data?.orderUpdateShipping.errors || []} open={params.action === "edit-shipping"} shippingMethod={order?.shippingMethod?.id} + shippingMethodName={order?.shippingMethodName} + shippingPrice={order?.shippingPrice} shippingMethods={order?.shippingMethods} onClose={closeModal} onSubmit={variables => diff --git a/src/orders/views/OrderDetails/OrderUnconfirmedDetails/index.tsx b/src/orders/views/OrderDetails/OrderUnconfirmedDetails/index.tsx index 0357098382e..c5fab507284 100644 --- a/src/orders/views/OrderDetails/OrderUnconfirmedDetails/index.tsx +++ b/src/orders/views/OrderDetails/OrderUnconfirmedDetails/index.tsx @@ -316,6 +316,8 @@ export const OrderUnconfirmedDetails = ({ errors={orderShippingMethodUpdate.opts.data?.orderUpdateShipping.errors || []} open={params.action === "edit-shipping"} shippingMethod={order?.shippingMethod?.id} + shippingMethodName={order?.shippingMethodName} + shippingPrice={order?.shippingPrice} shippingMethods={order?.shippingMethods} onClose={closeModal} onSubmit={variables => From 7dcf708c98dc7bbaf492044f2bde85f88bb988a0 Mon Sep 17 00:00:00 2001 From: Mirek Mencel Date: Mon, 8 Dec 2025 16:35:09 +0100 Subject: [PATCH 10/84] Merge the old UNCONFIRMED and DRAFT order summary into the new design "Order value" --- locale/defaultMessages.json | 64 ++- .../OrderDetailsPage/OrderDetailsPage.tsx | 36 +- .../OrderDraftDetails/OrderDraftDetails.tsx | 5 +- .../OrderDraftDetailsSummary.tsx | 18 +- .../OrderDraftDetailsSummary/messages.ts | 6 +- .../OrderDraftPage/OrderDraftPage.tsx | 20 + .../OrderSummary/OrderSummary.test.tsx | 4 +- .../components/OrderSummary/OrderSummary.tsx | 111 +++- .../OrderSummary/OrderSummaryListItem.tsx | 16 +- .../components/OrderSummary/OrderValue.tsx | 516 +++++++++++++++--- 10 files changed, 664 insertions(+), 132 deletions(-) diff --git a/locale/defaultMessages.json b/locale/defaultMessages.json index c839d3540c8..3946c6861c7 100644 --- a/locale/defaultMessages.json +++ b/locale/defaultMessages.json @@ -519,6 +519,10 @@ "context": "dialog header", "string": "Change customer shipping address" }, + "12K83x": { + "context": "shipping method option when name unknown", + "string": "Current method" + }, "15PiOX": { "context": "button title", "string": "Unassign" @@ -1067,6 +1071,10 @@ "context": "account information, header", "string": "Account Information" }, + "4xZFP4": { + "context": "tooltip for shipping amount", + "string": "Shipping cost" + }, "4y3Smi": { "string": "Removing permissions may cause extension to break." }, @@ -1561,6 +1569,10 @@ "context": "checkbox label", "string": "Send fulfillment email to customer" }, + "8EDRSz": { + "context": "tooltip shown when shipping method is selected but no other options exist", + "string": "No alternative shipping methods available" + }, "8EGagh": { "context": "search box label", "string": "Filter Countries" @@ -2873,6 +2885,10 @@ "context": "dialog header", "string": "Select destination channel:" }, + "GSC7Rw": { + "context": "tooltip for taxes amount", + "string": "Tax amount" + }, "GTCg9O": { "string": "You must add at least one voucher code" }, @@ -2938,6 +2954,10 @@ "context": "add metadata field,button", "string": "Add Field" }, + "GiJ1QZ": { + "context": "tooltip for total amount", + "string": "Order total" + }, "GiJm1v": { "context": "dialog content", "string": "{counter,plural,one{Are you sure you want to unassign this category?} other{Are you sure you want to unassign {displayQuantity} categories?}}" @@ -3273,6 +3293,10 @@ "context": "header", "string": "Edit Media" }, + "IirEpN": { + "context": "tooltip for taxes when showing gross prices", + "string": "Gross prices" + }, "Ila7WO": { "context": "change warehouse dialog description", "string": "Choose warehouse from which you want to fulfill {productName}" @@ -3310,6 +3334,10 @@ "context": "bulk disable label", "string": "Deactivate" }, + "IzrVyw": { + "context": "tooltip for taxes when showing net prices", + "string": "Net prices" + }, "J/QqOI": { "string": "This value already exists within this attribute" }, @@ -3491,6 +3519,10 @@ "context": "refund button", "string": "Refund" }, + "K/a8rS": { + "context": "tooltip for subtotal amount", + "string": "Sum of all line items" + }, "K/gnGg": { "string": "If you want to disable this User please uncheck the box below." }, @@ -4290,6 +4322,10 @@ "context": "back button", "string": "Back" }, + "PRlD0A": { + "context": "shipping label", + "string": "Shipping" + }, "PTW56s": { "context": "alert", "string": "Channel limit reached" @@ -4747,7 +4783,7 @@ "string": "Conditions" }, "SB//YQ": { - "string": "Order summary" + "string": "Summary" }, "SBb6Ej": { "context": "select a warehouse to fulfill product from", @@ -5524,6 +5560,10 @@ "context": "successfully created gift card alert title", "string": "Successfully created gift card" }, + "X1NCdA": { + "context": "shipping method not available label", + "string": "(not available)" + }, "X6PF8z": { "context": "entity (product, collection, shipping method) name", "string": "Name" @@ -5998,6 +6038,10 @@ "context": "note input subtitle", "string": "Why was this gift card issued. This note will not be shown to the customer. Note will be stored in gift card history" }, + "Zvjkx8": { + "context": "tooltip for discount amount", + "string": "Discount amount" + }, "Zvo5iu": { "string": "API reference" }, @@ -6576,10 +6620,6 @@ "context": "vat included in order price", "string": "VAT included" }, - "dJv+cZ": { - "context": "shown when shipping method is selected but no other options exist", - "string": "no alternatives available" - }, "dLIeRH": { "context": "charge status none", "string": "None" @@ -7967,9 +8007,6 @@ "mmcHeH": { "string": "Discount Value" }, - "mpkBZc": { - "string": "Shipping {carrierName}" - }, "mr9jbO": { "string": "Preferred Language" }, @@ -8794,6 +8831,10 @@ "context": "limit voucher", "string": "Limit of Uses" }, + "s6YKKO": { + "context": "set shipping link when no shipping method selected", + "string": "Set shipping" + }, "s6lW8R": { "context": "option label", "string": "Change address" @@ -9426,12 +9467,13 @@ "context": "success notifier message", "string": "Saved draft" }, - "vs0xiH": { - "string": "Discount {discountName}" - }, "vuKrlW": { "string": "Stock" }, + "vuNPLt": { + "context": "tooltip for gift card amount", + "string": "Gift card amount used" + }, "vwA9Fq": { "context": "notification", "string": "Selected models were deleted." diff --git a/src/orders/components/OrderDetailsPage/OrderDetailsPage.tsx b/src/orders/components/OrderDetailsPage/OrderDetailsPage.tsx index 1cc276d8aab..8025b84c89b 100644 --- a/src/orders/components/OrderDetailsPage/OrderDetailsPage.tsx +++ b/src/orders/components/OrderDetailsPage/OrderDetailsPage.tsx @@ -28,9 +28,11 @@ import { defaultGraphiQLQuery } from "@dashboard/orders/queries"; import { rippleOrderMetadata } from "@dashboard/orders/ripples/orderMetadata"; import { orderShouldUseTransactions } from "@dashboard/orders/types"; import { orderListUrl } from "@dashboard/orders/urls"; +import { OrderDiscountContext } from "@dashboard/products/components/OrderDiscountProviders/OrderDiscountProvider"; import { Ripple } from "@dashboard/ripples/components/Ripple"; import { Box, Button, Divider } from "@saleor/macaw-ui-next"; import { Code } from "lucide-react"; +import { useContext } from "react"; import { useIntl } from "react-intl"; import { getMutationErrors, maybe } from "../../../misc"; @@ -136,6 +138,7 @@ const OrderDetailsPage = (props: OrderDetailsPageProps) => { } = props; const navigate = useNavigator(); const intl = useIntl(); + const orderDiscountContext = useContext(OrderDiscountContext); const isOrderUnconfirmed = order?.status === OrderStatus.UNCONFIRMED; const canCancel = order?.status !== OrderStatus.CANCELED; const canEditAddresses = order?.status !== OrderStatus.CANCELED; @@ -250,6 +253,7 @@ const OrderDetailsPage = (props: OrderDetailsPageProps) => { onOrderLineChange={onOrderLineChange} onOrderLineRemove={onOrderLineRemove} onShippingMethodEdit={onShippingMethodEdit} + hideSummary /> @@ -269,7 +273,7 @@ const OrderDetailsPage = (props: OrderDetailsPageProps) => { /> ))} - {order && ( + {order && !isOrderUnconfirmed && ( <> { )} + {order && isOrderUnconfirmed && orderDiscountContext && ( + <> + + + + {orderShouldUseTransactions(order) && ( + + )} + + )} + void; onShippingMethodEdit: () => void; onOrderLineShowMetadata: (id: string) => void; + /** Hide the summary section (when using OrderSummary component instead) */ + hideSummary?: boolean; } const OrderDraftDetails = ({ @@ -39,6 +41,7 @@ const OrderDraftDetails = ({ onOrderLineRemove, onShippingMethodEdit, onOrderLineShowMetadata, + hideSummary = false, }: OrderDraftDetailsProps) => { const intl = useIntl(); const isChannelActive = order?.channel.isActive; @@ -70,7 +73,7 @@ const OrderDraftDetails = ({ onOrderLineRemove={onOrderLineRemove} onOrderLineShowMetadata={onOrderLineShowMetadata} /> - {maybe(() => order.lines.length) !== 0 && ( + {!hideSummary && maybe(() => order.lines.length) !== 0 && ( {(orderDiscountProps: OrderDiscountContextConsumerProps) => ( diff --git a/src/orders/components/OrderDraftDetailsSummary/OrderDraftDetailsSummary.tsx b/src/orders/components/OrderDraftDetailsSummary/OrderDraftDetailsSummary.tsx index 4c7c99a29d4..9614a8443db 100644 --- a/src/orders/components/OrderDraftDetailsSummary/OrderDraftDetailsSummary.tsx +++ b/src/orders/components/OrderDraftDetailsSummary/OrderDraftDetailsSummary.tsx @@ -121,18 +121,16 @@ const OrderDraftDetailsSummary = (props: OrderDraftDetailsSummaryProps) => { }; const getShippingMethodComponent = () => { if (hasChosenShippingMethod) { - // Shipping method is selected but no alternatives available - show as plain text + // Shipping method is selected but no alternatives available - show as plain text with tooltip if (!hasShippingMethods) { return ( -
- - {shippingMethodName} - - - - {`(${intl.formatMessage(messages.noAlternativeShippingMethods)})`} - -
+ + {shippingMethodName} + ); } diff --git a/src/orders/components/OrderDraftDetailsSummary/messages.ts b/src/orders/components/OrderDraftDetailsSummary/messages.ts index 0aaa018a85c..b195c069684 100644 --- a/src/orders/components/OrderDraftDetailsSummary/messages.ts +++ b/src/orders/components/OrderDraftDetailsSummary/messages.ts @@ -32,9 +32,9 @@ export const messages = defineMessages({ description: "no shipping carriers title", }, noAlternativeShippingMethods: { - id: "8EDRSz", - defaultMessage: "no alternatives available", - description: "shown when shipping method is selected but no other options exist", + id: "gCZYcl", + defaultMessage: "No alternative shipping methods available", + description: "tooltip shown when shipping method is selected but no other options exist", }, total: { id: "S/yAtJ", diff --git a/src/orders/components/OrderDraftPage/OrderDraftPage.tsx b/src/orders/components/OrderDraftPage/OrderDraftPage.tsx index b5e1238cb0b..256199f318e 100644 --- a/src/orders/components/OrderDraftPage/OrderDraftPage.tsx +++ b/src/orders/components/OrderDraftPage/OrderDraftPage.tsx @@ -23,13 +23,16 @@ import { SubmitPromise } from "@dashboard/hooks/useForm"; import useNavigator from "@dashboard/hooks/useNavigator"; import OrderChannelSectionCard from "@dashboard/orders/components/OrderChannelSectionCard"; import { orderDraftListUrl } from "@dashboard/orders/urls"; +import { OrderDiscountContext } from "@dashboard/products/components/OrderDiscountProviders/OrderDiscountProvider"; import { FetchMoreProps, RelayToFlat } from "@dashboard/types"; import { Box, Divider, Skeleton, Text } from "@saleor/macaw-ui-next"; +import { useContext } from "react"; import { useIntl } from "react-intl"; import OrderCustomer, { CustomerEditData } from "../OrderCustomer"; import OrderDraftDetails from "../OrderDraftDetails/OrderDraftDetails"; import OrderHistory, { FormData as HistoryFormData } from "../OrderHistory"; +import { OrderSummary } from "../OrderSummary/OrderSummary"; import OrderDraftAlert from "./OrderDraftAlert"; interface OrderDraftPageProps extends FetchMoreProps { @@ -90,6 +93,7 @@ const OrderDraftPage = (props: OrderDraftPageProps) => { } = props; const navigate = useNavigator(); const intl = useIntl(); + const orderDiscountContext = useContext(OrderDiscountContext); const backLinkUrl = useBackLinkWithState({ path: draftOrderListUrl, }); @@ -149,7 +153,23 @@ const OrderDraftPage = (props: OrderDraftPageProps) => { onOrderLineRemove={onOrderLineRemove} onShippingMethodEdit={onShippingMethodEdit} onOrderLineShowMetadata={onOrderLineShowMetadata} + hideSummary /> + {order && orderDiscountContext && ( + <> + { + // Draft orders cannot be marked as paid + }} + isEditable + onShippingMethodEdit={onShippingMethodEdit} + errors={errors} + {...orderDiscountContext} + /> + + + )} { describe("Basic Rendering", () => { - it("should render order summary title", () => { + it("should render summary title", () => { // Arrange const mockOrder = orderFixture("test-id"); const onMarkAsPaid = jest.fn(); @@ -84,7 +84,7 @@ describe("OrderSummary", () => { ); // Assert - expect(screen.getByText("Order summary")).toBeInTheDocument(); + expect(screen.getByText("Summary")).toBeInTheDocument(); }); }); diff --git a/src/orders/components/OrderSummary/OrderSummary.tsx b/src/orders/components/OrderSummary/OrderSummary.tsx index 46f1bd625e2..c8ba4b9a307 100644 --- a/src/orders/components/OrderSummary/OrderSummary.tsx +++ b/src/orders/components/OrderSummary/OrderSummary.tsx @@ -1,5 +1,6 @@ -import { OrderDetailsFragment } from "@dashboard/graphql"; +import { OrderDetailsFragment, OrderErrorFragment } from "@dashboard/graphql"; import { OrderDetailsViewModel } from "@dashboard/orders/utils/OrderDetailsViewModel"; +import { OrderDiscountContextConsumerProps } from "@dashboard/products/components/OrderDiscountProviders/OrderDiscountProvider"; import { Box, PropsWithBox, Text } from "@saleor/macaw-ui-next"; import { useIntl } from "react-intl"; @@ -8,26 +9,35 @@ import { OrderValue } from "./OrderValue"; import { PaymentsSummary } from "./PaymentsSummary"; import { TransactionsApiButtons } from "./TransactionsApiButtons"; -type OrderSummaryWithLegacyApi = { - useLegacyPaymentsApi: true; - onLegacyPaymentsApiCapture: () => any; - onLegacyPaymentsApiRefund: () => any; - onLegacyPaymentsApiVoid: () => any; -}; +type EditableOrderSummary = { + isEditable: true; + onShippingMethodEdit: () => void; + errors?: OrderErrorFragment[]; +} & OrderDiscountContextConsumerProps; -type OrderSummaryWithoutLegacyApi = { - useLegacyPaymentsApi?: false; +type ReadOnlyOrderSummary = { + isEditable?: false; }; type Props = PropsWithBox< { order: OrderDetailsFragment; onMarkAsPaid: () => any; - } & (OrderSummaryWithLegacyApi | OrderSummaryWithoutLegacyApi) + useLegacyPaymentsApi?: boolean; + onLegacyPaymentsApiCapture?: () => any; + onLegacyPaymentsApiRefund?: () => any; + onLegacyPaymentsApiVoid?: () => any; + } & (EditableOrderSummary | ReadOnlyOrderSummary) >; export const OrderSummary = (props: Props) => { - const { order, onMarkAsPaid, useLegacyPaymentsApi = false, ...restProps } = props; + const { + order, + onMarkAsPaid, + useLegacyPaymentsApi = false, + isEditable = false, + ...restProps + } = props; const intl = useIntl(); const giftCardsAmount = OrderDetailsViewModel.getGiftCardsAmountUsed({ id: order.id, @@ -47,13 +57,37 @@ export const OrderSummary = (props: Props) => { const canVoid = OrderDetailsViewModel.canOrderVoid(order.actions); const canRefund = OrderDetailsViewModel.canOrderRefund(order.actions); + // Extract editable props + const editableProps = isEditable ? (props as Props & EditableOrderSummary) : null; + + // Filter out props that shouldn't be passed to the DOM + const { + isEditable: _isEditable, + onShippingMethodEdit: _onShippingMethodEdit, + errors: _errors, + orderDiscount: _orderDiscount, + addOrderDiscount: _addOrderDiscount, + removeOrderDiscount: _removeOrderDiscount, + openDialog: _openDialog, + closeDialog: _closeDialog, + isDialogOpen: _isDialogOpen, + orderDiscountAddStatus: _orderDiscountAddStatus, + orderDiscountRemoveStatus: _orderDiscountRemoveStatus, + undiscountedPrice: _undiscountedPrice, + discountedPrice: _discountedPrice, + onLegacyPaymentsApiCapture: _onLegacyPaymentsApiCapture, + onLegacyPaymentsApiRefund: _onLegacyPaymentsApiRefund, + onLegacyPaymentsApiVoid: _onLegacyPaymentsApiVoid, + ...boxProps + } = restProps as any; + return ( - + {intl.formatMessage({ - defaultMessage: "Order summary", - id: "SB//YQ", + defaultMessage: "Summary", + id: "RrCui3", })} @@ -85,16 +119,45 @@ export const OrderSummary = (props: Props) => { - + {isEditable && editableProps ? ( + + ) : ( + + )} ; -export const OrderSummaryListItem = ({ children, amount, showSign, currency, ...props }: Props) => { +export const OrderSummaryListItem = ({ + children, + amount, + showSign, + currency, + title, + amountTitle, + ...props +}: Props): ReactNode => { return ( - + {children} - + {currency} {" "} diff --git a/src/orders/components/OrderSummary/OrderValue.tsx b/src/orders/components/OrderSummary/OrderValue.tsx index 4cc7cf4f0a1..921fca1da6c 100644 --- a/src/orders/components/OrderSummary/OrderValue.tsx +++ b/src/orders/components/OrderSummary/OrderValue.tsx @@ -1,12 +1,132 @@ -import { OrderDetailsFragment } from "@dashboard/graphql"; -import { Box, PropsWithBox, Text } from "@saleor/macaw-ui-next"; -import { useIntl } from "react-intl"; +import { + DiscountValueTypeEnum, + OrderDetailsFragment, + OrderErrorFragment, +} from "@dashboard/graphql"; +import { OrderDiscountContextConsumerProps } from "@dashboard/products/components/OrderDiscountProviders/OrderDiscountProvider"; +import { OrderDiscountData } from "@dashboard/products/components/OrderDiscountProviders/types"; +import { getFormErrors } from "@dashboard/utils/errors"; +import getOrderErrorMessage from "@dashboard/utils/errors/order"; +import { Box, Popover, PropsWithBox, sprinkles, Text } from "@saleor/macaw-ui-next"; +import { ReactNode } from "react"; +import { defineMessages, useIntl } from "react-intl"; +import OrderDiscountCommonModal from "../OrderDiscountCommonModal"; +import { ORDER_DISCOUNT } from "../OrderDiscountCommonModal/types"; import { OrderSummaryListAmount } from "./OrderSummaryListAmount"; import { OrderSummaryListItem } from "./OrderSummaryListItem"; import { OrderValueHeader } from "./OrderValueHeader"; -type Props = PropsWithBox<{ +const InlineLink = ({ + children, + onClick, + title, + "data-test-id": dataTestId, +}: { + children: ReactNode; + onClick?: () => void; + title?: string; + "data-test-id"?: string; +}): ReactNode => ( + { + e.preventDefault(); + onClick?.(); + }} + href="#" + title={title} + data-test-id={dataTestId} + style={{ + cursor: "pointer", + textDecoration: "none", + }} + __textDecoration={{ hover: "underline" }} + > + {children} + +); + +const messages = defineMessages({ + discount: { + id: "+8v1ny", + defaultMessage: "Discount", + description: "discount button", + }, + addDiscount: { + id: "Myx1Qp", + defaultMessage: "Add Discount", + description: "add discount button", + }, + setShipping: { + id: "zuWUXf", + defaultMessage: "Set shipping", + description: "set shipping link when no shipping method selected", + }, + shipping: { + id: "glR0om", + defaultMessage: "Shipping", + description: "shipping label", + }, + noShippingCarriers: { + id: "M9LXb5", + defaultMessage: "No applicable shipping carriers", + description: "no shipping carriers title", + }, + noAlternativeShippingMethods: { + id: "gCZYcl", + defaultMessage: "No alternative shipping methods available", + description: "tooltip shown when shipping method is selected but no other options exist", + }, + addShippingAddressFirst: { + id: "BjxQ3u", + defaultMessage: "add shipping address first", + description: "add shipping address first label", + }, + netPrices: { + id: "J/6a1+", + defaultMessage: "Net prices", + description: "tooltip for taxes when showing net prices", + }, + grossPrices: { + id: "TdoAea", + defaultMessage: "Gross prices", + description: "tooltip for taxes when showing gross prices", + }, + subtotalTitle: { + id: "ClUKur", + defaultMessage: "Sum of all line items", + description: "tooltip for subtotal amount", + }, + shippingTitle: { + id: "MHY5da", + defaultMessage: "Shipping cost", + description: "tooltip for shipping amount", + }, + taxesTitle: { + id: "fS2rip", + defaultMessage: "Tax amount", + description: "tooltip for taxes amount", + }, + totalTitle: { + id: "aaj/MI", + defaultMessage: "Order total", + description: "tooltip for total amount", + }, + discountTitle: { + id: "i3Hquc", + defaultMessage: "Discount amount", + description: "tooltip for discount amount", + }, + giftCardTitle: { + id: "ztsvOP", + defaultMessage: "Gift card amount used", + description: "tooltip for gift card amount", + }, +}); + +type BaseProps = { orderSubtotal: OrderDetailsFragment["subtotal"]; shippingMethodName: OrderDetailsFragment["shippingMethodName"]; shippingPrice: OrderDetailsFragment["shippingPrice"]; @@ -15,21 +135,271 @@ type Props = PropsWithBox<{ giftCardsAmount: number | null; usedGiftCards: OrderDetailsFragment["giftCards"] | null; displayGrossPrices: OrderDetailsFragment["displayGrossPrices"]; -}>; - -export const OrderValue = ({ - orderSubtotal, - shippingMethodName, - shippingPrice, - orderTotal, - discounts, - giftCardsAmount, - usedGiftCards, - displayGrossPrices, - ...props -}: Props) => { +}; + +type EditableProps = { + isEditable: true; + orderDiscount?: OrderDiscountData; + addOrderDiscount: OrderDiscountContextConsumerProps["addOrderDiscount"]; + removeOrderDiscount: OrderDiscountContextConsumerProps["removeOrderDiscount"]; + openDialog: OrderDiscountContextConsumerProps["openDialog"]; + closeDialog: OrderDiscountContextConsumerProps["closeDialog"]; + isDialogOpen: OrderDiscountContextConsumerProps["isDialogOpen"]; + orderDiscountAddStatus: OrderDiscountContextConsumerProps["orderDiscountAddStatus"]; + orderDiscountRemoveStatus: OrderDiscountContextConsumerProps["orderDiscountRemoveStatus"]; + undiscountedPrice: OrderDiscountContextConsumerProps["undiscountedPrice"]; + onShippingMethodEdit: () => void; + shippingMethods: OrderDetailsFragment["shippingMethods"]; + shippingMethod: OrderDetailsFragment["shippingMethod"]; + shippingAddress: OrderDetailsFragment["shippingAddress"]; + isShippingRequired: OrderDetailsFragment["isShippingRequired"]; + errors?: OrderErrorFragment[]; +}; + +type ReadOnlyProps = { + isEditable?: false; +}; + +type Props = PropsWithBox; + +const getOrderDiscountLabel = ( + orderDiscount: OrderDiscountData | undefined, + _currency: string, +): { value: string; percentage?: string } => { + if (!orderDiscount) { + return { value: "---" }; + } + + const { value: discountValue, calculationMode, amount: discountAmount } = orderDiscount; + + if (calculationMode === DiscountValueTypeEnum.PERCENTAGE) { + return { + value: discountAmount.amount.toFixed(2), + percentage: `${discountValue}%`, + }; + } + + return { value: discountValue.toFixed(2) }; +}; + +export const OrderValue = (props: Props) => { + const { + orderSubtotal, + shippingMethodName, + shippingPrice, + orderTotal, + discounts, + giftCardsAmount, + usedGiftCards, + displayGrossPrices, + isEditable = false, + ...restProps + } = props; const intl = useIntl(); + const editableProps = isEditable ? (props as BaseProps & EditableProps) : null; + + const hasChosenShippingMethod = + editableProps?.shippingMethod !== null && + editableProps?.shippingMethod !== undefined && + shippingMethodName !== null; + const hasShippingMethods = + !!editableProps?.shippingMethods?.length || editableProps?.isShippingRequired; + + const formErrors = editableProps + ? getFormErrors(["shipping"], editableProps.errors ?? []) + : { shipping: undefined }; + + const renderShippingRow = () => { + const shippingAmountTitle = intl.formatMessage(messages.shippingTitle); + + if (!isEditable) { + return ( + + {intl.formatMessage(messages.shipping)}{" "} + + {shippingMethodName} + + + ); + } + + if (hasChosenShippingMethod) { + if (!hasShippingMethods) { + return ( + + {intl.formatMessage(messages.shipping)}{" "} + + {shippingMethodName} + + + ); + } + + return ( + + {intl.formatMessage(messages.shipping)}{" "} + + {shippingMethodName} + + + ); + } + + if (!hasShippingMethods) { + return ( + + + {intl.formatMessage(messages.noShippingCarriers)} + + + ); + } + + const canSetShipping = !!editableProps?.shippingAddress; + + return ( + + {canSetShipping ? ( + + {intl.formatMessage(messages.setShipping)} + + ) : ( + + {intl.formatMessage(messages.setShipping)} + + )} + + ); + }; + + const renderDiscountRow = () => { + const discountAmountTitle = intl.formatMessage(messages.discountTitle); + + if (!isEditable) { + return discounts.map(discount => ( + + {intl.formatMessage(messages.discount)}{" "} + + {discount.name} + + + )); + } + + const hasDiscount = !!editableProps?.orderDiscount; + const discountLabel = getOrderDiscountLabel( + editableProps?.orderDiscount, + orderTotal.gross.currency, + ); + const discountReason = editableProps?.orderDiscount?.reason; + + if (!hasDiscount) { + return ( + + { + if (!val) { + editableProps?.closeDialog(); + } + }} + open={editableProps?.isDialogOpen} + > + + + + {intl.formatMessage(messages.addDiscount)} + + + + + + {editableProps && ( + + )} + + + + + ); + } + + const discountDisplayValue = discountLabel.percentage || discountLabel.value; + const discountAmount = parseFloat(discountLabel.value) || 0; + + return ( + 0} + > + {intl.formatMessage(messages.discount)}{" "} + { + if (!val) { + editableProps?.closeDialog(); + } + }} + open={editableProps?.isDialogOpen} + > + + + + {discountDisplayValue} + + + + + + {editableProps && ( + + )} + + + + + ); + }; + + const { isEditable: _, ...boxProps } = restProps as any; + return ( - + {intl.formatMessage({ defaultMessage: "Subtotal", id: "L8seEc", })} - - {intl.formatMessage( - { - defaultMessage: "Shipping {carrierName}", - id: "mpkBZc", - }, - { - carrierName: ( - - ({shippingMethodName}) - - ), - }, - )} - + + {renderShippingRow()} + + {formErrors.shipping && ( + + + {getOrderErrorMessage(formErrors.shipping, intl)} + + + )} + {!displayGrossPrices && ( - + {intl.formatMessage({ defaultMessage: "Taxes ", id: "HTiAMm", @@ -78,48 +451,9 @@ export const OrderValue = ({ )} - {discounts.map(discount => ( - - {intl.formatMessage( - { - defaultMessage: "Discount {discountName}", - id: "vs0xiH", - }, - { - discountName: ( - - ({discount.name}) - - ), - }, - )} - - ))} - - {giftCardsAmount && giftCardsAmount > 0 && usedGiftCards && ( - - {intl.formatMessage( - { - defaultMessage: - "{usedGiftCards, plural, one {Gift card} other {Gift cards} } {giftCardCodesList}", - id: "5kODlC", - }, - { - usedGiftCards: usedGiftCards.length, - giftCardCodesList: ( - - ({usedGiftCards.map(card => card.last4CodeChars).join(", ")}) - - ), - }, - )} - - )} + {renderDiscountRow()} - + + {displayGrossPrices && ( - + {intl.formatMessage({ defaultMessage: "Taxes ", id: "HTiAMm", @@ -156,6 +495,29 @@ export const OrderValue = ({
)} + + {giftCardsAmount && giftCardsAmount > 0 && usedGiftCards && ( + + {intl.formatMessage( + { + defaultMessage: + "{usedGiftCards, plural, one {Gift card} other {Gift cards} } {giftCardCodesList}", + id: "5kODlC", + }, + { + usedGiftCards: usedGiftCards.length, + giftCardCodesList: ( + + ({usedGiftCards.map(card => card.last4CodeChars).join(", ")}) + + ), + }, + )} + + )}
); From 4de0c0756a2ed9bc374d6b74a2f45ec3c8ad71a0 Mon Sep 17 00:00:00 2001 From: Mirek Mencel Date: Mon, 8 Dec 2025 16:59:08 +0100 Subject: [PATCH 11/84] Eslint --- src/orders/components/OrderSummary/OrderValue.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/orders/components/OrderSummary/OrderValue.tsx b/src/orders/components/OrderSummary/OrderValue.tsx index 921fca1da6c..c8c8b2fb934 100644 --- a/src/orders/components/OrderSummary/OrderValue.tsx +++ b/src/orders/components/OrderSummary/OrderValue.tsx @@ -182,7 +182,7 @@ const getOrderDiscountLabel = ( return { value: discountValue.toFixed(2) }; }; -export const OrderValue = (props: Props) => { +export const OrderValue = (props: Props): ReactNode => { const { orderSubtotal, shippingMethodName, @@ -210,7 +210,7 @@ export const OrderValue = (props: Props) => { ? getFormErrors(["shipping"], editableProps.errors ?? []) : { shipping: undefined }; - const renderShippingRow = () => { + const renderShippingRow = (): ReactNode => { const shippingAmountTitle = intl.formatMessage(messages.shippingTitle); if (!isEditable) { @@ -287,7 +287,7 @@ export const OrderValue = (props: Props) => { ); }; - const renderDiscountRow = () => { + const renderDiscountRow = (): ReactNode => { const discountAmountTitle = intl.formatMessage(messages.discountTitle); if (!isEditable) { From 8e09b0681bc63094e7c73a1f2f94469476f4a74c Mon Sep 17 00:00:00 2001 From: Mirek Mencel Date: Mon, 8 Dec 2025 18:01:43 +0100 Subject: [PATCH 12/84] More types --- src/orders/components/OrderSummary/OrderValue.tsx | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/orders/components/OrderSummary/OrderValue.tsx b/src/orders/components/OrderSummary/OrderValue.tsx index c8c8b2fb934..604b2a6ac78 100644 --- a/src/orders/components/OrderSummary/OrderValue.tsx +++ b/src/orders/components/OrderSummary/OrderValue.tsx @@ -12,7 +12,14 @@ import { ReactNode } from "react"; import { defineMessages, useIntl } from "react-intl"; import OrderDiscountCommonModal from "../OrderDiscountCommonModal"; -import { ORDER_DISCOUNT } from "../OrderDiscountCommonModal/types"; +import { ORDER_DISCOUNT, OrderDiscountCommonInput } from "../OrderDiscountCommonModal/types"; + +const emptyDiscount: OrderDiscountCommonInput = { + value: 0, + reason: "", + calculationMode: DiscountValueTypeEnum.PERCENTAGE, +}; + import { OrderSummaryListAmount } from "./OrderSummaryListAmount"; import { OrderSummaryListItem } from "./OrderSummaryListItem"; import { OrderValueHeader } from "./OrderValueHeader"; @@ -336,7 +343,7 @@ export const OrderValue = (props: Props): ReactNode => { {editableProps && ( { {editableProps && ( Date: Mon, 8 Dec 2025 18:03:16 +0100 Subject: [PATCH 13/84] Extract messages --- locale/defaultMessages.json | 85 ++++++++++++++++++------------------- 1 file changed, 41 insertions(+), 44 deletions(-) diff --git a/locale/defaultMessages.json b/locale/defaultMessages.json index 3946c6861c7..8d138947e6f 100644 --- a/locale/defaultMessages.json +++ b/locale/defaultMessages.json @@ -1071,10 +1071,6 @@ "context": "account information, header", "string": "Account Information" }, - "4xZFP4": { - "context": "tooltip for shipping amount", - "string": "Shipping cost" - }, "4y3Smi": { "string": "Removing permissions may cause extension to break." }, @@ -1569,10 +1565,6 @@ "context": "checkbox label", "string": "Send fulfillment email to customer" }, - "8EDRSz": { - "context": "tooltip shown when shipping method is selected but no other options exist", - "string": "No alternative shipping methods available" - }, "8EGagh": { "context": "search box label", "string": "Filter Countries" @@ -2283,6 +2275,10 @@ "context": "channel publication status", "string": "Not published" }, + "ClUKur": { + "context": "tooltip for subtotal amount", + "string": "Sum of all line items" + }, "CmxKIg": { "context": "window title", "string": "Grant refund" @@ -2885,10 +2881,6 @@ "context": "dialog header", "string": "Select destination channel:" }, - "GSC7Rw": { - "context": "tooltip for taxes amount", - "string": "Tax amount" - }, "GTCg9O": { "string": "You must add at least one voucher code" }, @@ -2954,10 +2946,6 @@ "context": "add metadata field,button", "string": "Add Field" }, - "GiJ1QZ": { - "context": "tooltip for total amount", - "string": "Order total" - }, "GiJm1v": { "context": "dialog content", "string": "{counter,plural,one{Are you sure you want to unassign this category?} other{Are you sure you want to unassign {displayQuantity} categories?}}" @@ -3293,10 +3281,6 @@ "context": "header", "string": "Edit Media" }, - "IirEpN": { - "context": "tooltip for taxes when showing gross prices", - "string": "Gross prices" - }, "Ila7WO": { "context": "change warehouse dialog description", "string": "Choose warehouse from which you want to fulfill {productName}" @@ -3334,7 +3318,7 @@ "context": "bulk disable label", "string": "Deactivate" }, - "IzrVyw": { + "J/6a1+": { "context": "tooltip for taxes when showing net prices", "string": "Net prices" }, @@ -3519,10 +3503,6 @@ "context": "refund button", "string": "Refund" }, - "K/a8rS": { - "context": "tooltip for subtotal amount", - "string": "Sum of all line items" - }, "K/gnGg": { "string": "If you want to disable this User please uncheck the box below." }, @@ -3806,6 +3786,10 @@ "context": "deactivate", "string": "Deactivate" }, + "MHY5da": { + "context": "tooltip for shipping amount", + "string": "Shipping cost" + }, "MIC9W7": { "context": "WarehouseSettings pickup title", "string": "Pickup" @@ -4322,10 +4306,6 @@ "context": "back button", "string": "Back" }, - "PRlD0A": { - "context": "shipping label", - "string": "Shipping" - }, "PTW56s": { "context": "alert", "string": "Channel limit reached" @@ -4782,9 +4762,6 @@ "S8kqP9": { "string": "Conditions" }, - "SB//YQ": { - "string": "Summary" - }, "SBb6Ej": { "context": "select a warehouse to fulfill product from", "string": "Select warehouse..." @@ -5022,6 +4999,10 @@ "TdTXXf": { "string": "Learn more" }, + "TdoAea": { + "context": "tooltip for taxes when showing gross prices", + "string": "Gross prices" + }, "Tenl9A": { "context": "grant refund, refund card toggle", "string": "Refund shipment: {currency} {amount}" @@ -6038,10 +6019,6 @@ "context": "note input subtitle", "string": "Why was this gift card issued. This note will not be shown to the customer. Note will be stored in gift card history" }, - "Zvjkx8": { - "context": "tooltip for discount amount", - "string": "Discount amount" - }, "Zvo5iu": { "string": "API reference" }, @@ -6174,6 +6151,10 @@ "context": "price rates info", "string": "This rate will apply to all orders" }, + "aaj/MI": { + "context": "tooltip for total amount", + "string": "Order total" + }, "aasX8r": { "context": "label", "string": "Link type" @@ -6861,6 +6842,10 @@ "context": "input label", "string": "Search Attributes" }, + "fS2rip": { + "context": "tooltip for taxes amount", + "string": "Tax amount" + }, "fU+a9k": { "context": "date attribute type", "string": "Date" @@ -6999,6 +6984,10 @@ "context": "gift card history message", "string": "Gift card was deactivated by {deactivatedBy}" }, + "gCZYcl": { + "context": "tooltip shown when shipping method is selected but no other options exist", + "string": "No alternative shipping methods available" + }, "gE6aiQ": { "context": "PageTypeDeleteWarningDialog single no assigned items description", "string": "Are you sure you want to delete {typeName}? If you remove it you won’t be able to assign it to created models." @@ -7069,6 +7058,10 @@ "context": "button", "string": "Create product type" }, + "glR0om": { + "context": "shipping label", + "string": "Shipping" + }, "glT6fm": { "string": "Voucher is limited to these countries" }, @@ -7268,6 +7261,10 @@ "context": "section header returned", "string": "Fulfillment waiting for approval" }, + "i3Hquc": { + "context": "tooltip for discount amount", + "string": "Discount amount" + }, "i3Mvj8": { "context": "product attribute error", "string": "This variant already exists" @@ -8831,10 +8828,6 @@ "context": "limit voucher", "string": "Limit of Uses" }, - "s6YKKO": { - "context": "set shipping link when no shipping method selected", - "string": "Set shipping" - }, "s6lW8R": { "context": "option label", "string": "Change address" @@ -9470,10 +9463,6 @@ "vuKrlW": { "string": "Stock" }, - "vuNPLt": { - "context": "tooltip for gift card amount", - "string": "Gift card amount used" - }, "vwA9Fq": { "context": "notification", "string": "Selected models were deleted." @@ -10086,10 +10075,18 @@ "ztQgD8": { "string": "No attributes found" }, + "ztsvOP": { + "context": "tooltip for gift card amount", + "string": "Gift card amount used" + }, "ztvvcm": { "context": "swatch attribute type", "string": "Swatch type" }, + "zuWUXf": { + "context": "set shipping link when no shipping method selected", + "string": "Set shipping" + }, "zv9OGI": { "string": "No models found" }, From a7905f84682cef2df636e91189c39d26a566e865 Mon Sep 17 00:00:00 2001 From: Mirek Mencel Date: Mon, 8 Dec 2025 18:06:12 +0100 Subject: [PATCH 14/84] Add changeset --- .changeset/few-bushes-ask.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/few-bushes-ask.md diff --git a/.changeset/few-bushes-ask.md b/.changeset/few-bushes-ask.md new file mode 100644 index 00000000000..2619b8d8bf7 --- /dev/null +++ b/.changeset/few-bushes-ask.md @@ -0,0 +1,5 @@ +--- +"saleor-dashboard": patch +--- + +Unify Draft and Unconfirmed order info with the rest of the statuses From f90f52b52b23d25be1151c2c0e90dedc59009908 Mon Sep 17 00:00:00 2001 From: Mirek Mencel Date: Mon, 8 Dec 2025 18:10:14 +0100 Subject: [PATCH 15/84] Fix imports placement --- src/orders/components/OrderSummary/OrderValue.tsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/orders/components/OrderSummary/OrderValue.tsx b/src/orders/components/OrderSummary/OrderValue.tsx index 604b2a6ac78..1b8db507b50 100644 --- a/src/orders/components/OrderSummary/OrderValue.tsx +++ b/src/orders/components/OrderSummary/OrderValue.tsx @@ -13,6 +13,9 @@ import { defineMessages, useIntl } from "react-intl"; import OrderDiscountCommonModal from "../OrderDiscountCommonModal"; import { ORDER_DISCOUNT, OrderDiscountCommonInput } from "../OrderDiscountCommonModal/types"; +import { OrderSummaryListAmount } from "./OrderSummaryListAmount"; +import { OrderSummaryListItem } from "./OrderSummaryListItem"; +import { OrderValueHeader } from "./OrderValueHeader"; const emptyDiscount: OrderDiscountCommonInput = { value: 0, @@ -20,10 +23,6 @@ const emptyDiscount: OrderDiscountCommonInput = { calculationMode: DiscountValueTypeEnum.PERCENTAGE, }; -import { OrderSummaryListAmount } from "./OrderSummaryListAmount"; -import { OrderSummaryListItem } from "./OrderSummaryListItem"; -import { OrderValueHeader } from "./OrderValueHeader"; - const InlineLink = ({ children, onClick, From c0d91044c54666645224697921caa1d128cba5ad Mon Sep 17 00:00:00 2001 From: Mirek Mencel Date: Tue, 9 Dec 2025 15:19:02 +0100 Subject: [PATCH 16/84] Clean up the old unused Draft summary component --- .../OrderDetailsPage/OrderDetailsPage.tsx | 2 - .../OrderDraftDetails/OrderDraftDetails.tsx | 25 -- .../OrderDraftDetailsSummary.tsx | 243 ------------------ .../OrderDraftDetailsSummary/index.ts | 2 - .../OrderDraftDetailsSummary/messages.ts | 49 ---- .../OrderDraftPage/OrderDraftPage.tsx | 2 - 6 files changed, 323 deletions(-) delete mode 100644 src/orders/components/OrderDraftDetailsSummary/OrderDraftDetailsSummary.tsx delete mode 100644 src/orders/components/OrderDraftDetailsSummary/index.ts delete mode 100644 src/orders/components/OrderDraftDetailsSummary/messages.ts diff --git a/src/orders/components/OrderDetailsPage/OrderDetailsPage.tsx b/src/orders/components/OrderDetailsPage/OrderDetailsPage.tsx index 8025b84c89b..632e7e4e93b 100644 --- a/src/orders/components/OrderDetailsPage/OrderDetailsPage.tsx +++ b/src/orders/components/OrderDetailsPage/OrderDetailsPage.tsx @@ -252,8 +252,6 @@ const OrderDetailsPage = (props: OrderDetailsPageProps) => { onOrderLineAdd={onOrderLineAdd} onOrderLineChange={onOrderLineChange} onOrderLineRemove={onOrderLineRemove} - onShippingMethodEdit={onShippingMethodEdit} - hideSummary /> diff --git a/src/orders/components/OrderDraftDetails/OrderDraftDetails.tsx b/src/orders/components/OrderDraftDetails/OrderDraftDetails.tsx index 8fe1f63cbbe..c35c547214f 100644 --- a/src/orders/components/OrderDraftDetails/OrderDraftDetails.tsx +++ b/src/orders/components/OrderDraftDetails/OrderDraftDetails.tsx @@ -6,16 +6,10 @@ import { OrderErrorFragment, OrderLineInput, } from "@dashboard/graphql"; -import { - OrderDiscountContext, - OrderDiscountContextConsumerProps, -} from "@dashboard/products/components/OrderDiscountProviders/OrderDiscountProvider"; import { Button } from "@saleor/macaw-ui-next"; import { FormattedMessage, useIntl } from "react-intl"; -import { maybe } from "../../../misc"; import OrderDraftDetailsProducts from "../OrderDraftDetailsProducts/OrderDraftDetailsProducts"; -import OrderDraftDetailsSummary from "../OrderDraftDetailsSummary"; interface OrderDraftDetailsProps { order: OrderDetailsFragment; @@ -25,10 +19,7 @@ interface OrderDraftDetailsProps { onOrderLineAdd: () => void; onOrderLineChange: (id: string, data: OrderLineInput) => void; onOrderLineRemove: (id: string) => void; - onShippingMethodEdit: () => void; onOrderLineShowMetadata: (id: string) => void; - /** Hide the summary section (when using OrderSummary component instead) */ - hideSummary?: boolean; } const OrderDraftDetails = ({ @@ -39,9 +30,7 @@ const OrderDraftDetails = ({ onOrderLineAdd, onOrderLineChange, onOrderLineRemove, - onShippingMethodEdit, onOrderLineShowMetadata, - hideSummary = false, }: OrderDraftDetailsProps) => { const intl = useIntl(); const isChannelActive = order?.channel.isActive; @@ -73,20 +62,6 @@ const OrderDraftDetails = ({ onOrderLineRemove={onOrderLineRemove} onOrderLineShowMetadata={onOrderLineShowMetadata} /> - {!hideSummary && maybe(() => order.lines.length) !== 0 && ( - - - {(orderDiscountProps: OrderDiscountContextConsumerProps) => ( - - )} - - - )} ); }; diff --git a/src/orders/components/OrderDraftDetailsSummary/OrderDraftDetailsSummary.tsx b/src/orders/components/OrderDraftDetailsSummary/OrderDraftDetailsSummary.tsx deleted file mode 100644 index 9614a8443db..00000000000 --- a/src/orders/components/OrderDraftDetailsSummary/OrderDraftDetailsSummary.tsx +++ /dev/null @@ -1,243 +0,0 @@ -// @ts-strict-ignore -import { ButtonLink } from "@dashboard/components/ButtonLink"; -import HorizontalSpacer from "@dashboard/components/HorizontalSpacer"; -import Money from "@dashboard/components/Money"; -import { - DiscountValueTypeEnum, - OrderDetailsFragment, - OrderErrorFragment, -} from "@dashboard/graphql"; -import { OrderDiscountContextConsumerProps } from "@dashboard/products/components/OrderDiscountProviders/OrderDiscountProvider"; -import { OrderDiscountData } from "@dashboard/products/components/OrderDiscountProviders/types"; -import { getFormErrors } from "@dashboard/utils/errors"; -import getOrderErrorMessage from "@dashboard/utils/errors/order"; -import { makeStyles } from "@saleor/macaw-ui"; -import { Box, Popover, sprinkles, Text } from "@saleor/macaw-ui-next"; -import { useIntl } from "react-intl"; - -import OrderDiscountCommonModal from "../OrderDiscountCommonModal"; -import { ORDER_DISCOUNT } from "../OrderDiscountCommonModal/types"; -import { messages } from "./messages"; - -const useStyles = makeStyles( - theme => ({ - root: { - ...theme.typography.body1, - lineHeight: 1.9, - width: "100%", - }, - textRight: { - textAlign: "right", - }, - textError: { - color: theme.palette.error.main, - marginLeft: theme.spacing(1.5), - display: "inline", - }, - subtitle: { - color: theme.palette.grey[500], - paddingRight: theme.spacing(1), - }, - relativeRow: { - position: "relative", - }, - percentDiscountLabelContainer: { - display: "flex", - flexDirection: "row", - alignItems: "baseline", - justifyContent: "flex-end", - }, - shippingMethodContainer: { - display: "flex", - flexDirection: "row", - alignItems: "baseline", - }, - }), - { name: "OrderDraftDetailsSummary" }, -); -const PRICE_PLACEHOLDER = "---"; - -interface OrderDraftDetailsSummaryProps extends OrderDiscountContextConsumerProps { - disabled?: boolean; - order: OrderDetailsFragment; - errors: OrderErrorFragment[]; - onShippingMethodEdit: () => void; -} - -const OrderDraftDetailsSummary = (props: OrderDraftDetailsSummaryProps) => { - const { - order, - errors, - onShippingMethodEdit, - orderDiscount, - addOrderDiscount, - removeOrderDiscount, - openDialog, - closeDialog, - isDialogOpen, - orderDiscountAddStatus, - orderDiscountRemoveStatus, - undiscountedPrice, - } = props; - const intl = useIntl(); - const classes = useStyles(props); - - if (!order) { - return null; - } - - const { - subtotal, - total, - shippingMethod, - shippingMethodName, - shippingMethods, - shippingPrice, - shippingAddress, - isShippingRequired, - } = order; - const formErrors = getFormErrors(["shipping"], errors); - const hasChosenShippingMethod = shippingMethod !== null && shippingMethodName !== null; - const hasShippingMethods = !!shippingMethods?.length || isShippingRequired; - const discountTitle = orderDiscount ? messages.discount : messages.addDiscount; - const getOrderDiscountLabel = (orderDiscountData: OrderDiscountData) => { - if (!orderDiscountData) { - return PRICE_PLACEHOLDER; - } - - const { value: discountValue, calculationMode, amount: discountAmount } = orderDiscountData; - const currency = total.gross.currency; - - if (calculationMode === DiscountValueTypeEnum.PERCENTAGE) { - return ( -
- {`(${discountValue}%)`} - -
- ); - } - - return ; - }; - const getShippingMethodComponent = () => { - if (hasChosenShippingMethod) { - // Shipping method is selected but no alternatives available - show as plain text with tooltip - if (!hasShippingMethods) { - return ( - - {shippingMethodName} - - ); - } - - return {`${shippingMethodName}`}; - } - - const shippingCarrierBase = intl.formatMessage(messages.addShippingCarrier); - - if (shippingAddress) { - return ( - - {shippingCarrierBase} - - ); - } - - const addShippingAddressInfo = intl.formatMessage(messages.addShippingAddressInfo); - - return ( -
- - {shippingCarrierBase} - - - {`(${addShippingAddressInfo})`} -
- ); - }; - - return ( - - - - - - - - - - - - - - - - - - - - - - - - -
- { - if (!val) { - closeDialog(); - } - }} - open={isDialogOpen} - > - - - - {intl.formatMessage(discountTitle)} - - - - - - - - - - {getOrderDiscountLabel(orderDiscount)}
{intl.formatMessage(messages.subtotal)} - -
- {(hasShippingMethods || hasChosenShippingMethod) && getShippingMethodComponent()} - - {!hasShippingMethods && - !hasChosenShippingMethod && - intl.formatMessage(messages.noShippingCarriers)} - - {formErrors.shipping && ( - - {getOrderErrorMessage(formErrors.shipping, intl)} - - )} - - {hasChosenShippingMethod ? : PRICE_PLACEHOLDER} -
{intl.formatMessage(messages.taxes)} - -
{intl.formatMessage(messages.total)} - -
- ); -}; - -export default OrderDraftDetailsSummary; diff --git a/src/orders/components/OrderDraftDetailsSummary/index.ts b/src/orders/components/OrderDraftDetailsSummary/index.ts deleted file mode 100644 index 78bbd58f09d..00000000000 --- a/src/orders/components/OrderDraftDetailsSummary/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from "./OrderDraftDetailsSummary"; -export * from "./OrderDraftDetailsSummary"; diff --git a/src/orders/components/OrderDraftDetailsSummary/messages.ts b/src/orders/components/OrderDraftDetailsSummary/messages.ts deleted file mode 100644 index b195c069684..00000000000 --- a/src/orders/components/OrderDraftDetailsSummary/messages.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { defineMessages } from "react-intl"; - -export const messages = defineMessages({ - addShippingAddressInfo: { - id: "BjxQ3u", - defaultMessage: "add shipping address first", - description: "add shipping address first label", - }, - subtotal: { - id: "xUvWaP", - defaultMessage: "Subtotal", - description: "subtotal price", - }, - addDiscount: { - id: "Myx1Qp", - defaultMessage: "Add Discount", - description: "add discount button", - }, - discount: { - id: "+8v1ny", - defaultMessage: "Discount", - description: "discount button", - }, - addShippingCarrier: { - id: "Jb1/3V", - defaultMessage: "Add shipping carrier", - description: "add shipping carrier button", - }, - noShippingCarriers: { - id: "M9LXb5", - defaultMessage: "No applicable shipping carriers", - description: "no shipping carriers title", - }, - noAlternativeShippingMethods: { - id: "gCZYcl", - defaultMessage: "No alternative shipping methods available", - description: "tooltip shown when shipping method is selected but no other options exist", - }, - total: { - id: "S/yAtJ", - defaultMessage: "Total", - description: "total price", - }, - taxes: { - id: "mQtoRO", - defaultMessage: "Taxes (VAT included)", - description: "taxes title", - }, -}); diff --git a/src/orders/components/OrderDraftPage/OrderDraftPage.tsx b/src/orders/components/OrderDraftPage/OrderDraftPage.tsx index 256199f318e..36e2f0004ab 100644 --- a/src/orders/components/OrderDraftPage/OrderDraftPage.tsx +++ b/src/orders/components/OrderDraftPage/OrderDraftPage.tsx @@ -151,9 +151,7 @@ const OrderDraftPage = (props: OrderDraftPageProps) => { onOrderLineAdd={onOrderLineAdd} onOrderLineChange={onOrderLineChange} onOrderLineRemove={onOrderLineRemove} - onShippingMethodEdit={onShippingMethodEdit} onOrderLineShowMetadata={onOrderLineShowMetadata} - hideSummary /> {order && orderDiscountContext && ( <> From d7a0151a9ea14f4373938f17176173c3570047bb Mon Sep 17 00:00:00 2001 From: Mirek Mencel Date: Tue, 9 Dec 2025 15:36:22 +0100 Subject: [PATCH 17/84] Unify the Draft header style and reuse OrderCardTitle component --- locale/defaultMessages.json | 24 ++------ .../OrderCardTitle/OrderCardTitle.tsx | 2 +- .../components/OrderCardTitle/messages.ts | 5 ++ src/orders/components/OrderCardTitle/utils.ts | 1 + .../OrderDraftDetails/OrderDraftDetails.tsx | 57 ++++++++++--------- 5 files changed, 41 insertions(+), 48 deletions(-) diff --git a/locale/defaultMessages.json b/locale/defaultMessages.json index 300cf0d9e29..a2e0962ba48 100644 --- a/locale/defaultMessages.json +++ b/locale/defaultMessages.json @@ -546,10 +546,6 @@ "16sza6": { "string": "Refund grants reserve a money which later can be sent to customers via original payment methods or a manual transaction." }, - "18wvf7": { - "context": "section header", - "string": "Order Details" - }, "19/lwV": { "string": "Determine attributes used to create product types" }, @@ -1348,6 +1344,10 @@ "6Y1YDn": { "string": "Updated extension permissions" }, + "6YKFVP": { + "context": "draft order lines, section header", + "string": "Order lines" + }, "6ZubLQ": { "string": "Manage available refunds reasons" }, @@ -3430,10 +3430,6 @@ "context": "button", "string": "Edit" }, - "Jb1/3V": { - "context": "add shipping carrier button", - "string": "Add shipping carrier" - }, "JbUg2b": { "context": "activate app", "string": "Are you sure you want to activate {name}? Activating will start gathering events." @@ -4727,10 +4723,6 @@ "RzsKm8": { "string": "Shipping methods" }, - "S/yAtJ": { - "context": "total price", - "string": "Total" - }, "S1u2pa": { "context": "app settings error", "string": "Failed to fetch extension settings" @@ -7963,10 +7955,6 @@ "mMOskm": { "string": "Open product detail" }, - "mQtoRO": { - "context": "taxes title", - "string": "Taxes (VAT included)" - }, "mSCZd4": { "context": "Webhook details asynchronous events", "string": "Asynchronous" @@ -9695,10 +9683,6 @@ "xTyg+p": { "string": "No options to select" }, - "xUvWaP": { - "context": "subtotal price", - "string": "Subtotal" - }, "xVn5B0": { "context": "added new attribute value", "string": "Added new value" diff --git a/src/orders/components/OrderCardTitle/OrderCardTitle.tsx b/src/orders/components/OrderCardTitle/OrderCardTitle.tsx index 480a2242148..e140daca0f7 100644 --- a/src/orders/components/OrderCardTitle/OrderCardTitle.tsx +++ b/src/orders/components/OrderCardTitle/OrderCardTitle.tsx @@ -9,7 +9,7 @@ import { TrackingNumberDisplay } from "./TrackingNumberDisplay"; import { getOrderTitleMessage } from "./utils"; import { WarehouseInfo } from "./WarehouseInfo"; -export type CardTitleStatus = FulfillmentStatus | "unfulfilled"; +export type CardTitleStatus = FulfillmentStatus | "unfulfilled" | "draft"; type BaseOrderCardTitleProps = { status?: CardTitleStatus; diff --git a/src/orders/components/OrderCardTitle/messages.ts b/src/orders/components/OrderCardTitle/messages.ts index da64b4eccdd..0616113c4f7 100644 --- a/src/orders/components/OrderCardTitle/messages.ts +++ b/src/orders/components/OrderCardTitle/messages.ts @@ -41,6 +41,11 @@ export const orderTitleMessages = defineMessages({ id: "vcMnX2", description: "unfulfilled fulfillment, section header", }, + draft: { + defaultMessage: "Order lines", + id: "wL850U", + description: "draft order lines, section header", + }, fulfilledFromWarehouse: { id: "W2glWk", defaultMessage: "From {warehouseName}", diff --git a/src/orders/components/OrderCardTitle/utils.ts b/src/orders/components/OrderCardTitle/utils.ts index 7973006001a..2f5b1c83c15 100644 --- a/src/orders/components/OrderCardTitle/utils.ts +++ b/src/orders/components/OrderCardTitle/utils.ts @@ -13,6 +13,7 @@ const STATUS_MESSAGE_MAP: Record = { [FulfillmentStatus.RETURNED]: orderTitleMessages.returned, [FulfillmentStatus.WAITING_FOR_APPROVAL]: orderTitleMessages.waitingForApproval, unfulfilled: orderTitleMessages.unfulfilled, + draft: orderTitleMessages.draft, }; export const getOrderTitleMessage = (status?: CardTitleStatus): MessageDescriptor => diff --git a/src/orders/components/OrderDraftDetails/OrderDraftDetails.tsx b/src/orders/components/OrderDraftDetails/OrderDraftDetails.tsx index c35c547214f..cefc9886a03 100644 --- a/src/orders/components/OrderDraftDetails/OrderDraftDetails.tsx +++ b/src/orders/components/OrderDraftDetails/OrderDraftDetails.tsx @@ -6,9 +6,10 @@ import { OrderErrorFragment, OrderLineInput, } from "@dashboard/graphql"; -import { Button } from "@saleor/macaw-ui-next"; -import { FormattedMessage, useIntl } from "react-intl"; +import { Box, Button } from "@saleor/macaw-ui-next"; +import { FormattedMessage } from "react-intl"; +import { OrderCardTitle } from "../OrderCardTitle/OrderCardTitle"; import OrderDraftDetailsProducts from "../OrderDraftDetailsProducts/OrderDraftDetailsProducts"; interface OrderDraftDetailsProps { @@ -32,36 +33,38 @@ const OrderDraftDetails = ({ onOrderLineRemove, onOrderLineShowMetadata, }: OrderDraftDetailsProps) => { - const intl = useIntl(); const isChannelActive = order?.channel.isActive; const areProductsInChannel = !!channelUsabilityData?.products.totalCount; return ( - - - - {intl.formatMessage({ - id: "18wvf7", - defaultMessage: "Order Details", - description: "section header", - })} - - - {isChannelActive && areProductsInChannel && ( - - )} - - - + + + + ) + } /> + + + ); }; From 4aae8de13486e718cb899271fa57ecbbe56b3242 Mon Sep 17 00:00:00 2001 From: Mirek Mencel Date: Tue, 9 Dec 2025 15:54:05 +0100 Subject: [PATCH 18/84] Improve the Add products button showing disabled state When no products are available in the channel or channel is inactive. --- locale/defaultMessages.json | 12 +- .../OrderDraftDetails.test.tsx | 117 ++++++++++++++++++ .../OrderDraftDetails/OrderDraftDetails.tsx | 47 +++++-- 3 files changed, 159 insertions(+), 17 deletions(-) create mode 100644 src/orders/components/OrderDraftDetails/OrderDraftDetails.test.tsx diff --git a/locale/defaultMessages.json b/locale/defaultMessages.json index a2e0962ba48..94fb31a7e19 100644 --- a/locale/defaultMessages.json +++ b/locale/defaultMessages.json @@ -1344,10 +1344,6 @@ "6Y1YDn": { "string": "Updated extension permissions" }, - "6YKFVP": { - "context": "draft order lines, section header", - "string": "Order lines" - }, "6ZubLQ": { "string": "Manage available refunds reasons" }, @@ -3396,6 +3392,10 @@ "context": "expiry date checkbox label", "string": "Gift card expires" }, + "JLH97N": { + "context": "add products button tooltip", + "string": "Add products from {channelName}" + }, "JMBsrr": { "context": "table header available stock label", "string": "Available" @@ -9527,6 +9527,10 @@ "wL7VAE": { "string": "Actions" }, + "wL850U": { + "context": "draft order lines, section header", + "string": "Order lines" + }, "wLB8B3": { "context": "column header", "string": "Visibility" diff --git a/src/orders/components/OrderDraftDetails/OrderDraftDetails.test.tsx b/src/orders/components/OrderDraftDetails/OrderDraftDetails.test.tsx new file mode 100644 index 00000000000..ad5049cc59c --- /dev/null +++ b/src/orders/components/OrderDraftDetails/OrderDraftDetails.test.tsx @@ -0,0 +1,117 @@ +import { channelsList } from "@dashboard/channels/fixtures"; +import { channelUsabilityData, order } from "@dashboard/orders/fixtures"; +import Wrapper from "@test/wrapper"; +import { render, screen } from "@testing-library/react"; + +import OrderDraftDetails from "./OrderDraftDetails"; + +// Mock the child component to avoid needing complex context providers +jest.mock("../OrderDraftDetailsProducts/OrderDraftDetailsProducts", () => ({ + __esModule: true, + default: () =>
, +})); + +describe("OrderDraftDetails", () => { + const defaultProps = { + order: order("--placeholder--"), + channelUsabilityData, + errors: [], + loading: false, + onOrderLineAdd: jest.fn(), + onOrderLineChange: jest.fn(), + onOrderLineRemove: jest.fn(), + onOrderLineShowMetadata: jest.fn(), + }; + + it("renders Add products button enabled with channel tooltip when channel is active and has products", () => { + // Arrange + render( + + + , + ); + + // Act + const button = screen.getByTestId("add-products-button"); + + // Assert + expect(button).toBeInTheDocument(); + expect(button).not.toBeDisabled(); + // Note: intl formatMessage in test env may not interpolate, so we check for the pattern + expect(button.getAttribute("title")).toMatch(/Add products from/); + }); + + it("renders Add products button disabled with tooltip when channel is inactive", () => { + // Arrange + const inactiveChannelOrder = { + ...order("--placeholder--"), + channel: { ...channelsList[0], isActive: false }, + }; + + render( + + + , + ); + + // Act + const button = screen.getByTestId("add-products-button"); + + // Assert + expect(button).toBeInTheDocument(); + expect(button).toBeDisabled(); + expect(button).toHaveAttribute("title", "Orders cannot be placed in an inactive channel."); + }); + + it("renders Add products button disabled with tooltip when no products in channel", () => { + // Arrange + const noProductsChannelData = { + ...channelUsabilityData, + products: { totalCount: 0, __typename: "ProductCountableConnection" as const }, + }; + + render( + + + , + ); + + // Act + const button = screen.getByTestId("add-products-button"); + + // Assert + expect(button).toBeInTheDocument(); + expect(button).toBeDisabled(); + expect(button).toHaveAttribute("title", "There are no available products in this channel."); + }); + + it("shows inactive channel tooltip when both channel is inactive and no products", () => { + // Arrange + const inactiveChannelOrder = { + ...order("--placeholder--"), + channel: { ...channelsList[0], isActive: false }, + }; + const noProductsChannelData = { + ...channelUsabilityData, + products: { totalCount: 0, __typename: "ProductCountableConnection" as const }, + }; + + render( + + + , + ); + + // Act + const button = screen.getByTestId("add-products-button"); + + // Assert + expect(button).toBeDisabled(); + // Inactive channel takes precedence + expect(button).toHaveAttribute("title", "Orders cannot be placed in an inactive channel."); + }); +}); diff --git a/src/orders/components/OrderDraftDetails/OrderDraftDetails.tsx b/src/orders/components/OrderDraftDetails/OrderDraftDetails.tsx index cefc9886a03..7f82365f9f5 100644 --- a/src/orders/components/OrderDraftDetails/OrderDraftDetails.tsx +++ b/src/orders/components/OrderDraftDetails/OrderDraftDetails.tsx @@ -7,10 +7,11 @@ import { OrderLineInput, } from "@dashboard/graphql"; import { Box, Button } from "@saleor/macaw-ui-next"; -import { FormattedMessage } from "react-intl"; +import { FormattedMessage, useIntl } from "react-intl"; import { OrderCardTitle } from "../OrderCardTitle/OrderCardTitle"; import OrderDraftDetailsProducts from "../OrderDraftDetailsProducts/OrderDraftDetailsProducts"; +import { alertMessages } from "../OrderDraftPage/messages"; interface OrderDraftDetailsProps { order: OrderDetailsFragment; @@ -33,26 +34,46 @@ const OrderDraftDetails = ({ onOrderLineRemove, onOrderLineShowMetadata, }: OrderDraftDetailsProps) => { + const intl = useIntl(); const isChannelActive = order?.channel.isActive; const areProductsInChannel = !!channelUsabilityData?.products.totalCount; + const canAddProducts = isChannelActive && areProductsInChannel; + + const getTooltip = () => { + if (!isChannelActive) { + return intl.formatMessage(alertMessages.inactiveChannel); + } + + if (!areProductsInChannel) { + return intl.formatMessage(alertMessages.noProductsInChannel); + } + + return intl.formatMessage( + { + id: "empNV9", + defaultMessage: "Add products from {channelName}", + description: "add products button tooltip", + }, + { channelName: order?.channel.name }, + ); + }; return ( - - - ) + + + } /> From c77648ca11a36dd7411eff8f5251286e5d3cfa20 Mon Sep 17 00:00:00 2001 From: Mirek Mencel Date: Tue, 9 Dec 2025 15:56:41 +0100 Subject: [PATCH 19/84] Extract messages --- locale/defaultMessages.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/locale/defaultMessages.json b/locale/defaultMessages.json index 94fb31a7e19..e5f122ea4a7 100644 --- a/locale/defaultMessages.json +++ b/locale/defaultMessages.json @@ -3392,10 +3392,6 @@ "context": "expiry date checkbox label", "string": "Gift card expires" }, - "JLH97N": { - "context": "add products button tooltip", - "string": "Add products from {channelName}" - }, "JMBsrr": { "context": "table header available stock label", "string": "Available" @@ -6750,6 +6746,10 @@ "context": "all captured amount from transactions in order", "string": "Captured" }, + "empNV9": { + "context": "add products button tooltip", + "string": "Add products from {channelName}" + }, "epB7b7": { "context": "fulfillment status replaced", "string": "Replaced" From b1c6b522ce570718523c983841048cdb796572e4 Mon Sep 17 00:00:00 2001 From: Mirek Mencel Date: Tue, 9 Dec 2025 16:30:24 +0100 Subject: [PATCH 20/84] Improve the "Set shipping" logic and messages in Order Summary --- locale/defaultMessages.json | 12 ++-- .../components/OrderSummary/OrderValue.tsx | 62 +++++++++---------- 2 files changed, 37 insertions(+), 37 deletions(-) diff --git a/locale/defaultMessages.json b/locale/defaultMessages.json index e5f122ea4a7..c5ebfea2744 100644 --- a/locale/defaultMessages.json +++ b/locale/defaultMessages.json @@ -2106,8 +2106,8 @@ "string": "Address Information" }, "BjxQ3u": { - "context": "add shipping address first label", - "string": "add shipping address first" + "context": "shown when shipping address is required but not set", + "string": "No shipping address" }, "Bkxrhw": { "context": "no webhooks message", @@ -3770,8 +3770,8 @@ "string": "If selected customer won't be able to choose this warehouse as pickup point" }, "M9LXb5": { - "context": "no shipping carriers title", - "string": "No applicable shipping carriers" + "context": "no shipping methods available", + "string": "No applicable shipping methods" }, "MAsLIT": { "context": "custom app token key", @@ -10075,8 +10075,8 @@ "string": "Swatch type" }, "zuWUXf": { - "context": "set shipping link when no shipping method selected", - "string": "Set shipping" + "context": "set shipping method link when no shipping method selected", + "string": "Set shipping method" }, "zv9OGI": { "string": "No models found" diff --git a/src/orders/components/OrderSummary/OrderValue.tsx b/src/orders/components/OrderSummary/OrderValue.tsx index 7cae2cd126d..e04514c1d9a 100644 --- a/src/orders/components/OrderSummary/OrderValue.tsx +++ b/src/orders/components/OrderSummary/OrderValue.tsx @@ -65,30 +65,30 @@ const messages = defineMessages({ defaultMessage: "Add Discount", description: "add discount button", }, - setShipping: { - id: "zuWUXf", - defaultMessage: "Set shipping", - description: "set shipping link when no shipping method selected", + setShippingMethod: { + id: "Dg4Y2p", + defaultMessage: "Set shipping method", + description: "set shipping method link when no shipping method selected", }, shipping: { id: "glR0om", defaultMessage: "Shipping", description: "shipping label", }, - noShippingCarriers: { - id: "M9LXb5", - defaultMessage: "No applicable shipping carriers", - description: "no shipping carriers title", + noShippingMethods: { + id: "G+MtyG", + defaultMessage: "No applicable shipping methods", + description: "no shipping methods available", }, noAlternativeShippingMethods: { id: "gCZYcl", defaultMessage: "No alternative shipping methods available", description: "tooltip shown when shipping method is selected but no other options exist", }, - addShippingAddressFirst: { - id: "BjxQ3u", - defaultMessage: "add shipping address first", - description: "add shipping address first label", + noShippingAddress: { + id: "Ai4XSg", + defaultMessage: "No shipping address", + description: "shown when shipping address is required but not set", }, netPrices: { id: "J/6a1+", @@ -259,36 +259,36 @@ export const OrderValue = (props: Props): ReactNode => { ); } - if (!hasShippingMethods) { + const hasShippingAddress = !!editableProps?.shippingAddress; + + if (!hasShippingAddress) { return ( - {intl.formatMessage(messages.noShippingCarriers)} + {intl.formatMessage(messages.noShippingAddress)} ); } - const canSetShipping = !!editableProps?.shippingAddress; + if (!hasShippingMethods) { + return ( + + + {intl.formatMessage(messages.noShippingMethods)} + + + ); + } return ( - {canSetShipping ? ( - - {intl.formatMessage(messages.setShipping)} - - ) : ( - - {intl.formatMessage(messages.setShipping)} - - )} + + {intl.formatMessage(messages.setShippingMethod)} + ); }; From f4d1ec1ea9036d7b537ed8037204b7ef7dc511c0 Mon Sep 17 00:00:00 2001 From: Mirek Mencel Date: Tue, 9 Dec 2025 16:37:33 +0100 Subject: [PATCH 21/84] Add tests to Order summary --- .../OrderSummary/OrderValue.test.tsx | 583 ++++++++++++++++++ 1 file changed, 583 insertions(+) create mode 100644 src/orders/components/OrderSummary/OrderValue.test.tsx diff --git a/src/orders/components/OrderSummary/OrderValue.test.tsx b/src/orders/components/OrderSummary/OrderValue.test.tsx new file mode 100644 index 00000000000..abaefc19b79 --- /dev/null +++ b/src/orders/components/OrderSummary/OrderValue.test.tsx @@ -0,0 +1,583 @@ +import { DiscountValueTypeEnum, OrderDetailsFragment } from "@dashboard/graphql"; +import { prepareMoney } from "@dashboard/orders/fixtures"; +import { OrderDiscountData } from "@dashboard/products/components/OrderDiscountProviders/types"; +import Wrapper from "@test/wrapper"; +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; + +import { OrderValue } from "./OrderValue"; + +type BaseOrderValueProps = { + orderSubtotal: OrderDetailsFragment["subtotal"]; + shippingMethodName: OrderDetailsFragment["shippingMethodName"]; + shippingPrice: OrderDetailsFragment["shippingPrice"]; + orderTotal: OrderDetailsFragment["total"]; + discounts: OrderDetailsFragment["discounts"]; + giftCardsAmount: number | null; + usedGiftCards: OrderDetailsFragment["giftCards"] | null; + displayGrossPrices: OrderDetailsFragment["displayGrossPrices"]; +}; + +const baseProps: BaseOrderValueProps = { + orderSubtotal: { + __typename: "TaxedMoney", + gross: { __typename: "Money", amount: 100, currency: "USD" }, + net: { __typename: "Money", amount: 100, currency: "USD" }, + }, + shippingMethodName: null, + shippingPrice: { + __typename: "TaxedMoney", + gross: { __typename: "Money", amount: 10, currency: "USD" }, + }, + orderTotal: { + __typename: "TaxedMoney", + gross: { __typename: "Money", amount: 110, currency: "USD" }, + net: { __typename: "Money", amount: 110, currency: "USD" }, + tax: { __typename: "Money", amount: 0, currency: "USD" }, + }, + discounts: [], + giftCardsAmount: null, + usedGiftCards: null, + displayGrossPrices: true, +}; + +const shippingAddress: OrderDetailsFragment["shippingAddress"] = { + __typename: "Address", + id: "address-1", + city: "New York", + cityArea: "", + companyName: "", + country: { __typename: "CountryDisplay", code: "US", country: "United States" }, + countryArea: "NY", + firstName: "John", + lastName: "Doe", + phone: "+1234567890", + postalCode: "10001", + streetAddress1: "123 Main St", + streetAddress2: "", +}; + +const shippingMethod: OrderDetailsFragment["shippingMethod"] = { + __typename: "ShippingMethod", + id: "shipping-method-1", +}; + +const shippingMethods: OrderDetailsFragment["shippingMethods"] = [ + { + __typename: "ShippingMethod", + id: "shipping-method-1", + name: "Standard Shipping", + price: { __typename: "Money", amount: 10, currency: "USD" }, + active: true, + message: null, + }, + { + __typename: "ShippingMethod", + id: "shipping-method-2", + name: "Express Shipping", + price: { __typename: "Money", amount: 25, currency: "USD" }, + active: true, + message: null, + }, +]; + +const createEditableProps = (overrides: Partial[0]> = {}) => ({ + ...baseProps, + isEditable: true as const, + orderDiscount: undefined, + addOrderDiscount: jest.fn(), + removeOrderDiscount: jest.fn(), + openDialog: jest.fn(), + closeDialog: jest.fn(), + isDialogOpen: false, + orderDiscountAddStatus: "default" as const, + orderDiscountRemoveStatus: "default" as const, + undiscountedPrice: prepareMoney(110), + onShippingMethodEdit: jest.fn(), + shippingMethods: [], + shippingMethod: null, + shippingAddress: null, + isShippingRequired: true, + errors: [], + ...overrides, +}); + +describe("OrderValue", () => { + describe("Read-only mode", () => { + it("should render shipping method name as text when not editable", () => { + // Arrange + const props = { + ...baseProps, + shippingMethodName: "Standard Shipping", + }; + + // Act + render( + + + , + ); + + // Assert + expect(screen.getByText("Shipping")).toBeInTheDocument(); + expect(screen.getByText("Standard Shipping")).toBeInTheDocument(); + }); + + it("should render discounts as text when not editable", () => { + // Arrange + const props = { + ...baseProps, + discounts: [ + { + __typename: "OrderDiscount" as const, + id: "discount-1", + name: "Summer Sale", + amount: { __typename: "Money" as const, amount: 15, currency: "USD" }, + type: DiscountValueTypeEnum.FIXED, + valueType: DiscountValueTypeEnum.FIXED, + value: 15, + reason: null, + }, + ], + }; + + // Act + render( + + + , + ); + + // Assert + expect(screen.getByText("Discount")).toBeInTheDocument(); + expect(screen.getByText("Summer Sale")).toBeInTheDocument(); + }); + }); + + describe("Editable mode - Shipping", () => { + it("should show 'No shipping address' when shipping address is not set", () => { + // Arrange + const props = createEditableProps({ + shippingAddress: null, + shippingMethods: shippingMethods, + isShippingRequired: true, + }); + + // Act + render( + + + , + ); + + // Assert + expect(screen.getByText("No shipping address")).toBeInTheDocument(); + expect(screen.queryByText("Set shipping method")).not.toBeInTheDocument(); + }); + + it("should show 'No applicable shipping methods' when address is set but no methods available", () => { + // Arrange + const props = createEditableProps({ + shippingAddress, + shippingMethods: [], + isShippingRequired: false, + }); + + // Act + render( + + + , + ); + + // Assert + expect(screen.getByText("No applicable shipping methods")).toBeInTheDocument(); + expect(screen.queryByText("Set shipping method")).not.toBeInTheDocument(); + }); + + it("should show 'Set shipping method' link when address and methods are available", () => { + // Arrange + const onShippingMethodEdit = jest.fn(); + const props = createEditableProps({ + shippingAddress, + shippingMethods, + isShippingRequired: true, + onShippingMethodEdit, + }); + + // Act + render( + + + , + ); + + // Assert + const setShippingLink = screen.getByText("Set shipping method"); + + expect(setShippingLink).toBeInTheDocument(); + expect(setShippingLink.tagName).toBe("A"); + }); + + it("should call onShippingMethodEdit when 'Set shipping method' link is clicked", async () => { + // Arrange + const onShippingMethodEdit = jest.fn(); + const props = createEditableProps({ + shippingAddress, + shippingMethods, + isShippingRequired: true, + onShippingMethodEdit, + }); + + // Act + render( + + + , + ); + + const setShippingLink = screen.getByText("Set shipping method"); + + await userEvent.click(setShippingLink); + + // Assert + expect(onShippingMethodEdit).toHaveBeenCalledTimes(1); + }); + + it("should show chosen shipping method as clickable link when alternatives exist", async () => { + // Arrange + const onShippingMethodEdit = jest.fn(); + const props = createEditableProps({ + shippingAddress, + shippingMethods, + shippingMethod, + shippingMethodName: "Standard Shipping", + isShippingRequired: true, + onShippingMethodEdit, + }); + + // Act + render( + + + , + ); + + // Assert + const methodLink = screen.getByText("Standard Shipping"); + + expect(methodLink).toBeInTheDocument(); + expect(methodLink.tagName).toBe("A"); + + await userEvent.click(methodLink); + expect(onShippingMethodEdit).toHaveBeenCalledTimes(1); + }); + + it("should show chosen shipping method as non-clickable text when no alternatives exist", () => { + // Arrange + const onShippingMethodEdit = jest.fn(); + const props = createEditableProps({ + shippingAddress, + shippingMethods: [], + shippingMethod, + shippingMethodName: "Standard Shipping", + isShippingRequired: false, + onShippingMethodEdit, + }); + + // Act + render( + + + , + ); + + // Assert + const methodText = screen.getByText("Standard Shipping"); + + expect(methodText).toBeInTheDocument(); + expect(methodText.tagName).toBe("SPAN"); + expect(methodText).toHaveAttribute("title", "No alternative shipping methods available"); + }); + + it("should prioritize 'No shipping address' over 'No applicable shipping methods'", () => { + // Arrange + const props = createEditableProps({ + shippingAddress: null, + shippingMethods: [], + isShippingRequired: false, + }); + + // Act + render( + + + , + ); + + // Assert + expect(screen.getByText("No shipping address")).toBeInTheDocument(); + expect(screen.queryByText("No applicable shipping methods")).not.toBeInTheDocument(); + }); + }); + + describe("Editable mode - Discount", () => { + it("should show 'Add Discount' link when no discount is set", () => { + // Arrange + const props = createEditableProps({ + shippingAddress, + shippingMethods, + orderDiscount: undefined, + }); + + // Act + render( + + + , + ); + + // Assert + expect(screen.getByText("Add Discount")).toBeInTheDocument(); + }); + + it("should call openDialog when 'Add Discount' link is clicked", async () => { + // Arrange + const openDialog = jest.fn(); + const props = createEditableProps({ + shippingAddress, + shippingMethods, + orderDiscount: undefined, + openDialog, + }); + + // Act + render( + + + , + ); + + const addDiscountLink = screen.getByText("Add Discount"); + + await userEvent.click(addDiscountLink); + + // Assert + expect(openDialog).toHaveBeenCalledTimes(1); + }); + + it("should show percentage discount value when discount is percentage type", () => { + // Arrange + const orderDiscount: OrderDiscountData = { + id: "discount-1", + value: 10, + calculationMode: DiscountValueTypeEnum.PERCENTAGE, + amount: { __typename: "Money", amount: 11, currency: "USD" }, + reason: "Loyalty discount", + }; + const props = createEditableProps({ + shippingAddress, + shippingMethods, + orderDiscount, + }); + + // Act + render( + + + , + ); + + // Assert + expect(screen.getByText("Discount")).toBeInTheDocument(); + expect(screen.getByText("10%")).toBeInTheDocument(); + }); + + it("should show fixed discount value when discount is fixed type", () => { + // Arrange + const orderDiscount: OrderDiscountData = { + id: "discount-1", + value: 15, + calculationMode: DiscountValueTypeEnum.FIXED, + amount: { __typename: "Money", amount: 15, currency: "USD" }, + reason: "Special offer", + }; + const props = createEditableProps({ + shippingAddress, + shippingMethods, + orderDiscount, + }); + + // Act + render( + + + , + ); + + // Assert + expect(screen.getByText("Discount")).toBeInTheDocument(); + expect(screen.getByText("15.00")).toBeInTheDocument(); + }); + + it("should show discount reason as tooltip on existing discount", () => { + // Arrange + const orderDiscount: OrderDiscountData = { + id: "discount-1", + value: 10, + calculationMode: DiscountValueTypeEnum.PERCENTAGE, + amount: { __typename: "Money", amount: 11, currency: "USD" }, + reason: "Loyalty discount", + }; + const props = createEditableProps({ + shippingAddress, + shippingMethods, + orderDiscount, + }); + + // Act + render( + + + , + ); + + // Assert + const discountLink = screen.getByText("10%"); + + expect(discountLink).toHaveAttribute("title", "Loyalty discount"); + }); + }); + + describe("Gift cards", () => { + it("should render gift card row when gift cards are present", () => { + // Arrange + const props = { + ...baseProps, + giftCardsAmount: 25, + usedGiftCards: [ + { + __typename: "GiftCard" as const, + id: "gc-1", + last4CodeChars: "ABCD", + currentBalance: { __typename: "Money" as const, amount: 25, currency: "USD" }, + events: [], + }, + ], + }; + + // Act + render( + + + , + ); + + // Assert - query by the title attribute which is reliably set + expect(screen.getByTitle("Gift card amount used")).toBeInTheDocument(); + expect(screen.getByText("-25")).toBeInTheDocument(); + }); + + it("should render multiple gift cards row", () => { + // Arrange + const props = { + ...baseProps, + giftCardsAmount: 50, + usedGiftCards: [ + { + __typename: "GiftCard" as const, + id: "gc-1", + last4CodeChars: "ABCD", + currentBalance: { __typename: "Money" as const, amount: 25, currency: "USD" }, + events: [], + }, + { + __typename: "GiftCard" as const, + id: "gc-2", + last4CodeChars: "WXYZ", + currentBalance: { __typename: "Money" as const, amount: 25, currency: "USD" }, + events: [], + }, + ], + }; + + // Act + render( + + + , + ); + + // Assert + expect(screen.getByTitle("Gift card amount used")).toBeInTheDocument(); + expect(screen.getByText("-50")).toBeInTheDocument(); + }); + + it("should not render gift card section when no gift cards used", () => { + // Arrange + const props = { + ...baseProps, + giftCardsAmount: 0, + usedGiftCards: null, + }; + + // Act + render( + + + , + ); + + // Assert + expect(screen.queryByTitle("Gift card amount used")).not.toBeInTheDocument(); + }); + }); + + describe("Taxes display", () => { + it("should show taxes as included when displayGrossPrices is true", () => { + // Arrange + const props = { + ...baseProps, + displayGrossPrices: true, + orderTotal: { + ...baseProps.orderTotal, + tax: { __typename: "Money" as const, amount: 10, currency: "USD" }, + }, + }; + + // Act + render( + + + , + ); + + // Assert + expect(screen.getByText("Taxes")).toBeInTheDocument(); + expect(screen.getByText("(included)")).toBeInTheDocument(); + }); + + it("should show taxes without 'included' when displayGrossPrices is false", () => { + // Arrange + const props = { + ...baseProps, + displayGrossPrices: false, + orderTotal: { + ...baseProps.orderTotal, + tax: { __typename: "Money" as const, amount: 10, currency: "USD" }, + }, + }; + + // Act + render( + + + , + ); + + // Assert + expect(screen.getByText("Taxes")).toBeInTheDocument(); + expect(screen.queryByText("(included)")).not.toBeInTheDocument(); + }); + }); +}); From f26dfd526e8d9a5b66b08222c33e6f6edf2bc2e6 Mon Sep 17 00:00:00 2001 From: Mirek Mencel Date: Tue, 9 Dec 2025 16:38:12 +0100 Subject: [PATCH 22/84] Extract messages --- locale/defaultMessages.json | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/locale/defaultMessages.json b/locale/defaultMessages.json index c5ebfea2744..495bb18754d 100644 --- a/locale/defaultMessages.json +++ b/locale/defaultMessages.json @@ -1935,6 +1935,10 @@ "context": "attribute properties regarding storefront", "string": "Storefront Properties" }, + "Ai4XSg": { + "context": "shown when shipping address is required but not set", + "string": "No shipping address" + }, "AiurXc": { "string": "Outstanding authorized" }, @@ -2105,10 +2109,6 @@ "context": "header", "string": "Address Information" }, - "BjxQ3u": { - "context": "shown when shipping address is required but not set", - "string": "No shipping address" - }, "Bkxrhw": { "context": "no webhooks message", "string": "No webhooks found" @@ -2429,6 +2429,10 @@ "Dd0Dwl": { "string": "Go to promotions" }, + "Dg4Y2p": { + "context": "set shipping method link when no shipping method selected", + "string": "Set shipping method" + }, "Dgp38J": { "context": "checkbox label", "string": "Restrict order value" @@ -2793,6 +2797,10 @@ "context": "checkbox description", "string": "If selected, this will add all of the countries not selected to other shipping zones" }, + "G+MtyG": { + "context": "no shipping methods available", + "string": "No applicable shipping methods" + }, "G/pgG3": { "context": "dialog header", "string": "Select a channel" @@ -3769,10 +3777,6 @@ "context": "WarehouseSettings disabled warehouse description", "string": "If selected customer won't be able to choose this warehouse as pickup point" }, - "M9LXb5": { - "context": "no shipping methods available", - "string": "No applicable shipping methods" - }, "MAsLIT": { "context": "custom app token key", "string": "Key" @@ -10074,10 +10078,6 @@ "context": "swatch attribute type", "string": "Swatch type" }, - "zuWUXf": { - "context": "set shipping method link when no shipping method selected", - "string": "Set shipping method" - }, "zv9OGI": { "string": "No models found" }, From 94b676f6e358e1472ed7b1e3943c272319810ae6 Mon Sep 17 00:00:00 2001 From: Mirek Mencel Date: Tue, 9 Dec 2025 18:15:05 +0100 Subject: [PATCH 23/84] Fix liniting errors --- src/orders/components/OrderSummary/OrderValue.test.tsx | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/orders/components/OrderSummary/OrderValue.test.tsx b/src/orders/components/OrderSummary/OrderValue.test.tsx index abaefc19b79..4840d0045ce 100644 --- a/src/orders/components/OrderSummary/OrderValue.test.tsx +++ b/src/orders/components/OrderSummary/OrderValue.test.tsx @@ -1,4 +1,4 @@ -import { DiscountValueTypeEnum, OrderDetailsFragment } from "@dashboard/graphql"; +import { DiscountValueTypeEnum, OrderDetailsFragment, OrderDiscountType } from "@dashboard/graphql"; import { prepareMoney } from "@dashboard/orders/fixtures"; import { OrderDiscountData } from "@dashboard/products/components/OrderDiscountProviders/types"; import Wrapper from "@test/wrapper"; @@ -133,8 +133,8 @@ describe("OrderValue", () => { id: "discount-1", name: "Summer Sale", amount: { __typename: "Money" as const, amount: 15, currency: "USD" }, - type: DiscountValueTypeEnum.FIXED, - valueType: DiscountValueTypeEnum.FIXED, + type: OrderDiscountType.MANUAL, + calculationMode: DiscountValueTypeEnum.FIXED, value: 15, reason: null, }, @@ -369,7 +369,6 @@ describe("OrderValue", () => { it("should show percentage discount value when discount is percentage type", () => { // Arrange const orderDiscount: OrderDiscountData = { - id: "discount-1", value: 10, calculationMode: DiscountValueTypeEnum.PERCENTAGE, amount: { __typename: "Money", amount: 11, currency: "USD" }, @@ -396,7 +395,6 @@ describe("OrderValue", () => { it("should show fixed discount value when discount is fixed type", () => { // Arrange const orderDiscount: OrderDiscountData = { - id: "discount-1", value: 15, calculationMode: DiscountValueTypeEnum.FIXED, amount: { __typename: "Money", amount: 15, currency: "USD" }, @@ -423,7 +421,6 @@ describe("OrderValue", () => { it("should show discount reason as tooltip on existing discount", () => { // Arrange const orderDiscount: OrderDiscountData = { - id: "discount-1", value: 10, calculationMode: DiscountValueTypeEnum.PERCENTAGE, amount: { __typename: "Money", amount: 11, currency: "USD" }, From ee0aea14bdf0d32c208bf1c8db87ae69f1230b61 Mon Sep 17 00:00:00 2001 From: Mirek Mencel Date: Tue, 9 Dec 2025 20:09:55 +0100 Subject: [PATCH 24/84] Let users remove the selected shipping method in Drafts --- locale/defaultMessages.json | 4 ++ .../OrderShippingMethodEditDialog.test.tsx | 57 +++++++++++++++++++ .../OrderShippingMethodEditDialog.tsx | 53 +++++++++++++++-- .../OrderDetails/OrderDraftDetails/index.tsx | 1 + 4 files changed, 109 insertions(+), 6 deletions(-) create mode 100644 src/orders/components/OrderShippingMethodEditDialog/OrderShippingMethodEditDialog.test.tsx diff --git a/locale/defaultMessages.json b/locale/defaultMessages.json index 495bb18754d..bcc0de53dd9 100644 --- a/locale/defaultMessages.json +++ b/locale/defaultMessages.json @@ -6633,6 +6633,10 @@ "dWK/Ck": { "string": "Choose countries, you want voucher to be limited to, from the list below" }, + "dYc+5u": { + "context": "no shipping method option", + "string": "No shipping method" + }, "da1ebU": { "string": "Product type" }, diff --git a/src/orders/components/OrderShippingMethodEditDialog/OrderShippingMethodEditDialog.test.tsx b/src/orders/components/OrderShippingMethodEditDialog/OrderShippingMethodEditDialog.test.tsx new file mode 100644 index 00000000000..9db84e37a0c --- /dev/null +++ b/src/orders/components/OrderShippingMethodEditDialog/OrderShippingMethodEditDialog.test.tsx @@ -0,0 +1,57 @@ +import { ConfirmButtonTransitionState } from "@dashboard/components/ConfirmButton"; +import { order } from "@dashboard/orders/fixtures"; +import Wrapper from "@test/wrapper"; +import { render, screen } from "@testing-library/react"; + +import OrderShippingMethodEditDialog from "./OrderShippingMethodEditDialog"; + +describe("OrderShippingMethodEditDialog", () => { + const shippingMethods = order("").shippingMethods!; + const defaultProps = { + confirmButtonState: "default" as ConfirmButtonTransitionState, + errors: [], + open: true, + shippingMethod: shippingMethods[1].id, + shippingMethodName: shippingMethods[1].name, + shippingPrice: shippingMethods[1].price, + shippingMethods, + onClose: jest.fn(), + onSubmit: jest.fn(), + }; + + it("renders with available shipping methods", () => { + render( + + + , + ); + + expect(screen.getByText("Edit Shipping Method")).toBeInTheDocument(); + + expect(screen.getByTestId("shipping-method-select")).toBeInTheDocument(); + }); + + it("renders 'No shipping method' option when isClearable is true", () => { + render( + + + , + ); + + expect(screen.getByText("No shipping method")).toBeInTheDocument(); + }); + + it("renders current shipping method name when selected", () => { + render( + + + , + ); + + expect(screen.getByText(shippingMethods[1].name)).toBeInTheDocument(); + }); +}); diff --git a/src/orders/components/OrderShippingMethodEditDialog/OrderShippingMethodEditDialog.tsx b/src/orders/components/OrderShippingMethodEditDialog/OrderShippingMethodEditDialog.tsx index 8c6f887f6bb..af6cb7506a8 100644 --- a/src/orders/components/OrderShippingMethodEditDialog/OrderShippingMethodEditDialog.tsx +++ b/src/orders/components/OrderShippingMethodEditDialog/OrderShippingMethodEditDialog.tsx @@ -22,14 +22,17 @@ interface OrderShippingMethodEditDialogProps { confirmButtonState: ConfirmButtonTransitionState; errors: OrderErrorFragment[]; open: boolean; - shippingMethod: string; + shippingMethod: string | null | undefined; shippingMethodName?: string; shippingPrice?: OrderDetailsFragment["shippingPrice"]; shippingMethods?: OrderDetailsFragment["shippingMethods"]; onClose: () => any; - onSubmit?: (data: FormData) => any; + onSubmit?: (data: { shippingMethod: string | null }) => any; + isClearable?: boolean; } +const NO_SHIPPING_METHOD_ID = "no-shipping-method-selection"; + const OrderShippingMethodEditDialog = (props: OrderShippingMethodEditDialogProps) => { const { confirmButtonState, @@ -41,6 +44,7 @@ const OrderShippingMethodEditDialog = (props: OrderShippingMethodEditDialogProps shippingMethods, onClose, onSubmit, + isClearable = false, } = props; const errors = useModalDialogErrors(apiErrors, open); const intl = useIntl(); @@ -116,16 +120,54 @@ const OrderShippingMethodEditDialog = (props: OrderShippingMethodEditDialogProps ] : []; - const choices = [...unavailableMethodOption, ...availableChoices]; + const noShippingOption = { + label: ( + + + + + + + + ), + value: NO_SHIPPING_METHOD_ID, + disabled: false, + }; + + const choices = [ + ...(isClearable ? [noShippingOption] : []), + ...unavailableMethodOption, + ...availableChoices, + ]; const initialForm: FormData = { - shippingMethod: currentMethodInChoices ? shippingMethod : "", + shippingMethod: + currentMethodInChoices || unavailableMethodOption.length > 0 + ? (shippingMethod as string) + : NO_SHIPPING_METHOD_ID, + }; + + const handleSubmit = (data: FormData) => { + if (onSubmit) { + onSubmit({ + shippingMethod: data.shippingMethod === NO_SHIPPING_METHOD_ID ? null : data.shippingMethod, + }); + } }; return ( {open && ( -
+ {({ change, data, submit }) => ( <> @@ -178,7 +220,6 @@ const OrderShippingMethodEditDialog = (props: OrderShippingMethodEditDialogProps data-test-id="confirm-button" transitionState={confirmButtonState} onClick={submit} - disabled={!data.shippingMethod} > diff --git a/src/orders/views/OrderDetails/OrderDraftDetails/index.tsx b/src/orders/views/OrderDetails/OrderDraftDetails/index.tsx index ddcd9a51680..c64b1aab75a 100644 --- a/src/orders/views/OrderDetails/OrderDraftDetails/index.tsx +++ b/src/orders/views/OrderDetails/OrderDraftDetails/index.tsx @@ -257,6 +257,7 @@ export const OrderDraftDetails = ({ Date: Tue, 9 Dec 2025 20:16:59 +0100 Subject: [PATCH 25/84] Extract messages --- locale/defaultMessages.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/locale/defaultMessages.json b/locale/defaultMessages.json index bcc0de53dd9..0acb6a0fef5 100644 --- a/locale/defaultMessages.json +++ b/locale/defaultMessages.json @@ -1492,6 +1492,10 @@ "context": "model", "string": "will be visible from {date}" }, + "7ZKQU3": { + "context": "no shipping method option", + "string": "No shipping method" + }, "7dhhzL": { "context": "bulk issue gift cards dialog title", "string": "Bulk Issue Gift Cards" @@ -6633,10 +6637,6 @@ "dWK/Ck": { "string": "Choose countries, you want voucher to be limited to, from the list below" }, - "dYc+5u": { - "context": "no shipping method option", - "string": "No shipping method" - }, "da1ebU": { "string": "Product type" }, From 3210bc122de8f8e03b24d55b17fc1ee03d6d23f7 Mon Sep 17 00:00:00 2001 From: Mirek Mencel Date: Tue, 9 Dec 2025 20:18:18 +0100 Subject: [PATCH 26/84] Types --- .../OrderShippingMethodEditDialog.test.tsx | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/orders/components/OrderShippingMethodEditDialog/OrderShippingMethodEditDialog.test.tsx b/src/orders/components/OrderShippingMethodEditDialog/OrderShippingMethodEditDialog.test.tsx index 9db84e37a0c..29af83e9fca 100644 --- a/src/orders/components/OrderShippingMethodEditDialog/OrderShippingMethodEditDialog.test.tsx +++ b/src/orders/components/OrderShippingMethodEditDialog/OrderShippingMethodEditDialog.test.tsx @@ -7,13 +7,32 @@ import OrderShippingMethodEditDialog from "./OrderShippingMethodEditDialog"; describe("OrderShippingMethodEditDialog", () => { const shippingMethods = order("").shippingMethods!; + const mockShippingPrice = { + __typename: "TaxedMoney" as const, + gross: { + __typename: "Money" as const, + amount: 10, + currency: "USD", + }, + net: { + __typename: "Money" as const, + amount: 10, + currency: "USD", + }, + tax: { + __typename: "Money" as const, + amount: 0, + currency: "USD", + }, + }; + const defaultProps = { confirmButtonState: "default" as ConfirmButtonTransitionState, errors: [], open: true, shippingMethod: shippingMethods[1].id, shippingMethodName: shippingMethods[1].name, - shippingPrice: shippingMethods[1].price, + shippingPrice: mockShippingPrice, shippingMethods, onClose: jest.fn(), onSubmit: jest.fn(), From dd3ad2cde79541d02b759600c5aaf948d1429e65 Mon Sep 17 00:00:00 2001 From: Mirek Mencel Date: Wed, 10 Dec 2025 23:11:56 +0100 Subject: [PATCH 27/84] Add new unified OrderCaptureDialog with amount options - Create new OrderCaptureDialog component using macaw-ui-next - Supports three capture options: order total, authorized amount, custom - Shows contextual warnings when authorized differs from total - Integrate with both Legacy Payments API and Transactions API flows - Include development overrides for testing (to be removed) --- src/components/Callout/Callout.tsx | 100 +-- src/components/PriceField/utils.ts | 37 ++ .../OrderCaptureDialog/OrderCaptureDialog.tsx | 576 ++++++++++++++++++ .../components/OrderCaptureDialog/messages.ts | 146 +++++ .../OrderSummary/LegacyPaymentsApiButtons.tsx | 2 +- .../components/OrderSummary/OrderSummary.tsx | 3 + .../OrderDetails/OrderNormalDetails/index.tsx | 52 +- .../OrderUnconfirmedDetails/index.tsx | 52 +- 8 files changed, 907 insertions(+), 61 deletions(-) create mode 100644 src/orders/components/OrderCaptureDialog/OrderCaptureDialog.tsx create mode 100644 src/orders/components/OrderCaptureDialog/messages.ts diff --git a/src/components/Callout/Callout.tsx b/src/components/Callout/Callout.tsx index dbf42137d42..c4493ef5a8d 100644 --- a/src/components/Callout/Callout.tsx +++ b/src/components/Callout/Callout.tsx @@ -1,58 +1,66 @@ -import { ExclamationIcon } from "@dashboard/icons/ExclamationIcon"; -import { Box } from "@saleor/macaw-ui-next"; +import { iconSize, iconStrokeWidthBySize } from "@dashboard/components/icons"; +import { getStatusColor, PillStatusType } from "@dashboard/misc"; +import { Box, Text, useTheme } from "@saleor/macaw-ui-next"; +import { AlertTriangle, CircleAlert, Info, LucideIcon } from "lucide-react"; import { ReactNode } from "react"; -import { DashboardCard } from "../Card"; +type CalloutType = "info" | "warning" | "error"; -type CalloutType = "info" | "warning"; +interface CalloutStyles { + status: PillStatusType; + iconColor: "warning1" | "critical1" | "default1"; + Icon: LucideIcon; +} -const warningStylesBox = { - backgroundColor: "warning1", - borderColor: "warning1", -} as const; - -const warningStylesIcon = { - color: "warning1", -} as const; - -const gridTemplate = ` - "icon title" - "empty content" -`; +const calloutStylesMap: Record = { + warning: { + status: "warning", + iconColor: "warning1", + Icon: CircleAlert, + }, + error: { + status: "error", + iconColor: "critical1", + Icon: AlertTriangle, + }, + info: { + status: "neutral", + iconColor: "default1", + Icon: Info, + }, +}; -export const Callout = ({ - children, - title, - type, -}: { - children: ReactNode; +interface CalloutProps { + children?: ReactNode; title: ReactNode; type: CalloutType; -}) => { - const boxStyles = type === "warning" ? warningStylesBox : null; - const iconStyles = type === "warning" ? warningStylesIcon : null; +} + +export const Callout = ({ children, title, type }: CalloutProps): JSX.Element => { + const { theme: currentTheme } = useTheme(); + const { status, iconColor, Icon } = calloutStylesMap[type]; + const backgroundColor = getStatusColor({ status, currentTheme }).base; return ( - - - - - - {title} - - - {children} - - + + + + + {title} + {children && ( + + {children} + + )} + + ); }; diff --git a/src/components/PriceField/utils.ts b/src/components/PriceField/utils.ts index a966bd3073a..bcd41c3597a 100644 --- a/src/components/PriceField/utils.ts +++ b/src/components/PriceField/utils.ts @@ -32,3 +32,40 @@ export const getCurrencyDecimalPoints = (currency?: string) => { export const findPriceSeparator = (input: string) => SEPARATOR_CHARACTERS.find(separator => input.includes(separator)); + +/** + * Normalizes decimal separator to JavaScript standard (dot). + * Converts comma to dot for locales that use comma as decimal separator. + */ +export const normalizeDecimalSeparator = (value: string): string => value.replace(",", "."); + +/** + * Parses a decimal string value to a number, handling locale-specific separators. + * Returns 0 if the value cannot be parsed. + */ +export const parseDecimalValue = (value: string): number => + parseFloat(normalizeDecimalSeparator(value)) || 0; + +/** + * Limits decimal places in a string value, preserving the user's original separator. + * Useful for input validation while typing. + */ +export const limitDecimalPlaces = (value: string, maxDecimalPlaces: number): string => { + const normalized = normalizeDecimalSeparator(value); + const separator = value.includes(",") ? "," : "."; + const [integerPart, decimalPart] = normalized.split("."); + + if (!decimalPart) { + return value; + } + + if (maxDecimalPlaces === 0) { + return integerPart; + } + + if (decimalPart.length > maxDecimalPlaces) { + return `${integerPart}${separator}${decimalPart.slice(0, maxDecimalPlaces)}`; + } + + return value; +}; diff --git a/src/orders/components/OrderCaptureDialog/OrderCaptureDialog.tsx b/src/orders/components/OrderCaptureDialog/OrderCaptureDialog.tsx new file mode 100644 index 00000000000..b9523ac2571 --- /dev/null +++ b/src/orders/components/OrderCaptureDialog/OrderCaptureDialog.tsx @@ -0,0 +1,576 @@ +import BackButton from "@dashboard/components/BackButton"; +import { Callout } from "@dashboard/components/Callout/Callout"; +import { ConfirmButton, ConfirmButtonTransitionState } from "@dashboard/components/ConfirmButton"; +import { iconSize, iconStrokeWidthBySize } from "@dashboard/components/icons"; +import { DashboardModal } from "@dashboard/components/Modal"; +import Money from "@dashboard/components/Money"; +import { Pill } from "@dashboard/components/Pill"; +import { + getCurrencyDecimalPoints, + limitDecimalPlaces, + parseDecimalValue, +} from "@dashboard/components/PriceField/utils"; +import { OrderErrorFragment, TransactionRequestActionErrorFragment } from "@dashboard/graphql"; +import getOrderErrorMessage from "@dashboard/utils/errors/order"; +import { getOrderTransactionErrorMessage } from "@dashboard/utils/errors/transaction"; +import { IMoney } from "@dashboard/utils/intl"; +import { Box, Input, RadioGroup, Text } from "@saleor/macaw-ui-next"; +import { AlertTriangle, Box as BoxIcon, CheckCircle2, CircleAlert, CreditCard } from "lucide-react"; +import { ChangeEvent, useMemo, useState } from "react"; +import { FormattedMessage, useIntl } from "react-intl"; + +import { messages } from "./messages"; + +type CaptureError = OrderErrorFragment | TransactionRequestActionErrorFragment; + +const isTransactionError = ( + error: CaptureError, +): error is TransactionRequestActionErrorFragment => { + // TransactionRequestActionErrorFragment has a different __typename + return error.__typename === "TransactionRequestActionError"; +}; + +export type CaptureAmountOption = "orderTotal" | "custom"; + +type AuthorizationStatus = "full" | "partial" | "none" | "charged"; + +export interface OrderCaptureDialogProps { + open: boolean; + confirmButtonState: ConfirmButtonTransitionState; + orderTotal: IMoney; + authorizedAmount: IMoney; + /** Amount already charged/captured - used for display */ + chargedAmount?: IMoney; + /** + * Order's total balance (for multi-transaction orders). + * Negative = customer owes money, Positive = overpaid. + * When provided, used instead of (orderTotal - chargedAmount) for remaining calculation. + */ + orderBalance?: IMoney; + /** When true, shows "Transaction authorized" instead of "Authorized" */ + isTransaction?: boolean; + /** Server errors from the capture mutation (supports both Legacy and Transactions API errors) */ + errors?: CaptureError[]; + onClose: () => void; + onSubmit: (amount: number) => void; +} + +export const OrderCaptureDialog = ({ + open, + confirmButtonState, + orderTotal, + authorizedAmount, + chargedAmount, + orderBalance, + isTransaction: _isTransaction = false, + errors = [], + onClose, + onSubmit, +}: OrderCaptureDialogProps): JSX.Element => { + const intl = useIntl(); + + const totalAmount = orderTotal.amount; + const authAmount = authorizedAmount.amount; // Available to capture (bucket model) + const alreadyCharged = chargedAmount?.amount ?? 0; + const currency = orderTotal.currency; + + // With bucket model: authorizedAmount = what's available to capture + // (funds move from authorizedAmount to chargedAmount when captured) + const availableToCapture = authAmount; + + // Calculate what customer still owes: + // - If orderBalance provided (multi-transaction): use it (negative balance = owes money) + // - Otherwise: simple calculation from order total minus charged + const remainingToPay = orderBalance + ? Math.max(0, -orderBalance.amount) // Convert negative balance to positive amount owed + : totalAmount - alreadyCharged; + + // Order-wide captured amount (for display in Order section) + // = order total minus what's still owed + const orderTotalCaptured = totalAmount - remainingToPay; + + // Determine authorization status + const getAuthorizationStatus = (): AuthorizationStatus => { + // Check if fully charged first (nothing left to pay) + if (remainingToPay <= 0) { + return "charged"; + } + + if (availableToCapture <= 0) { + return "none"; + } + + if (availableToCapture >= remainingToPay) { + return "full"; + } + + return "partial"; + }; + + const authStatus = getAuthorizationStatus(); + const maxCapturable = Math.max(0, availableToCapture); + const canCaptureOrderTotal = availableToCapture >= remainingToPay && remainingToPay > 0; + const shortfall = remainingToPay - availableToCapture; + + // Default selection based on status + const getDefaultOption = (): CaptureAmountOption => { + // For full and partial, default to the first option + // For "none" or "charged" we can't select anything meaningful + if (authStatus === "full" || authStatus === "partial") { + return "orderTotal"; + } + + return "custom"; + }; + + const getDefaultCustomAmount = (): string => { + if (authStatus === "none" || authStatus === "charged") { + return "0"; + } + + if (authStatus === "partial") { + // Default to max capturable (remaining auth) + return String(availableToCapture); + } + + // Default to remaining amount to pay + return String(remainingToPay); + }; + + const [selectedOption, setSelectedOption] = useState(getDefaultOption); + const [customAmount, setCustomAmount] = useState(getDefaultCustomAmount); + + // Get max decimal places for this currency (e.g., 2 for USD, 0 for JPY, 3 for KWD) + const maxDecimalPlaces = useMemo(() => getCurrencyDecimalPoints(currency), [currency]); + + const handleCustomAmountChange = (e: ChangeEvent): void => { + const limitedValue = limitDecimalPlaces(e.target.value, maxDecimalPlaces); + + setCustomAmount(limitedValue); + }; + + const getSelectedAmount = (): number => { + switch (selectedOption) { + case "orderTotal": + // For partial auth, capture max available; for full, capture remaining balance + return authStatus === "partial" ? availableToCapture : remainingToPay; + case "custom": + return parseDecimalValue(customAmount); + } + }; + + const selectedAmount = getSelectedAmount(); + const customAmountValue = parseDecimalValue(customAmount); + const isCustomAmountInRange = customAmountValue > 0 && customAmountValue <= maxCapturable; + const isCustomAmountValid = selectedOption !== "custom" || isCustomAmountInRange; + const showCustomAmountError = + selectedOption === "custom" && + authStatus !== "none" && + authStatus !== "charged" && + !isCustomAmountInRange; + const canSubmit = + authStatus !== "none" && authStatus !== "charged" && isCustomAmountValid && selectedAmount > 0; + + const handleSubmit = (): void => { + if (canSubmit) { + onSubmit(selectedAmount); + } + }; + + const formatMoney = (amount: number): JSX.Element => ( + + + + ); + + // Calculate predicted outcome status after capture (order-wide) + type OutcomeStatus = "fullyCharged" | "partiallyCharged" | "overcharged"; + + const getOutcomeStatus = (): OutcomeStatus => { + const totalAfterCapture = orderTotalCaptured + selectedAmount; + + if (totalAfterCapture > totalAmount) { + return "overcharged"; + } else if (totalAfterCapture >= totalAmount) { + return "fullyCharged"; + } else { + return "partiallyCharged"; + } + }; + + const outcomeStatus = getOutcomeStatus(); + + const getStatusPill = (): JSX.Element => { + switch (authStatus) { + case "charged": + return ( + } + /> + ); + case "full": + return ( + } + /> + ); + case "partial": + return ( + } + /> + ); + case "none": + return ( + } + /> + ); + } + }; + + type AuthorizationColor = "success1" | "warning1" | "critical1"; + + const authStatusColorMap: Record = { + charged: "success1", + full: "success1", + partial: "warning1", + none: "critical1", + }; + + const authorizedAmountColor = authStatusColorMap[authStatus]; + + return ( + + + + + + {getStatusPill()} + + + + + {/* Summary box with order and payment sections */} + + {/* Order section */} + + + + + + + + + {formatMoney(totalAmount)} + + {orderTotalCaptured > 0 && ( + + + {/* Spacer to align with icon above */} + + + + + {formatMoney(orderTotalCaptured)} + + )} + + + {/* Spacer to align with icon above */} + + + + + {formatMoney(remainingToPay)} + + + + {/* Transaction section */} + + + + + + + + + + + {formatMoney(availableToCapture)} + + + {alreadyCharged > 0 && ( + + + {/* Spacer to align with icon above */} + + + + + {formatMoney(alreadyCharged)} + + )} + + + {/* Warning/Error messages */} + {authStatus === "partial" && ( + + {formatMoney(shortfall)}, + }} + /> + + } + /> + )} + + {authStatus === "none" && ( + + {formatMoney(remainingToPay)}, + }} + /> + + } + /> + )} + + + + {/* Radio options - primary section */} + + + + + + setSelectedOption(value)} + > + + {/* Order Total / Remaining Balance / Remaining Max option */} + + + + + 0 + ? messages.remainingBalance + : messages.optionOrderTotal)} + /> + + + + {formatMoney(authStatus === "partial" ? availableToCapture : remainingToPay)} + + + {canCaptureOrderTotal && ( + + + + + + )} + + + {/* Custom amount option */} + + + + + + + + + + + + + {currency} + + } + /> + + + + + + + + + + + + {/* Outcome prediction */} + {canSubmit && selectedAmount > 0 && ( + + + {outcomeStatus === "overcharged" && ( + + ), + }} + /> + )} + {outcomeStatus === "fullyCharged" && ( + + ), + }} + /> + )} + {outcomeStatus === "partiallyCharged" && ( + + ), + }} + /> + )} + + + )} + + {errors.length > 0 && ( + + {errors.map((error, index) => ( + + {isTransactionError(error) + ? getOrderTransactionErrorMessage(error, intl) + : getOrderErrorMessage(error, intl)} + + ))} + + )} + + + + + + + + + + ); +}; + +OrderCaptureDialog.displayName = "OrderCaptureDialog"; diff --git a/src/orders/components/OrderCaptureDialog/messages.ts b/src/orders/components/OrderCaptureDialog/messages.ts new file mode 100644 index 00000000000..21416d288c6 --- /dev/null +++ b/src/orders/components/OrderCaptureDialog/messages.ts @@ -0,0 +1,146 @@ +import { defineMessages } from "react-intl"; + +export const messages = defineMessages({ + title: { + id: "0f6YvV", + defaultMessage: "Capture Payment", + description: "dialog title", + }, + statusFullyAuthorized: { + id: "HwIhau", + defaultMessage: "Fully Authorized", + description: "status pill for fully authorized payment", + }, + statusPartial: { + id: "ZUYQ+C", + defaultMessage: "Partial authorisation", + description: "status pill for partial authorization", + }, + statusNoAuthorization: { + id: "mxGY7T", + defaultMessage: "No Authorization", + description: "status pill for no authorization", + }, + statusFullyCaptured: { + id: "L8J/jr", + defaultMessage: "Fully Captured", + description: "status pill when order is fully paid", + }, + orderTotal: { + id: "4YyeCx", + defaultMessage: "Order total", + description: "label for order total amount", + }, + authorized: { + id: "U0IK0G", + defaultMessage: "Authorized", + description: "label for authorized amount", + }, + transactionAuthorized: { + id: "H0eCbU", + defaultMessage: "Transaction authorized", + description: "label for transaction authorized amount", + }, + capturedSoFar: { + id: "0YOedO", + defaultMessage: "Captured so far", + description: "label for already charged amount", + }, + balanceDue: { + id: "qlfssi", + defaultMessage: "Balance due", + description: "label for remaining amount customer owes", + }, + availableToCapture: { + id: "MhlYkx", + defaultMessage: "Available to capture (authorized)", + description: "label for available authorization amount", + }, + transactionCaptured: { + id: "R/YHMH", + defaultMessage: "Already captured", + description: "label for amount already captured from this transaction", + }, + remainingBalance: { + id: "OUMqG1", + defaultMessage: "Remaining balance", + description: "label for remaining balance to capture", + }, + remainingMax: { + id: "jhyt3I", + defaultMessage: "Remaining max (authorized)", + description: "label for max capturable amount when partial authorization", + }, + selectAmount: { + id: "XrliJg", + defaultMessage: "Select amount to capture:", + description: "label for amount selection", + }, + optionOrderTotal: { + id: "tS2K/N", + defaultMessage: "Order total", + description: "radio option for capturing order total", + }, + optionOrderTotalHint: { + id: "v8e93p", + defaultMessage: "Matches what customer owes", + description: "hint for order total option", + }, + optionCustom: { + id: "IU1lif", + defaultMessage: "Custom amount", + description: "radio option for custom capture amount", + }, + customAmountMax: { + id: "Mm/Stj", + defaultMessage: "Max: {amount}", + description: "hint showing maximum allowed custom amount", + }, + captureButton: { + id: "bRXgSC", + defaultMessage: "Capture {amount}", + description: "capture button with amount", + }, + warningPartialAuthorization: { + id: "8JEG80", + defaultMessage: + "The remaining authorization doesn't cover the balance. {shortfall} will need a separate payment.", + description: "warning when authorized is less than total", + }, + errorNoAuthorization: { + id: "SnV3LR", + defaultMessage: + "No payment has been authorized for this order. The full amount of {amount} cannot be captured.", + description: "error when no authorization exists", + }, + outcomeFullyCharged: { + id: "VwCTbx", + defaultMessage: "This will result in a {status} order", + description: "outcome prediction when order will be fully charged", + }, + outcomePartiallyCharged: { + id: "ycg2RR", + defaultMessage: "This will result in a {status} order", + description: "outcome prediction when order will be partially charged", + }, + outcomeOvercharged: { + id: "DXaxpH", + defaultMessage: "This will result in an {status} order", + description: "outcome prediction when order will be overcharged", + }, + statusFullyCapturedPill: { + id: "G9y5Ze", + defaultMessage: "Fully captured", + description: "pill status for fully captured outcome", + }, + statusPartiallyCapturedPill: { + id: "BJRu4V", + defaultMessage: "Partially captured", + description: "pill status for partially captured outcome", + }, + statusOvercapturedPill: { + id: "u7ShY+", + defaultMessage: "Overcaptured", + description: "pill status for overcaptured outcome", + }, +}); diff --git a/src/orders/components/OrderSummary/LegacyPaymentsApiButtons.tsx b/src/orders/components/OrderSummary/LegacyPaymentsApiButtons.tsx index 0a974c3b9ca..9fe777dc7ec 100644 --- a/src/orders/components/OrderSummary/LegacyPaymentsApiButtons.tsx +++ b/src/orders/components/OrderSummary/LegacyPaymentsApiButtons.tsx @@ -24,7 +24,7 @@ export const LegacyPaymentsApiButtons = ({ onLegacyPaymentsApiCapture, onLegacyPaymentsApiRefund, onLegacyPaymentsApiVoid, -}: Props) => { +}: Props): JSX.Element | null => { const intl = useIntl(); const showButtons = diff --git a/src/orders/components/OrderSummary/OrderSummary.tsx b/src/orders/components/OrderSummary/OrderSummary.tsx index c8ba4b9a307..b0d19d953ed 100644 --- a/src/orders/components/OrderSummary/OrderSummary.tsx +++ b/src/orders/components/OrderSummary/OrderSummary.tsx @@ -78,6 +78,9 @@ export const OrderSummary = (props: Props) => { onLegacyPaymentsApiCapture: _onLegacyPaymentsApiCapture, onLegacyPaymentsApiRefund: _onLegacyPaymentsApiRefund, onLegacyPaymentsApiVoid: _onLegacyPaymentsApiVoid, + onTestCaptureFull: _onTestCaptureFull, + onTestCapturePartial: _onTestCapturePartial, + onTestCaptureNone: _onTestCaptureNone, ...boxProps } = restProps as any; diff --git a/src/orders/views/OrderDetails/OrderNormalDetails/index.tsx b/src/orders/views/OrderDetails/OrderNormalDetails/index.tsx index 0315f611185..218c30de4f0 100644 --- a/src/orders/views/OrderDetails/OrderNormalDetails/index.tsx +++ b/src/orders/views/OrderDetails/OrderNormalDetails/index.tsx @@ -15,6 +15,7 @@ import { OrderTransactionRequestActionMutationVariables, OrderUpdateMutation, OrderUpdateMutationVariables, + TransactionActionEnum, useCustomerAddressesQuery, useWarehouseListQuery, } from "@dashboard/graphql"; @@ -53,11 +54,11 @@ import { customerUrl } from "../../../../customers/urls"; import { productUrl } from "../../../../products/urls"; import OrderAddressFields from "../../../components/OrderAddressFields/OrderAddressFields"; import OrderCancelDialog from "../../../components/OrderCancelDialog"; +import { OrderCaptureDialog } from "../../../components/OrderCaptureDialog/OrderCaptureDialog"; import OrderDetailsPage from "../../../components/OrderDetailsPage/OrderDetailsPage"; import OrderFulfillmentCancelDialog from "../../../components/OrderFulfillmentCancelDialog"; import OrderFulfillmentTrackingDialog from "../../../components/OrderFulfillmentTrackingDialog"; import OrderMarkAsPaidDialog from "../../../components/OrderMarkAsPaidDialog/OrderMarkAsPaidDialog"; -import OrderPaymentDialog from "../../../components/OrderPaymentDialog"; import OrderPaymentVoidDialog from "../../../components/OrderPaymentVoidDialog"; import { orderFulfillUrl, @@ -303,10 +304,46 @@ export const OrderNormalDetails = ({ }) } /> + {/* Transaction Capture Dialog - for CHARGE action */} + {params.action === "transaction-action" && params.type === TransactionActionEnum.CHARGE && ( + t.id === params.id)?.authorizedAmount ?? { + amount: 0, + currency: "USD", + } + } + chargedAmount={ + order?.transactions?.find(t => t.id === params.id)?.chargedAmount ?? { + amount: 0, + currency: "USD", + } + } + orderBalance={order?.totalBalance ?? { amount: 0, currency: "USD" }} + isTransaction + open={true} + onClose={closeModal} + onSubmit={amount => + orderTransactionAction + .mutate({ + action: params.type, + transactionId: params.id, + amount, + }) + .finally(() => closeModal()) + } + /> + )} + {/* Transaction Action Dialog - for other actions like CANCEL */} orderTransactionAction @@ -354,15 +391,16 @@ export const OrderNormalDetails = ({ onClose={closeModal} onConfirm={() => orderVoid.mutate({ id })} /> - + onSubmit={amount => orderPaymentCapture.mutate({ - ...variables, + amount, id, }) } diff --git a/src/orders/views/OrderDetails/OrderUnconfirmedDetails/index.tsx b/src/orders/views/OrderDetails/OrderUnconfirmedDetails/index.tsx index c5fab507284..2b249a7b740 100644 --- a/src/orders/views/OrderDetails/OrderUnconfirmedDetails/index.tsx +++ b/src/orders/views/OrderDetails/OrderUnconfirmedDetails/index.tsx @@ -14,6 +14,7 @@ import { OrderTransactionRequestActionMutationVariables, OrderUpdateMutation, OrderUpdateMutationVariables, + TransactionActionEnum, useCustomerAddressesQuery, useWarehouseListQuery, } from "@dashboard/graphql"; @@ -46,11 +47,11 @@ import { import { productUrl } from "../../../../products/urls"; import OrderAddressFields from "../../../components/OrderAddressFields/OrderAddressFields"; import OrderCancelDialog from "../../../components/OrderCancelDialog"; +import { OrderCaptureDialog } from "../../../components/OrderCaptureDialog/OrderCaptureDialog"; import OrderDetailsPage from "../../../components/OrderDetailsPage/OrderDetailsPage"; import OrderFulfillmentCancelDialog from "../../../components/OrderFulfillmentCancelDialog"; import OrderFulfillmentTrackingDialog from "../../../components/OrderFulfillmentTrackingDialog"; import OrderMarkAsPaidDialog from "../../../components/OrderMarkAsPaidDialog/OrderMarkAsPaidDialog"; -import OrderPaymentDialog from "../../../components/OrderPaymentDialog"; import OrderPaymentVoidDialog from "../../../components/OrderPaymentVoidDialog"; import OrderProductAddDialog from "../../../components/OrderProductAddDialog"; import OrderShippingMethodEditDialog from "../../../components/OrderShippingMethodEditDialog"; @@ -366,10 +367,46 @@ export const OrderUnconfirmedDetails = ({ transactionReference={transactionReference} handleTransactionReference={({ target }) => setTransactionReference(target.value)} /> + {/* Transaction Capture Dialog - for CHARGE action */} + {params.action === "transaction-action" && params.type === TransactionActionEnum.CHARGE && ( + t.id === params.id)?.authorizedAmount ?? { + amount: 0, + currency: "USD", + } + } + chargedAmount={ + order?.transactions?.find(t => t.id === params.id)?.chargedAmount ?? { + amount: 0, + currency: "USD", + } + } + orderBalance={order?.totalBalance ?? { amount: 0, currency: "USD" }} + isTransaction + open={true} + onClose={closeModal} + onSubmit={amount => + orderTransactionAction + .mutate({ + action: params.type, + transactionId: params.id, + amount, + }) + .finally(() => closeModal()) + } + /> + )} + {/* Transaction Action Dialog - for other actions like CANCEL */} orderTransactionAction @@ -398,15 +435,16 @@ export const OrderUnconfirmedDetails = ({ onClose={closeModal} onConfirm={() => orderVoid.mutate({ id })} /> - + onSubmit={amount => orderPaymentCapture.mutate({ - ...variables, + amount, id, }) } From e3343271ee5b92239f8b8084c77504b30c3543af Mon Sep 17 00:00:00 2001 From: Mirek Mencel Date: Fri, 12 Dec 2025 14:00:33 +0100 Subject: [PATCH 28/84] Add changeset --- .changeset/mighty-toes-march.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/mighty-toes-march.md diff --git a/.changeset/mighty-toes-march.md b/.changeset/mighty-toes-march.md new file mode 100644 index 00000000000..4ddcd95b7c3 --- /dev/null +++ b/.changeset/mighty-toes-march.md @@ -0,0 +1,5 @@ +--- +"saleor-dashboard": patch +--- + +Introduce new, unified order capture dialog From 17546f517fb04682e7747cbad42b22ae31af846f Mon Sep 17 00:00:00 2001 From: Mirek Mencel Date: Fri, 12 Dec 2025 14:01:50 +0100 Subject: [PATCH 29/84] Extract messages --- locale/defaultMessages.json | 112 ++++++++++++++++++++++++++++++++++++ 1 file changed, 112 insertions(+) diff --git a/locale/defaultMessages.json b/locale/defaultMessages.json index 0acb6a0fef5..72e426ddacc 100644 --- a/locale/defaultMessages.json +++ b/locale/defaultMessages.json @@ -430,6 +430,10 @@ "context": "column header", "string": "Title" }, + "0YOedO": { + "context": "label for already charged amount", + "string": "Captured so far" + }, "0YjGFG": { "context": "alert message", "string": "For subscription" @@ -449,6 +453,10 @@ "context": "ProductTypeDeleteWarningDialog multiple consent label", "string": "Yes, I want to delete those products types and assigned products" }, + "0f6YvV": { + "context": "dialog title", + "string": "Capture Payment" + }, "0iMYc+": { "context": "field is optional", "string": "(Optional)" @@ -1025,6 +1033,10 @@ "4YJHut": { "string": "Clear search" }, + "4YyeCx": { + "context": "label for order total amount", + "string": "Order total" + }, "4Z0O2B": { "context": "section header title", "string": "Gift Card Timeline" @@ -1593,6 +1605,10 @@ "context": "ordered product sku", "string": "SKU" }, + "8JEG80": { + "context": "warning when authorized is less than total", + "string": "The remaining authorization doesn't cover the balance. {shortfall} will need a separate payment." + }, "8LWaFr": { "context": "dialog content", "string": "{counter,plural,one{Are you sure you want to unpublish this model?} other{Are you sure you want to unpublish {displayQuantity} models?}}" @@ -2046,6 +2062,10 @@ "context": "no warehouses info", "string": "There are no warehouses set up for this product. To add stock quantity to the product configure a warehouse or use existing one by clicking button below." }, + "BJRu4V": { + "context": "pill status for partially captured outcome", + "string": "Partially captured" + }, "BJtUQI": { "context": "button", "string": "Add" @@ -2424,6 +2444,10 @@ "DWWw3M": { "string": "Model type Name" }, + "DXaxpH": { + "context": "outcome prediction when order will be overcharged", + "string": "This will result in an {status} order" + }, "DaPGcn": { "string": "Model title" }, @@ -2828,6 +2852,10 @@ "context": "tooltip", "string": "Checkout reservation time threshold is enabled in settings." }, + "G9y5Ze": { + "context": "pill status for fully captured outcome", + "string": "Fully captured" + }, "GA+Djy": { "string": "Are you sure you want to delete these voucher codes?" }, @@ -3014,6 +3042,10 @@ "context": "dialog content", "string": "You are not able to modify this group members. Solve this problem to continue with request." }, + "H0eCbU": { + "context": "label for transaction authorized amount", + "string": "Transaction authorized" + }, "H1L1cc": { "context": "url", "string": "URL" @@ -3144,6 +3176,10 @@ "HvJPcU": { "string": "Category deleted" }, + "HwIhau": { + "context": "status pill for fully authorized payment", + "string": "Fully Authorized" + }, "HwTMFL": { "string": "Go to channels" }, @@ -3241,6 +3277,10 @@ "ITYiRy": { "string": "Go to collections" }, + "IU1lif": { + "context": "radio option for custom capture amount", + "string": "Custom amount" + }, "IUeGzv": { "context": "plugin name", "string": "Plugin Name" @@ -3656,6 +3696,10 @@ "L87bp7": { "string": "Order payment successfully voided" }, + "L8J/jr": { + "context": "status pill when order is fully paid", + "string": "Fully Captured" + }, "L8seEc": { "string": "Subtotal" }, @@ -3874,10 +3918,18 @@ "context": "customer input label", "string": "Customer" }, + "MhlYkx": { + "context": "label for available authorization amount", + "string": "Available to capture (authorized)" + }, "MjUyhA": { "context": "section subheader", "string": "Active member since {date}" }, + "Mm/Stj": { + "context": "hint showing maximum allowed custom amount", + "string": "Max: {amount}" + }, "MmGlkp": { "context": "dialog header", "string": "Unassign Collections From Voucher" @@ -4166,6 +4218,10 @@ "context": "tab name", "string": "All staff members" }, + "OUMqG1": { + "context": "label for remaining balance to capture", + "string": "Remaining balance" + }, "OUX4LB": { "context": "input label", "string": "Model type" @@ -4589,6 +4645,10 @@ "context": "add new refund button", "string": "New refund" }, + "R/YHMH": { + "context": "label for amount already captured from this transaction", + "string": "Already captured" + }, "R4IIw1": { "context": "tracking number of the shipment", "string": "Tracking number" @@ -4844,6 +4904,10 @@ "context": "product filter label", "string": "Product" }, + "SnV3LR": { + "context": "error when no authorization exists", + "string": "No payment has been authorized for this order. The full amount of {amount} cannot be captured." + }, "SpngiS": { "context": "sale status", "string": "Status" @@ -5037,6 +5101,10 @@ "context": "order expiration card description", "string": "The time in days after expired orders will be deleted. Allowed range between 1 and 120." }, + "U0IK0G": { + "context": "label for authorized amount", + "string": "Authorized" + }, "U1eJIw": { "context": "order history message", "string": "Products were added to an order" @@ -5349,6 +5417,10 @@ "context": "command menu shortcut", "string": "Command menu" }, + "VwCTbx": { + "context": "outcome prediction when order will be fully charged", + "string": "This will result in a {status} order" + }, "VyC+Bm": { "context": "radio button label", "string": "Enter the refund amount without using the product list. Best for overcharges and custom adjustments." @@ -5690,6 +5762,10 @@ "context": "product pricing, section header", "string": "Pricing" }, + "XrliJg": { + "context": "label for amount selection", + "string": "Select amount to capture:" + }, "Xsh2Pa": { "context": "column picker search input placeholder", "string": "Search for columns" @@ -5972,6 +6048,10 @@ "context": "empty state message", "string": "No refunds made for this order." }, + "ZUYQ+C": { + "context": "status pill for partial authorization", + "string": "Partial authorisation" + }, "ZXOpCJ": { "string": "An unexpected issue occurred when parsing manifest. Please contact support. ({errorCode})" }, @@ -6285,6 +6365,10 @@ "context": "button", "string": "Create permission group" }, + "bRXgSC": { + "context": "capture button with amount", + "string": "Capture {amount}" + }, "bS7A8u": { "context": "add tracking button", "string": "Add tracking" @@ -7472,6 +7556,10 @@ "context": "product price", "string": "Select channel" }, + "jhyt3I": { + "context": "label for max capturable amount when partial authorization", + "string": "Remaining max (authorized)" + }, "jiXbx5": { "context": "fulfillment status canceled", "string": "Canceled" @@ -8013,6 +8101,10 @@ "mvVmbJ": { "string": "Install extension from manifest" }, + "mxGY7T": { + "context": "status pill for no authorization", + "string": "No Authorization" + }, "mxtAFx": { "string": "Are you sure you want to delete draft #{orderNumber}?" }, @@ -8634,6 +8726,10 @@ "context": "bulk delete label", "string": "Delete" }, + "qlfssi": { + "context": "label for remaining amount customer owes", + "string": "Balance due" + }, "qov29K": { "context": "dialog content", "string": "Select one of customer addresses or add a new address:" @@ -9008,6 +9104,10 @@ "tR+UuE": { "string": "User doesn't exist. Please check your email in URL" }, + "tS2K/N": { + "context": "radio option for capturing order total", + "string": "Order total" + }, "tTuCYj": { "context": "all gift cards label", "string": "All Gift Cards" @@ -9124,6 +9224,10 @@ "context": "shipping zones configuration", "string": "Change default weight unit" }, + "u7ShY+": { + "context": "pill status for overcaptured outcome", + "string": "Overcaptured" + }, "u9/vj9": { "context": "webhook input label", "string": "Target URL" @@ -9307,6 +9411,10 @@ "v3WWK+": { "string": "Status is invalid" }, + "v8e93p": { + "context": "hint for order total option", + "string": "Matches what customer owes" + }, "vAxm7u": { "string": "You must select a channel first and select at least one gift" }, @@ -9833,6 +9941,10 @@ "context": "warehouses section name", "string": "Warehouses" }, + "ycg2RR": { + "context": "outcome prediction when order will be partially charged", + "string": "This will result in a {status} order" + }, "ychKsb": { "context": "error message", "string": "Shipping method is required for this order" From 71ac249568793d8cd379af300b1aa4201d513b8b Mon Sep 17 00:00:00 2001 From: Mirek Mencel Date: Fri, 12 Dec 2025 14:17:20 +0100 Subject: [PATCH 30/84] Fix Callout tests to respect ThemeProvider --- .../AddCustomExtension.test.tsx | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/src/extensions/views/AddCustomExtension/AddCustomExtension.test.tsx b/src/extensions/views/AddCustomExtension/AddCustomExtension.test.tsx index f0818f0815e..7a2a0b79487 100644 --- a/src/extensions/views/AddCustomExtension/AddCustomExtension.test.tsx +++ b/src/extensions/views/AddCustomExtension/AddCustomExtension.test.tsx @@ -1,3 +1,4 @@ +import { ThemeProvider } from "@saleor/macaw-ui-next"; import { render, screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import * as React from "react"; @@ -8,6 +9,10 @@ import { usePermissions } from "./hooks/usePermissions"; import { useUserAppCreationPermissions } from "./hooks/useUserAppCreationPermissions"; import { useUserPermissionSet } from "./hooks/useUserPermissionMap"; +const Wrapper = ({ children }: React.PropsWithChildren<{}>) => ( + {children} +); + // Mock ResizeObserver used by Radix checkbox class ResizeObserverMock { observe() { @@ -61,7 +66,7 @@ describe("AddCustomExtension", () => { it("renders the component with all required elements", () => { // Arrange - render(); + render(, { wrapper: Wrapper }); // Assert expect(screen.getByPlaceholderText("Extension Name")).toBeInTheDocument(); @@ -73,7 +78,7 @@ describe("AddCustomExtension", () => { it("displays validation error when submitting empty form", async () => { // Arrange - render(); + render(, { wrapper: Wrapper }); // Act await userEvent.click(screen.getByText("save")); @@ -84,7 +89,7 @@ describe("AddCustomExtension", () => { it("creates app without permissions", async () => { // Arrange - render(); + render(, { wrapper: Wrapper }); const appNameInput = screen.getByPlaceholderText("Extension Name"); @@ -107,7 +112,7 @@ describe("AddCustomExtension", () => { it("creates app with some permissions when checked by user", async () => { // Arrange - render(); + render(, { wrapper: Wrapper }); const appNameInput = screen.getByPlaceholderText("Extension Name"); const ordersCheckbox = screen.getByLabelText(/Manage Orders/i); @@ -135,7 +140,7 @@ describe("AddCustomExtension", () => { it("creates app with all permissions when toggled 'Grant full access'", async () => { // Arrange - render(); + render(, { wrapper: Wrapper }); const appNameInput = screen.getByPlaceholderText("Extension Name"); const fullAccessCheckbox = screen.getByRole("checkbox", { @@ -166,7 +171,7 @@ describe("AddCustomExtension", () => { it("creates app with no permissions when toggling between 'Grant full access'", async () => { // Arrange - render(); + render(, { wrapper: Wrapper }); const appNameInput = screen.getByPlaceholderText("Extension Name"); const fullAccessCheckbox = screen.getByRole("checkbox", { @@ -211,7 +216,7 @@ describe("AddCustomExtension", () => { (useUserAppCreationPermissions as jest.Mock).mockReturnValue(true); // Act - render(); + render(, { wrapper: Wrapper }); // Assert expect(screen.getByText(/warning/i)).toBeInTheDocument(); @@ -225,7 +230,7 @@ describe("AddCustomExtension", () => { const availablePermissions = new Set(["MANAGE_ORDERS"]); (useUserPermissionSet as jest.Mock).mockReturnValue(availablePermissions); - render(); + render(, { wrapper: Wrapper }); const appNameInput = screen.getByPlaceholderText("Extension Name"); const ordersCheckbox = screen.getByLabelText(/Manage Orders/i); From 86b7f7c499993bf7ab70f7dc7261ff5910fc1286 Mon Sep 17 00:00:00 2001 From: Mirek Mencel Date: Fri, 12 Dec 2025 14:20:29 +0100 Subject: [PATCH 31/84] Add essential PriceField util tests --- src/components/PriceField/utils.test.ts | 113 ++++++++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 src/components/PriceField/utils.test.ts diff --git a/src/components/PriceField/utils.test.ts b/src/components/PriceField/utils.test.ts new file mode 100644 index 00000000000..3fde43f2ffd --- /dev/null +++ b/src/components/PriceField/utils.test.ts @@ -0,0 +1,113 @@ +import { + getCurrencyDecimalPoints, + limitDecimalPlaces, + normalizeDecimalSeparator, + parseDecimalValue, +} from "./utils"; + +describe("normalizeDecimalSeparator", () => { + it("converts comma to dot", () => { + expect(normalizeDecimalSeparator("10,50")).toBe("10.50"); + }); + + it("leaves dot unchanged", () => { + expect(normalizeDecimalSeparator("10.50")).toBe("10.50"); + }); + + it("handles integers without separator", () => { + expect(normalizeDecimalSeparator("100")).toBe("100"); + }); + + it("handles empty string", () => { + expect(normalizeDecimalSeparator("")).toBe(""); + }); +}); + +describe("parseDecimalValue", () => { + it("parses dot-separated value", () => { + expect(parseDecimalValue("10.50")).toBe(10.5); + }); + + it("parses comma-separated value", () => { + expect(parseDecimalValue("10,50")).toBe(10.5); + }); + + it("parses integer", () => { + expect(parseDecimalValue("100")).toBe(100); + }); + + it("returns 0 for empty string", () => { + expect(parseDecimalValue("")).toBe(0); + }); + + it("returns 0 for invalid input", () => { + expect(parseDecimalValue("abc")).toBe(0); + }); + + it("handles negative values", () => { + expect(parseDecimalValue("-10.50")).toBe(-10.5); + }); +}); + +describe("limitDecimalPlaces", () => { + it("limits decimal places with dot separator", () => { + expect(limitDecimalPlaces("10.12345", 2)).toBe("10.12"); + }); + + it("limits decimal places with comma separator", () => { + expect(limitDecimalPlaces("10,12345", 2)).toBe("10,12"); + }); + + it("preserves original separator when limiting", () => { + expect(limitDecimalPlaces("10,999", 2)).toBe("10,99"); + expect(limitDecimalPlaces("10.999", 2)).toBe("10.99"); + }); + + it("returns integer when maxDecimalPlaces is 0", () => { + expect(limitDecimalPlaces("10.50", 0)).toBe("10"); + expect(limitDecimalPlaces("10,50", 0)).toBe("10"); + }); + + it("returns value unchanged if decimal places are within limit", () => { + expect(limitDecimalPlaces("10.12", 2)).toBe("10.12"); + expect(limitDecimalPlaces("10.1", 2)).toBe("10.1"); + }); + + it("returns value unchanged if no decimal part", () => { + expect(limitDecimalPlaces("100", 2)).toBe("100"); + }); + + it("handles three decimal places for currencies like KWD", () => { + expect(limitDecimalPlaces("10.1234", 3)).toBe("10.123"); + }); + + it("handles zero decimal places for currencies like JPY", () => { + expect(limitDecimalPlaces("1000.99", 0)).toBe("1000"); + }); +}); + +describe("getCurrencyDecimalPoints", () => { + it("returns 2 for USD", () => { + expect(getCurrencyDecimalPoints("USD")).toBe(2); + }); + + it("returns 2 for EUR", () => { + expect(getCurrencyDecimalPoints("EUR")).toBe(2); + }); + + it("returns 0 for JPY (Japanese Yen)", () => { + expect(getCurrencyDecimalPoints("JPY")).toBe(0); + }); + + it("returns 3 for KWD (Kuwaiti Dinar)", () => { + expect(getCurrencyDecimalPoints("KWD")).toBe(3); + }); + + it("returns 2 as fallback for undefined currency", () => { + expect(getCurrencyDecimalPoints(undefined)).toBe(2); + }); + + it("returns 2 as fallback for invalid currency code", () => { + expect(getCurrencyDecimalPoints("INVALID")).toBe(2); + }); +}); From 6bfcc7c5cb263c2dc9bf7c9373d2cad5456b33a8 Mon Sep 17 00:00:00 2001 From: Mirek Mencel Date: Fri, 12 Dec 2025 14:33:26 +0100 Subject: [PATCH 32/84] Add dialog tests --- .../OrderCaptureDialog.test.tsx | 474 ++++++++++++++++++ 1 file changed, 474 insertions(+) create mode 100644 src/orders/components/OrderCaptureDialog/OrderCaptureDialog.test.tsx diff --git a/src/orders/components/OrderCaptureDialog/OrderCaptureDialog.test.tsx b/src/orders/components/OrderCaptureDialog/OrderCaptureDialog.test.tsx new file mode 100644 index 00000000000..171c5b61da6 --- /dev/null +++ b/src/orders/components/OrderCaptureDialog/OrderCaptureDialog.test.tsx @@ -0,0 +1,474 @@ +import { ConfirmButtonTransitionState } from "@dashboard/components/ConfirmButton"; +import { OrderErrorCode, OrderErrorFragment } from "@dashboard/graphql"; +import { IMoney } from "@dashboard/utils/intl"; +import Wrapper from "@test/wrapper"; +import { fireEvent, render, screen, within } from "@testing-library/react"; + +import { OrderCaptureDialog, OrderCaptureDialogProps } from "./OrderCaptureDialog"; + +const createMoney = (amount: number, currency = "USD"): IMoney => ({ + amount, + currency, +}); + +const defaultProps: OrderCaptureDialogProps = { + open: true, + confirmButtonState: "default" as ConfirmButtonTransitionState, + orderTotal: createMoney(100), + authorizedAmount: createMoney(100), + onClose: jest.fn(), + onSubmit: jest.fn(), +}; + +const renderDialog = (props: Partial = {}) => + render( + + + , + ); + +describe("OrderCaptureDialog", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe("rendering", () => { + it("renders the dialog when open is true", () => { + // Arrange & Act + renderDialog(); + + // Assert + expect(screen.getByRole("dialog")).toBeInTheDocument(); + expect(screen.getByText("Capture Payment")).toBeInTheDocument(); + }); + + it("does not render the dialog when open is false", () => { + // Arrange & Act + renderDialog({ open: false }); + + // Assert + expect(screen.queryByRole("dialog")).not.toBeInTheDocument(); + }); + + it("displays order total label", () => { + // Arrange & Act + renderDialog({ orderTotal: createMoney(150) }); + + // Assert + expect(screen.getByText("Order total")).toBeInTheDocument(); + }); + + it("displays available to capture label", () => { + // Arrange & Act + renderDialog({ authorizedAmount: createMoney(80) }); + + // Assert + expect(screen.getByText("Available to capture (authorized)")).toBeInTheDocument(); + }); + }); + + describe("authorization status", () => { + it("shows 'Fully Authorized' pill when authorized >= remaining", () => { + // Arrange & Act + renderDialog({ + orderTotal: createMoney(100), + authorizedAmount: createMoney(100), + }); + + // Assert + expect(screen.getByText("Fully Authorized")).toBeInTheDocument(); + }); + + it("shows 'Partial authorisation' pill when authorized < remaining", () => { + // Arrange & Act + renderDialog({ + orderTotal: createMoney(100), + authorizedAmount: createMoney(50), + }); + + // Assert + expect(screen.getByText("Partial authorisation")).toBeInTheDocument(); + }); + + it("shows warning callout for partial authorization with shortfall", () => { + // Arrange & Act + renderDialog({ + orderTotal: createMoney(100), + authorizedAmount: createMoney(50), + }); + + // Assert + expect( + screen.getByText(/The remaining authorization doesn't cover the balance/), + ).toBeInTheDocument(); + }); + + it("shows 'No Authorization' pill when authorized is 0", () => { + // Arrange & Act + renderDialog({ + orderTotal: createMoney(100), + authorizedAmount: createMoney(0), + }); + + // Assert + expect(screen.getByText("No Authorization")).toBeInTheDocument(); + }); + + it("shows error callout when no authorization", () => { + // Arrange & Act + renderDialog({ + orderTotal: createMoney(100), + authorizedAmount: createMoney(0), + }); + + // Assert + expect(screen.getByText(/No payment has been authorized for this order/)).toBeInTheDocument(); + }); + + it("shows 'Fully Captured' pill when order is fully paid", () => { + // Arrange & Act + renderDialog({ + orderTotal: createMoney(100), + authorizedAmount: createMoney(0), + chargedAmount: createMoney(100), + }); + + // Assert + expect(screen.getByText("Fully Captured")).toBeInTheDocument(); + }); + }); + + describe("radio options", () => { + it("defaults to first option for full authorization", () => { + // Arrange & Act + renderDialog({ + orderTotal: createMoney(100), + authorizedAmount: createMoney(100), + }); + + // Assert + const radioGroup = screen.getByRole("radiogroup"); + const selectedRadio = within(radioGroup).getByRole("radio", { checked: true }); + + expect(selectedRadio).toHaveAttribute("value", "orderTotal"); + }); + + it("shows 'Remaining max (authorized)' label for partial authorization", () => { + // Arrange & Act + renderDialog({ + orderTotal: createMoney(100), + authorizedAmount: createMoney(50), + }); + + // Assert + expect(screen.getByText("Remaining max (authorized)")).toBeInTheDocument(); + }); + + it("disables radio options when no authorization", () => { + // Arrange & Act + renderDialog({ + orderTotal: createMoney(100), + authorizedAmount: createMoney(0), + }); + + // Assert + const radioGroup = screen.getByRole("radiogroup"); + const radios = within(radioGroup).getAllByRole("radio"); + + radios.forEach(radio => { + expect(radio).toBeDisabled(); + }); + }); + + it("allows selecting custom amount option", async () => { + // Arrange + renderDialog(); + + // Act + const customRadio = screen.getByRole("radio", { name: /Custom amount/i }); + + fireEvent.click(customRadio); + + // Assert + expect(customRadio).toBeChecked(); + }); + }); + + describe("custom amount input", () => { + const getCustomInput = () => { + const dialog = screen.getByRole("dialog"); + + return dialog.querySelector('input[type="text"]') as HTMLInputElement; + }; + + it("is disabled when custom option is not selected", () => { + // Arrange & Act + renderDialog(); + + // Assert + const input = getCustomInput(); + + expect(input).toBeDisabled(); + }); + + it("is enabled when custom option is selected", () => { + // Arrange + renderDialog(); + + // Act + const customRadio = screen.getByRole("radio", { name: /Custom amount/i }); + + fireEvent.click(customRadio); + + // Assert + const input = getCustomInput(); + + expect(input).not.toBeDisabled(); + }); + + it("shows max capturable hint", () => { + // Arrange & Act + renderDialog({ + authorizedAmount: createMoney(75), + }); + + // Assert + expect(screen.getByText(/Max:/)).toBeInTheDocument(); + }); + + it("accepts valid custom amount input", () => { + // Arrange + renderDialog(); + + const customRadio = screen.getByRole("radio", { name: /Custom amount/i }); + + fireEvent.click(customRadio); + + // Act + const input = getCustomInput(); + + fireEvent.change(input, { target: { value: "50" } }); + + // Assert + expect(input).toHaveValue("50"); + }); + + it("limits decimal places based on currency", () => { + // Arrange + renderDialog(); + + const customRadio = screen.getByRole("radio", { name: /Custom amount/i }); + + fireEvent.click(customRadio); + + // Act + const input = getCustomInput(); + + fireEvent.change(input, { target: { value: "50.999" } }); + + // Assert - USD has 2 decimal places + expect(input).toHaveValue("50.99"); + }); + }); + + describe("submit button", () => { + it("shows capture amount in button text", () => { + // Arrange & Act + renderDialog({ + orderTotal: createMoney(100), + authorizedAmount: createMoney(100), + }); + + // Assert + expect(screen.getByRole("button", { name: /Capture/i })).toBeInTheDocument(); + }); + + it("calls onSubmit with correct amount for full authorization", () => { + // Arrange + const onSubmit = jest.fn(); + + renderDialog({ + orderTotal: createMoney(100), + authorizedAmount: createMoney(100), + onSubmit, + }); + + // Act + const captureButton = screen.getByRole("button", { name: /Capture/i }); + + fireEvent.click(captureButton); + + // Assert + expect(onSubmit).toHaveBeenCalledWith(100); + }); + + it("calls onSubmit with max available for partial authorization", () => { + // Arrange + const onSubmit = jest.fn(); + + renderDialog({ + orderTotal: createMoney(100), + authorizedAmount: createMoney(50), + onSubmit, + }); + + // Act + const captureButton = screen.getByRole("button", { name: /Capture/i }); + + fireEvent.click(captureButton); + + // Assert + expect(onSubmit).toHaveBeenCalledWith(50); + }); + + it("calls onSubmit with custom amount when selected", () => { + // Arrange + const onSubmit = jest.fn(); + + renderDialog({ onSubmit }); + + const customRadio = screen.getByRole("radio", { name: /Custom amount/i }); + + fireEvent.click(customRadio); + + const dialog = screen.getByRole("dialog"); + const input = dialog.querySelector('input[type="text"]') as HTMLInputElement; + + fireEvent.change(input, { target: { value: "25" } }); + + // Act + const captureButton = screen.getByRole("button", { name: /Capture/i }); + + fireEvent.click(captureButton); + + // Assert + expect(onSubmit).toHaveBeenCalledWith(25); + }); + + it("is disabled when no authorization", () => { + // Arrange & Act + renderDialog({ + orderTotal: createMoney(100), + authorizedAmount: createMoney(0), + }); + + // Assert + const captureButton = screen.getByRole("button", { name: /Capture/i }); + + expect(captureButton).toBeDisabled(); + }); + + it("is disabled when custom amount exceeds max", () => { + // Arrange + renderDialog({ + authorizedAmount: createMoney(50), + }); + + const customRadio = screen.getByRole("radio", { name: /Custom amount/i }); + + fireEvent.click(customRadio); + + const dialog = screen.getByRole("dialog"); + const input = dialog.querySelector('input[type="text"]') as HTMLInputElement; + + fireEvent.change(input, { target: { value: "100" } }); + + // Assert + const captureButton = screen.getByRole("button", { name: /Capture/i }); + + expect(captureButton).toBeDisabled(); + }); + }); + + describe("close button", () => { + it("calls onClose when back button is clicked", () => { + // Arrange + const onClose = jest.fn(); + + renderDialog({ onClose }); + + // Act + const backButton = screen.getByRole("button", { name: /back/i }); + + fireEvent.click(backButton); + + // Assert + expect(onClose).toHaveBeenCalled(); + }); + }); + + describe("error handling", () => { + it("displays error messages when provided", () => { + // Arrange + const errors = [ + { + __typename: "OrderError" as const, + code: OrderErrorCode.CAPTURE_INACTIVE_PAYMENT, + field: null, + addressType: null, + message: null, + orderLines: null, + }, + ] as OrderErrorFragment[]; + + // Act + renderDialog({ errors }); + + // Assert + // The error message will be rendered by getOrderErrorMessage utility + const dialog = screen.getByRole("dialog"); + + expect(dialog).toBeInTheDocument(); + }); + }); + + describe("outcome prediction", () => { + it("shows outcome message when capturing full balance", () => { + // Arrange & Act + renderDialog({ + orderTotal: createMoney(100), + authorizedAmount: createMoney(100), + }); + + // Assert - outcome prediction message is shown + expect(screen.getByText(/This will result in a/)).toBeInTheDocument(); + }); + + it("shows outcome message when capturing partial balance", () => { + // Arrange & Act + renderDialog({ + orderTotal: createMoney(100), + authorizedAmount: createMoney(50), + }); + + // Assert - outcome prediction message is shown + expect(screen.getByText(/This will result in a/)).toBeInTheDocument(); + }); + }); + + describe("with charged amount", () => { + it("displays 'Captured so far' label when there is prior charged amount", () => { + // Arrange & Act + renderDialog({ + orderTotal: createMoney(100), + authorizedAmount: createMoney(50), + chargedAmount: createMoney(30), + }); + + // Assert + expect(screen.getByText("Captured so far")).toBeInTheDocument(); + }); + + it("displays 'Balance due' label with prior charges", () => { + // Arrange & Act + renderDialog({ + orderTotal: createMoney(100), + authorizedAmount: createMoney(50), + chargedAmount: createMoney(30), + }); + + // Assert + expect(screen.getByText("Balance due")).toBeInTheDocument(); + }); + }); +}); From e2a82e43cb8f98482efa96c729d71f926b39b00c Mon Sep 17 00:00:00 2001 From: Mirek Mencel Date: Fri, 12 Dec 2025 15:08:34 +0100 Subject: [PATCH 33/84] Fix types --- src/orders/components/OrderCaptureDialog/OrderCaptureDialog.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/orders/components/OrderCaptureDialog/OrderCaptureDialog.tsx b/src/orders/components/OrderCaptureDialog/OrderCaptureDialog.tsx index b9523ac2571..bde64b3b46f 100644 --- a/src/orders/components/OrderCaptureDialog/OrderCaptureDialog.tsx +++ b/src/orders/components/OrderCaptureDialog/OrderCaptureDialog.tsx @@ -379,7 +379,7 @@ export const OrderCaptureDialog = ({ setSelectedOption(value)} + onValueChange={value => setSelectedOption(value as CaptureAmountOption)} > {/* Order Total / Remaining Balance / Remaining Max option */} From 655723f5a35db9c0479fe11b58a77430a3e81c25 Mon Sep 17 00:00:00 2001 From: Mikail Kocak Date: Fri, 12 Dec 2025 17:47:19 +0100 Subject: [PATCH 34/84] fix: add support for feature branches in testenvs (CI/CD) This fixes an issue where deploying to testenvs was crashing when a developer was using feature branches (e.g., `head=my-branch, base=my-feature` instead of `head=my-branch, base=main`) We believe this is the simplest way of handling this case. We do not believe we need to build any complicated solutions for this issue as we do not expect anyone to be using feature branches against stables branches (e.g., 3.21) --- .../actions/prepare-api-variables/action.yml | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/.github/actions/prepare-api-variables/action.yml b/.github/actions/prepare-api-variables/action.yml index b9da6904800..8924cf41294 100644 --- a/.github/actions/prepare-api-variables/action.yml +++ b/.github/actions/prepare-api-variables/action.yml @@ -55,16 +55,25 @@ runs: echo "POOL_NAME=${PREFIX}${PULL_REQUEST_NUMBER}" >> $GITHUB_OUTPUT echo "POOL_INSTANCE=https://${PREFIX}${PULL_REQUEST_NUMBER}.staging.saleor.cloud" >> $GITHUB_OUTPUT - if [[ "$DESTINATION_BRANCH" == 'main' ]]; then - echo "BACKUP_NAMESPACE=snapshot-automation-tests" >> $GITHUB_OUTPUT - echo "SALEOR_CLOUD_SERVICE=saleor-master-staging" >> $GITHUB_OUTPUT - echo "RUN_SLUG=${PREFIX}${PULL_REQUEST_NUMBER}" >> $GITHUB_OUTPUT - else - # it handles pull requests to the other branches than main, e.g. release branches + if [[ "$DESTINATION_BRANCH" =~ ^[0-9]+\.[0-9]+$ ]]; then + # handles pull requests to the other branches than main, e.g. release branches VERSION_SLUG=$(echo "${DESTINATION_BRANCH}" | sed "s/\.//") echo "BACKUP_NAMESPACE=snapshot-automation-tests-${DESTINATION_BRANCH}" >> $GITHUB_OUTPUT echo "SALEOR_CLOUD_SERVICE=saleor-staging-v${VERSION_SLUG}" >> $GITHUB_OUTPUT echo "RUN_SLUG=${DESTINATION_BRANCH}" >> $GITHUB_OUTPUT + else + # fallback to "saleor-master-staging" (i.e., 'main' branch) when the base + # branch isn't a version number (e.g., 3.21). + # We expect this to occur in two cases: + # 1. The base branch is `main` + # 2. The base branch is a feature branch, e.g., "unify-order-value-sections", + # in which case it makes sense to fallback and to treat the PR as targeting + # the `main` branch as `main` is an unstable (dev) branch, whereas + # version number branches are stable branches thus we do not expect anyone + # to be using feature branches against these. + echo "BACKUP_NAMESPACE=snapshot-automation-tests" >> $GITHUB_OUTPUT + echo "SALEOR_CLOUD_SERVICE=saleor-master-staging" >> $GITHUB_OUTPUT + echo "RUN_SLUG=${PREFIX}${PULL_REQUEST_NUMBER}" >> $GITHUB_OUTPUT fi exit 0 From 810a191cff279f51b96bfc1b6d882149ab777de2 Mon Sep 17 00:00:00 2001 From: Mirek Mencel Date: Sat, 13 Dec 2025 13:31:13 +0100 Subject: [PATCH 35/84] Extract messages --- locale/defaultMessages.json | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/locale/defaultMessages.json b/locale/defaultMessages.json index 3d1056692c8..4304fe9025a 100644 --- a/locale/defaultMessages.json +++ b/locale/defaultMessages.json @@ -3283,6 +3283,7 @@ "IU1lif": { "context": "radio option for custom capture amount", "string": "Custom amount" + }, "IUWJKt": { "context": "order was discounted event title", "string": "Order was discounted" @@ -5137,10 +5138,6 @@ "context": "label for authorized amount", "string": "Authorized" }, - "U1eJIw": { - "context": "order history message", - "string": "Products were added to an order" - }, "U2DyeR": { "string": "Are you sure you want to delete structure {menuName}?" }, @@ -5452,6 +5449,7 @@ "VwCTbx": { "context": "outcome prediction when order will be fully charged", "string": "This will result in a {status} order" + }, "VxStyU": { "context": "order history message", "string": "Replacement order was created" From 7e640948f06a7351a582b6334a50407f059212b5 Mon Sep 17 00:00:00 2001 From: Mirek Mencel Date: Sat, 13 Dec 2025 14:25:32 +0100 Subject: [PATCH 36/84] Don't ignore apollo errors --- src/misc.ts | 11 ++++++++++- src/orders/utils/OrderDetailsViewModel.ts | 6 ++++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/misc.ts b/src/misc.ts index cff05c1bfcf..7f9c88d8789 100644 --- a/src/misc.ts +++ b/src/misc.ts @@ -293,7 +293,16 @@ export function getMutationStatus( diff --git a/src/orders/utils/OrderDetailsViewModel.ts b/src/orders/utils/OrderDetailsViewModel.ts index 7179ab19728..66d49292198 100644 --- a/src/orders/utils/OrderDetailsViewModel.ts +++ b/src/orders/utils/OrderDetailsViewModel.ts @@ -46,8 +46,10 @@ export abstract class OrderDetailsViewModel { return orderActions.includes(OrderAction.MARK_AS_PAID); } - static canOrderCapture(actions: OrderAction[]): boolean { - return actions.includes(OrderAction.CAPTURE); + static canOrderCapture(_actions: OrderAction[]): boolean { + // TODO: Remove this override - temporary for testing capture dialog + return true; + // return actions.includes(OrderAction.CAPTURE); } static canOrderVoid(actions: OrderAction[]): boolean { From 818bb5e0b165ccfdd35b8ad0f92890e99a50e950 Mon Sep 17 00:00:00 2001 From: Mirek Mencel Date: Sat, 13 Dec 2025 14:30:31 +0100 Subject: [PATCH 37/84] Remove the temp testing hack --- src/orders/utils/OrderDetailsViewModel.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/orders/utils/OrderDetailsViewModel.ts b/src/orders/utils/OrderDetailsViewModel.ts index 66d49292198..7179ab19728 100644 --- a/src/orders/utils/OrderDetailsViewModel.ts +++ b/src/orders/utils/OrderDetailsViewModel.ts @@ -46,10 +46,8 @@ export abstract class OrderDetailsViewModel { return orderActions.includes(OrderAction.MARK_AS_PAID); } - static canOrderCapture(_actions: OrderAction[]): boolean { - // TODO: Remove this override - temporary for testing capture dialog - return true; - // return actions.includes(OrderAction.CAPTURE); + static canOrderCapture(actions: OrderAction[]): boolean { + return actions.includes(OrderAction.CAPTURE); } static canOrderVoid(actions: OrderAction[]): boolean { From 0ed6529e351469a3d5e588b436ec481736a353df Mon Sep 17 00:00:00 2001 From: Mirek Mencel Date: Sat, 13 Dec 2025 14:59:52 +0100 Subject: [PATCH 38/84] Reset the dialog to the default option on open --- .../OrderCaptureDialog/OrderCaptureDialog.tsx | 37 +++++++++---------- 1 file changed, 17 insertions(+), 20 deletions(-) diff --git a/src/orders/components/OrderCaptureDialog/OrderCaptureDialog.tsx b/src/orders/components/OrderCaptureDialog/OrderCaptureDialog.tsx index bde64b3b46f..01cf228a148 100644 --- a/src/orders/components/OrderCaptureDialog/OrderCaptureDialog.tsx +++ b/src/orders/components/OrderCaptureDialog/OrderCaptureDialog.tsx @@ -16,7 +16,7 @@ import { getOrderTransactionErrorMessage } from "@dashboard/utils/errors/transac import { IMoney } from "@dashboard/utils/intl"; import { Box, Input, RadioGroup, Text } from "@saleor/macaw-ui-next"; import { AlertTriangle, Box as BoxIcon, CheckCircle2, CircleAlert, CreditCard } from "lucide-react"; -import { ChangeEvent, useMemo, useState } from "react"; +import { ChangeEvent, useEffect, useMemo, useState } from "react"; import { FormattedMessage, useIntl } from "react-intl"; import { messages } from "./messages"; @@ -112,15 +112,11 @@ export const OrderCaptureDialog = ({ const canCaptureOrderTotal = availableToCapture >= remainingToPay && remainingToPay > 0; const shortfall = remainingToPay - availableToCapture; - // Default selection based on status + // Default selection: always prefer "orderTotal" unless it's disabled + const isFirstOptionDisabled = authStatus === "none" || authStatus === "charged"; const getDefaultOption = (): CaptureAmountOption => { - // For full and partial, default to the first option - // For "none" or "charged" we can't select anything meaningful - if (authStatus === "full" || authStatus === "partial") { - return "orderTotal"; - } - - return "custom"; + // Always default to orderTotal (first option) unless it's disabled + return isFirstOptionDisabled ? "custom" : "orderTotal"; }; const getDefaultCustomAmount = (): string => { @@ -140,6 +136,15 @@ export const OrderCaptureDialog = ({ const [selectedOption, setSelectedOption] = useState(getDefaultOption); const [customAmount, setCustomAmount] = useState(getDefaultCustomAmount); + // Reset state when dialog opens to ensure correct defaults based on current props + useEffect(() => { + if (open) { + setSelectedOption(getDefaultOption()); + setCustomAmount(getDefaultCustomAmount()); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [open]); + // Get max decimal places for this currency (e.g., 2 for USD, 0 for JPY, 3 for KWD) const maxDecimalPlaces = useMemo(() => getCurrencyDecimalPoints(currency), [currency]); @@ -393,15 +398,9 @@ export const OrderCaptureDialog = ({ - + {formatMoney(authStatus === "partial" ? availableToCapture : remainingToPay)} From 43cb77389021a5e2bb78c14e8b467ea0ab1256a0 Mon Sep 17 00:00:00 2001 From: Mirek Mencel Date: Sat, 13 Dec 2025 19:16:24 +0100 Subject: [PATCH 39/84] POC --- package.json | 1 + pnpm-lock.yaml | 17 ++ src/components/messages/Container.tsx | 10 - src/components/messages/MessageDisplay.tsx | 72 ------- .../messages/MessageManagerProvider.test.tsx | 30 ++- .../messages/MessageManagerProvider.tsx | 58 +++++- src/components/messages/ToastDebug.tsx | 191 ++++++++++++++++++ src/components/messages/Transition.tsx | 28 --- src/components/messages/index.ts | 18 +- src/components/messages/messages.ts | 14 -- src/components/messages/sonnerStyles.css | 117 +++++++++++ src/components/messages/styles.ts | 25 --- .../messages/useMessageState.test.tsx | 121 ----------- src/components/messages/useMessageState.tsx | 108 ---------- src/hooks/useNotifier/useNotifier.ts | 16 -- src/hooks/useNotifier/useNotifier.tsx | 46 +++++ 16 files changed, 441 insertions(+), 431 deletions(-) delete mode 100644 src/components/messages/Container.tsx delete mode 100644 src/components/messages/MessageDisplay.tsx create mode 100644 src/components/messages/ToastDebug.tsx delete mode 100644 src/components/messages/Transition.tsx delete mode 100644 src/components/messages/messages.ts create mode 100644 src/components/messages/sonnerStyles.css delete mode 100644 src/components/messages/styles.ts delete mode 100644 src/components/messages/useMessageState.test.tsx delete mode 100644 src/components/messages/useMessageState.tsx delete mode 100644 src/hooks/useNotifier/useNotifier.ts create mode 100644 src/hooks/useNotifier/useNotifier.tsx diff --git a/package.json b/package.json index 3433df156b0..ac8d79de8d3 100644 --- a/package.json +++ b/package.json @@ -134,6 +134,7 @@ "react-sortable-hoc": "^1.11.0", "react-transition-group": "4.4.5", "slugify": "^1.6.6", + "sonner": "^2.0.7", "tslib": "^2.8.1", "url-join": "^4.0.1", "use-react-router": "^1.0.7", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0b519e41d46..bcd4b41cb41 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -245,6 +245,9 @@ importers: slugify: specifier: ^1.6.6 version: 1.6.6 + sonner: + specifier: ^2.0.7 + version: 2.0.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1) tslib: specifier: ^2.8.1 version: 2.8.1 @@ -11770,6 +11773,15 @@ packages: integrity: sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==, } + sonner@2.0.7: + resolution: + { + integrity: sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==, + } + peerDependencies: + react: ^18.0.0 || ^19.0.0 || ^19.0.0-rc + react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-rc + sort-object-keys@1.1.3: resolution: { @@ -21626,6 +21638,11 @@ snapshots: dot-case: 3.0.4 tslib: 2.8.1 + sonner@2.0.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + sort-object-keys@1.1.3: {} sort-package-json@3.4.0: diff --git a/src/components/messages/Container.tsx b/src/components/messages/Container.tsx deleted file mode 100644 index 41803c7259e..00000000000 --- a/src/components/messages/Container.tsx +++ /dev/null @@ -1,10 +0,0 @@ -// @ts-strict-ignore -import { useStyles } from "./styles"; - -const Container = ({ children }) => { - const classes = useStyles({}); - - return !!children.length &&
{children}
; -}; - -export default Container; diff --git a/src/components/messages/MessageDisplay.tsx b/src/components/messages/MessageDisplay.tsx deleted file mode 100644 index ba7969464a4..00000000000 --- a/src/components/messages/MessageDisplay.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { commonMessages } from "@dashboard/intl"; -import { Portal } from "@radix-ui/react-portal"; -import { ApiMessageData, Notification } from "@saleor/macaw-ui"; -import { useIntl } from "react-intl"; -import { TransitionGroup } from "react-transition-group"; - -import Container from "./Container"; -import { messages as notificationMessages } from "./messages"; -import { useStyles } from "./styles"; -import Transition from "./Transition"; -import { MessageComponentValues } from "./useMessageState"; - -export const MessageDisplay = ({ - notifications, - pauseTimer, - resumeTimer, -}: MessageComponentValues) => { - const classes = useStyles(); - const intl = useIntl(); - - return ( - - - {notifications?.map(notification => ( - - pauseTimer(notification), - onMouseLeave: () => resumeTimer(notification), - } - : {})} - onClose={notification.close} - title={ - (notification.message.apiMessage && !notification.message.title - ? intl.formatMessage(commonMessages.defaultErrorTitle) - : notification.message.title) as string - } - type={notification.message.status || "info"} - content={notification.message.text} - apiMessage={ - (notification.message.apiMessage && { - apiMessageContent: ( -
-                      {notification.message.apiMessage}
-                    
- ), - hideApiLabel: intl.formatMessage(notificationMessages.hideError), - showApiLabel: intl.formatMessage(notificationMessages.seeError), - }) as ApiMessageData - } - {...(notification.message.actionBtn - ? { - action: { - label: notification.message.actionBtn.label, - onClick: notification.message.actionBtn.action, - }, - } - : {})} - className={classes.notification} - /> -
- ))} -
-
- ); -}; diff --git a/src/components/messages/MessageManagerProvider.test.tsx b/src/components/messages/MessageManagerProvider.test.tsx index fbaad5b10d9..0abdd2669fc 100644 --- a/src/components/messages/MessageManagerProvider.test.tsx +++ b/src/components/messages/MessageManagerProvider.test.tsx @@ -1,24 +1,34 @@ +import { ThemeProvider } from "@saleor/macaw-ui-next"; import { renderHook } from "@testing-library/react-hooks"; import { PropsWithChildren, useContext } from "react"; import { MessageContext } from "."; import MessageManagerProvider from "./MessageManagerProvider"; -// Mock MessageDisplay component since we're not testing its functionality -jest.mock("./MessageDisplay", () => ({ - MessageDisplay: () =>
Message Display
, +// Mock sonner +jest.mock("sonner", () => ({ + toast: { + success: jest.fn(), + error: jest.fn(), + warning: jest.fn(), + info: jest.fn(), + dismiss: jest.fn(), + }, + Toaster: () => null, })); describe("MessageManagerProvider", () => { + const wrapper = ({ children }: PropsWithChildren) => ( + + {children} + + ); + test("should provide MessageContext to children", () => { - // Arrange - const { result } = renderHook(() => useContext(MessageContext), { - wrapper: ({ children }: PropsWithChildren) => ( - {children} - ), - }); + // Arrange & Act + const { result } = renderHook(() => useContext(MessageContext), { wrapper }); - // Assert: context exists + // Assert expect(result.current).toBeTruthy(); expect(typeof result.current?.show).toBe("function"); expect(typeof result.current?.remove).toBe("function"); diff --git a/src/components/messages/MessageManagerProvider.tsx b/src/components/messages/MessageManagerProvider.tsx index ee5754ebda9..2c4960a9d88 100644 --- a/src/components/messages/MessageManagerProvider.tsx +++ b/src/components/messages/MessageManagerProvider.tsx @@ -1,18 +1,56 @@ -import { PropsWithChildren } from "react"; +import "./sonnerStyles.css"; -import { MessageContext } from "."; -import { MessageDisplay } from "./MessageDisplay"; -import { useMessageState } from "./useMessageState"; +import { useTheme } from "@saleor/macaw-ui-next"; +import { AlertTriangle, CheckCircle2, Info, XCircle } from "lucide-react"; +import { CSSProperties, PropsWithChildren } from "react"; +import { toast, Toaster } from "sonner"; + +import { INotificationContext, MessageContext } from "."; +import { ToastDebug } from "./ToastDebug"; const MessageManagerProvider = ({ children }: PropsWithChildren) => { - const { context, componentState } = useMessageState(); + const { theme, themeValues } = useTheme(); + const isDark = theme === "defaultDark"; + + const context: INotificationContext = { + show: () => {}, + remove: (id: number) => { + toast.dismiss(id); + }, + clearErrorNotifications: () => { + toast.dismiss(); + }, + }; + + const toasterStyle = { + "--toast-bg": themeValues?.colors.background.default1, + "--toast-border": themeValues?.colors.border.default1, + "--toast-text": themeValues?.colors.text.default1, + "--toast-text-secondary": themeValues?.colors.text.default2, + // Success toast colors - light vs dark + "--toast-success-bg": isDark ? "oklch(25% 0.04 145)" : "oklch(97% 0.04 145)", + "--toast-success-border": isDark ? "oklch(35% 0.06 145)" : "oklch(90% 0.06 145)", + } as CSSProperties; return ( - <> - {/* This component is used in main `App` component, to pass context */} - {children} - - + + {children} + , + error: , + warning: , + info: , + }} + /> + + ); }; diff --git a/src/components/messages/ToastDebug.tsx b/src/components/messages/ToastDebug.tsx new file mode 100644 index 00000000000..221182b75d1 --- /dev/null +++ b/src/components/messages/ToastDebug.tsx @@ -0,0 +1,191 @@ +/** + * Temporary debug component to preview toast variants + * Remove this file after styling is finalized + */ +import { Box, Button, Text } from "@saleor/macaw-ui-next"; +import { GripVertical } from "lucide-react"; +import { useRef, useState } from "react"; +import { toast } from "sonner"; + +export const ToastDebug = () => { + const [position, setPosition] = useState({ x: 20, y: 20 }); + const [isDragging, setIsDragging] = useState(false); + const dragOffset = useRef({ x: 0, y: 0 }); + + const handleMouseDown = (e: React.MouseEvent) => { + setIsDragging(true); + dragOffset.current = { + x: e.clientX - position.x, + y: window.innerHeight - e.clientY - position.y, + }; + + const handleMouseMove = (e: MouseEvent) => { + setPosition({ + x: e.clientX - dragOffset.current.x, + y: window.innerHeight - e.clientY - dragOffset.current.y, + }); + }; + + const handleMouseUp = () => { + setIsDragging(false); + document.removeEventListener("mousemove", handleMouseMove); + document.removeEventListener("mouseup", handleMouseUp); + }; + + document.addEventListener("mousemove", handleMouseMove); + document.addEventListener("mouseup", handleMouseUp); + }; + + const showSavedChanges = () => { + toast.success("Saved changes", { + description: "Product details have been updated successfully.", + }); + }; + + const showOrderConfirmed = () => { + toast.success("Order confirmed", { + description: "Order #12345 has been confirmed and is ready for fulfillment.", + }); + }; + + const showSimpleError = () => { + toast.error("Something went wrong", { + description: "Unable to save changes. Please try again.", + duration: Infinity, + }); + }; + + const showPermissionError = () => { + toast.error("Permission denied", { + description: "You don't have permission to perform this action.", + duration: Infinity, + }); + }; + + const showGraphQLError = () => { + toast.error("Failed to update product", { + description: "Product with this SKU already exists.", + duration: Infinity, + }); + }; + + const showNetworkError = () => { + toast.error("Network error", { + description: "Unable to connect to the server. Please try again later.", + duration: Infinity, + }); + }; + + const showSessionExpiring = () => { + toast.warning("Session expiring soon", { + description: "Your session will expire in 5 minutes. Save your work.", + }); + }; + + const showExportStarted = () => { + toast.info("Export started", { + description: "Your export is being processed. You'll be notified when it's ready.", + }); + }; + + const showUndoAction = () => { + toast.success("Product deleted", { + description: "The product has been moved to trash.", + action: { + label: "Undo", + onClick: () => toast.success("Restored", { description: "Product has been restored." }), + }, + duration: 8000, + }); + }; + + return ( + + {/* Drag handle */} + + + + 🍞 Toast Preview + + + + + + SUCCESS + + + + + + + + + + + ERRORS + + + + + + + + + + + + OTHER + + + + + + + + + + ); +}; diff --git a/src/components/messages/Transition.tsx b/src/components/messages/Transition.tsx deleted file mode 100644 index afa4cd0aa9f..00000000000 --- a/src/components/messages/Transition.tsx +++ /dev/null @@ -1,28 +0,0 @@ -// @ts-strict-ignore -import { Transition as MessageManagerTransition } from "react-transition-group"; - -const duration = 250; -const defaultStyle = { - opacity: 0, - transition: `opacity ${duration}ms ease`, -}; -const transitionStyles = { - entered: { opacity: 1 }, - entering: { opacity: 0 }, -}; -const Transition = ({ children, ...props }) => ( - - {state => ( -
- {children} -
- )} -
-); - -export default Transition; diff --git a/src/components/messages/index.ts b/src/components/messages/index.ts index a0e50a179aa..2bc178c118f 100644 --- a/src/components/messages/index.ts +++ b/src/components/messages/index.ts @@ -1,35 +1,19 @@ import { createContext } from "react"; type Status = "success" | "error" | "info" | "warning"; + export interface IMessage { actionBtn?: { label: string; action: () => void; }; autohide?: number; - expandText?: string; title?: string; text?: React.ReactNode; - onUndo?: () => void; status?: Status; apiMessage?: string; } -export interface INotification { - id: number; - message: IMessage; - timeout: number; - close: () => void; -} - -export interface ITimer { - id: number; - notification: INotification; - remaining: number; - start: number; - timeoutId: number; -} - export interface INotificationContext { show: (message: IMessage, timeout?: number | null) => void; remove: (notificationId: number) => void; diff --git a/src/components/messages/messages.ts b/src/components/messages/messages.ts deleted file mode 100644 index 4928db95848..00000000000 --- a/src/components/messages/messages.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { defineMessages } from "react-intl"; - -export const messages = defineMessages({ - seeError: { - id: "w9xgN9", - defaultMessage: "See error log", - description: "see error log label in notification", - }, - hideError: { - id: "s8FlDW", - defaultMessage: "Hide log", - description: "hide error log label in notification", - }, -}); diff --git a/src/components/messages/sonnerStyles.css b/src/components/messages/sonnerStyles.css new file mode 100644 index 00000000000..1a341c89c5b --- /dev/null +++ b/src/components/messages/sonnerStyles.css @@ -0,0 +1,117 @@ +/* + * Sonner toast styles for Saleor Dashboard + * + * CSS custom properties are set on [data-sonner-toaster] from React: + * --toast-bg, --toast-border, --toast-text, --toast-text-secondary + * --toast-success-bg, --toast-success-border + */ + +[data-sonner-toaster] { + --width: 380px; + font-family: + Inter, + -apple-system, + BlinkMacSystemFont, + "Segoe UI", + Roboto, + sans-serif; +} + +[data-sonner-toast] { + background: var(--toast-bg) !important; + border-color: var(--toast-border) !important; + color: var(--toast-text) !important; + border-radius: 12px; + box-shadow: + 0 4px 6px -1px rgb(0 0 0 / 0.05), + 0 2px 4px -2px rgb(0 0 0 / 0.05); + position: relative; + gap: 16px !important; + font-family: inherit; + padding: 16px 36px 16px 20px !important; +} + +/* Toast title */ +[data-sonner-toast] [data-title] { + font-size: 14px !important; + font-weight: 500 !important; + line-height: 1.4 !important; + color: var(--toast-text) !important; +} + +/* Toast description */ +[data-sonner-toast] [data-description] { + font-size: 13px !important; + font-weight: 400 !important; + line-height: 1.4 !important; + color: var(--toast-text-secondary) !important; + opacity: 0.9; +} + +/* Success - green background */ +[data-sonner-toast][data-type="success"] { + background: var(--toast-success-bg) !important; + border-color: var(--toast-success-border) !important; +} + +/* Icon colors - light theme */ +[data-sonner-toast][data-type="success"] [data-icon] svg { + color: oklch(45% 0.16 145) !important; +} + +[data-sonner-toast][data-type="error"] [data-icon] svg { + color: oklch(50% 0.2 20) !important; +} + +[data-sonner-toast][data-type="warning"] [data-icon] svg { + color: oklch(55% 0.16 70) !important; +} + +[data-sonner-toast][data-type="info"] [data-icon] svg { + color: oklch(50% 0.15 250) !important; +} + +/* Icon colors - dark theme */ +[data-sonner-toaster][data-theme="dark"] [data-sonner-toast][data-type="success"] [data-icon] svg { + color: oklch(65% 0.18 145) !important; +} + +[data-sonner-toaster][data-theme="dark"] [data-sonner-toast][data-type="error"] [data-icon] svg { + color: oklch(65% 0.2 20) !important; +} + +[data-sonner-toaster][data-theme="dark"] [data-sonner-toast][data-type="warning"] [data-icon] svg { + color: oklch(70% 0.18 70) !important; +} + +[data-sonner-toaster][data-theme="dark"] [data-sonner-toast][data-type="info"] [data-icon] svg { + color: oklch(65% 0.17 250) !important; +} + +/* Close button */ +[data-sonner-toast] [data-close-button] { + position: absolute !important; + top: 8px !important; + right: 8px !important; + left: auto !important; + transform: none !important; + background: transparent !important; + border: 1px solid rgba(0, 0, 0, 0.1) !important; + border-radius: 4px !important; + opacity: 0.6; + transition: all 0.15s; +} + +[data-sonner-toast] [data-close-button]:hover { + opacity: 1; + background: rgba(0, 0, 0, 0.05) !important; +} + +/* Dark theme close button */ +[data-sonner-toaster][data-theme="dark"] [data-sonner-toast] [data-close-button] { + border-color: rgba(255, 255, 255, 0.15) !important; +} + +[data-sonner-toaster][data-theme="dark"] [data-sonner-toast] [data-close-button]:hover { + background: rgba(255, 255, 255, 0.1) !important; +} diff --git a/src/components/messages/styles.ts b/src/components/messages/styles.ts deleted file mode 100644 index 07658bda2b3..00000000000 --- a/src/components/messages/styles.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { makeStyles } from "@saleor/macaw-ui"; - -export const useStyles = makeStyles( - theme => ({ - container: { - display: "grid", - justifyContent: "end", - right: 0, - pointerEvents: "auto", - position: "fixed", - top: 0, - width: "auto", - maxHeight: "100vh", - overflowY: "auto", - zIndex: 10000, - }, - notification: { - // Parent container has disabled pointer events so we need to turn them on - // for action and timer pausing to work - pointerEvents: "all", - margin: theme.spacing(2), - }, - }), - { name: "MessageManager" }, -); diff --git a/src/components/messages/useMessageState.test.tsx b/src/components/messages/useMessageState.test.tsx deleted file mode 100644 index d35e42aca8f..00000000000 --- a/src/components/messages/useMessageState.test.tsx +++ /dev/null @@ -1,121 +0,0 @@ -import { DEFAULT_NOTIFICATION_SHOW_TIME } from "@dashboard/config"; -import { act, renderHook } from "@testing-library/react-hooks"; - -import { useMessageState } from "./useMessageState"; - -describe("useMessageState", () => { - beforeEach(() => { - jest.useFakeTimers(); - }); - - afterEach(() => { - jest.useRealTimers(); - }); - - test("should initialize with empty notifications", () => { - // Arrange - const { result } = renderHook(() => useMessageState()); - - // Assert - expect(result.current.componentState.notifications).toEqual([]); - }); - - test("should add a notification when show is called", () => { - // Arrange - const { result } = renderHook(() => useMessageState()); - - // Act - act(() => { - result.current.context.show({ text: "Test notification" }); - }); - - // Assert - expect(result.current.componentState.notifications).toHaveLength(1); - expect(result.current.componentState.notifications[0].message.text).toBe("Test notification"); - }); - - test("should remove a notification when remove is called", () => { - // Arrange - const { result } = renderHook(() => useMessageState()); - - // Act - act(() => { - result.current.context.show({ text: "Test notification" }); - }); - - act(() => { - result.current.context.remove(result.current.componentState.notifications[0].id); - }); - - // Assert - expect(result.current.componentState.notifications).toHaveLength(0); - }); - - test("should auto-remove notification after timeout", () => { - // Arrange - const { result } = renderHook(() => useMessageState()); - - // Act - act(() => { - result.current.context.show({ text: "Test notification" }, 1000); - }); - - // Assert - expect(result.current.componentState.notifications).toHaveLength(1); - - // Act - act(() => { - jest.advanceTimersByTime(1000); - }); - - // Assert - expect(result.current.componentState.notifications).toHaveLength(0); - }); - - test("should pause and resume timer correctly", () => { - // Arrange - const { result } = renderHook(() => useMessageState()); - - // Act - act(() => { - result.current.context.show({ text: "Test notification" }, 2000); - }); - - act(() => { - // Advance time partially - jest.advanceTimersByTime(10); - }); - - // Assert: Notification should still be there - expect(result.current.componentState.notifications).toHaveLength(1); - - // Act - act(() => { - // Pause the timer - result.current.componentState.pauseTimer(result.current.componentState.notifications[0]); - }); - - act(() => { - // Advance time beyond original timeout - jest.advanceTimersByTime(DEFAULT_NOTIFICATION_SHOW_TIME); - }); - - // Assert: Notification should still be there because timer was paused - expect(result.current.componentState.notifications).toHaveLength(1); - - // Act - act(() => { - // Resume the timer - result.current.componentState.resumeTimer(result.current.componentState.notifications[0]); - }); - - // Act - act(() => { - // Advance time to complete the remaining time - jest.advanceTimersByTime(DEFAULT_NOTIFICATION_SHOW_TIME); - }); - - // Assert: Notification should be removed now - expect(result.current.componentState.notifications).toHaveLength(0); - }); -}); diff --git a/src/components/messages/useMessageState.tsx b/src/components/messages/useMessageState.tsx deleted file mode 100644 index d7892887636..00000000000 --- a/src/components/messages/useMessageState.tsx +++ /dev/null @@ -1,108 +0,0 @@ -import { DEFAULT_NOTIFICATION_SHOW_TIME } from "@dashboard/config"; -import { useCallback, useEffect, useRef, useState } from "react"; - -import { INotification, INotificationContext, ITimer } from "."; - -export type MessageComponentValues = ReturnType["componentState"]; - -export const useMessageState = () => { - const timer = useRef(0); - const timersArr = useRef([]); - const [notifications, setNotifications] = useState([]); - - useEffect(() => { - const timersArrRef = timersArr.current; - - return () => { - timersArrRef.forEach(timer => clearTimeout(timer.timeoutId)); - }; - }, []); - - const timerCallback = (notification: INotification) => { - remove(notification.id); - timersArr.current = timersArr.current.filter(timer => timer.id !== notification.id); - }; - const remove = useCallback((notificationId: number) => { - setNotifications(currentNotifications => - currentNotifications.filter(n => n.id !== notificationId), - ); - }, []); - const clearErrorNotifications = useCallback(() => { - setNotifications(notifications => - notifications.filter(notification => notification.message.status !== "error"), - ); - }, []); - - const show = useCallback((message = {}, timeout = DEFAULT_NOTIFICATION_SHOW_TIME) => { - const id = timer.current; - - timer.current += 1; - - const notification = { - close: () => remove(id), - id, - message, - timeout, - }; - - if (timeout !== null) { - const timeoutId = window.setTimeout(() => { - timerCallback(notification); - }, timeout); - - timersArr.current.push({ - id: notification.id, - notification, - remaining: timeout, - start: new Date().getTime(), - timeoutId, - }); - } - - setNotifications(state => [notification, ...state]); - - return notification; - }, []); - - const getCurrentTimer = (notification: INotification) => { - const currentTimerIndex = timersArr.current.findIndex(timer => timer.id === notification.id); - - return timersArr.current[currentTimerIndex]; - }; - const pauseTimer = (notification: INotification) => { - const currentTimer = getCurrentTimer(notification); - - if (currentTimer) { - currentTimer.remaining = currentTimer.remaining - (new Date().getTime() - currentTimer.start); - window.clearTimeout(currentTimer.timeoutId); - } - }; - const resumeTimer = (notification: INotification) => { - const currentTimer = getCurrentTimer(notification); - - if (currentTimer) { - currentTimer.start = new Date().getTime(); - currentTimer.timeoutId = window.setTimeout( - () => timerCallback(notification), - currentTimer.remaining, - ); - } - }; - - const context = { - remove, - show, - clearErrorNotifications, - } as INotificationContext; - - const componentState = { - pauseTimer, - resumeTimer, - notifications, - }; - - return { - context, - componentState, - } as const; -}; diff --git a/src/hooks/useNotifier/useNotifier.ts b/src/hooks/useNotifier/useNotifier.ts deleted file mode 100644 index 78acba1b29c..00000000000 --- a/src/hooks/useNotifier/useNotifier.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { IMessage, IMessageContext, MessageContext } from "@dashboard/components/messages"; -import { useContext } from "react"; - -export type UseNotifierResult = IMessageContext; - -function useNotifier(): UseNotifierResult { - const notificationContext = useContext(MessageContext); - const notify = (options: IMessage) => { - const timeout = options.status === "error" ? null : options.autohide; - - notificationContext?.show(options, timeout); - }; - - return notify; -} -export default useNotifier; diff --git a/src/hooks/useNotifier/useNotifier.tsx b/src/hooks/useNotifier/useNotifier.tsx new file mode 100644 index 00000000000..45dca36d5bc --- /dev/null +++ b/src/hooks/useNotifier/useNotifier.tsx @@ -0,0 +1,46 @@ +import { IMessage, IMessageContext } from "@dashboard/components/messages"; +import { DEFAULT_NOTIFICATION_SHOW_TIME } from "@dashboard/config"; +import { toast } from "sonner"; + +export type UseNotifierResult = IMessageContext; + +function useNotifier(): UseNotifierResult { + const notify = (options: IMessage) => { + const duration = + options.status === "error" ? Infinity : (options.autohide ?? DEFAULT_NOTIFICATION_SHOW_TIME); + + // Build description - use apiMessage as fallback if no text + const description = options.text || options.apiMessage; + + const toastOptions = { + description: description as string | undefined, + duration, + ...(options.actionBtn && { + action: { + label: options.actionBtn.label, + onClick: options.actionBtn.action, + }, + }), + }; + + switch (options.status) { + case "success": + toast.success(options.title || "Success", toastOptions); + break; + case "error": + toast.error(options.title || "Error", toastOptions); + break; + case "warning": + toast.warning(options.title || "Warning", toastOptions); + break; + case "info": + default: + toast.info(options.title || "Info", toastOptions); + break; + } + }; + + return notify; +} + +export default useNotifier; From ec4c2bc81b2b53f01890fcf078e76aa86decfa5e Mon Sep 17 00:00:00 2001 From: Mirek Mencel Date: Sun, 14 Dec 2025 14:19:28 +0100 Subject: [PATCH 40/84] Tests for empty state --- .../OrderSummary/OrderSummary.test.tsx | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/src/orders/components/OrderSummary/OrderSummary.test.tsx b/src/orders/components/OrderSummary/OrderSummary.test.tsx index 8943c3044b8..746aaba24ba 100644 --- a/src/orders/components/OrderSummary/OrderSummary.test.tsx +++ b/src/orders/components/OrderSummary/OrderSummary.test.tsx @@ -88,6 +88,66 @@ describe("OrderSummary", () => { }); }); + describe("PaymentsSummaryEmptyState", () => { + it("should display empty state with CreditCard icon when hasNoPayment is true", () => { + // Arrange + const mockOrder = createOrderWithNoPayment(); + const onMarkAsPaid = jest.fn(); + + // Act + render( + + + , + ); + + // Assert + expect(screen.getByText("No payment received")).toBeInTheDocument(); + }); + + it("should display instruction message in empty state", () => { + // Arrange + const mockOrder = createOrderWithNoPayment(); + const onMarkAsPaid = jest.fn(); + + // Act + render( + + + , + ); + + // Assert + expect( + screen.getByText("Mark as paid manually if the payment is confirmed"), + ).toBeInTheDocument(); + }); + + it("should not display empty state when order has transactions", () => { + // Arrange + const mockOrder = { + ...orderFixture("test-id"), + transactions: [transaction], + payments: [], + grantedRefunds: [], + }; + const onMarkAsPaid = jest.fn(); + + // Act + render( + + + , + ); + + // Assert + expect(screen.queryByText("No payment received")).not.toBeInTheDocument(); + expect( + screen.queryByText("Mark as paid manually if the payment is confirmed"), + ).not.toBeInTheDocument(); + }); + }); + describe("Transactions API Mode", () => { it("should show TransactionsApiButtons when useLegacyPaymentsApi is false", () => { // Arrange From 3ee9d9b253f2f8d39fbd7de143eca3607ca9a5fb Mon Sep 17 00:00:00 2001 From: Mirek Mencel Date: Sun, 14 Dec 2025 14:23:44 +0100 Subject: [PATCH 41/84] Drop no-op function in favour of optional callback --- .../OrderDraftPage/OrderDraftPage.tsx | 3 --- .../OrderSummary/LegacyPaymentsApiButtons.tsx | 7 +++--- .../components/OrderSummary/OrderSummary.tsx | 2 +- .../OrderSummary/TransactionsApiButtons.tsx | 23 ++++++++++--------- 4 files changed, 17 insertions(+), 18 deletions(-) diff --git a/src/orders/components/OrderDraftPage/OrderDraftPage.tsx b/src/orders/components/OrderDraftPage/OrderDraftPage.tsx index 36e2f0004ab..7670ca6e9f3 100644 --- a/src/orders/components/OrderDraftPage/OrderDraftPage.tsx +++ b/src/orders/components/OrderDraftPage/OrderDraftPage.tsx @@ -157,9 +157,6 @@ const OrderDraftPage = (props: OrderDraftPageProps) => { <> { - // Draft orders cannot be marked as paid - }} isEditable onShippingMethodEdit={onShippingMethodEdit} errors={errors} diff --git a/src/orders/components/OrderSummary/LegacyPaymentsApiButtons.tsx b/src/orders/components/OrderSummary/LegacyPaymentsApiButtons.tsx index 0a974c3b9ca..54982ef689c 100644 --- a/src/orders/components/OrderSummary/LegacyPaymentsApiButtons.tsx +++ b/src/orders/components/OrderSummary/LegacyPaymentsApiButtons.tsx @@ -8,7 +8,7 @@ type Props = { canRefund: boolean; canVoid: boolean; canMarkAsPaid: boolean; - onMarkAsPaid: () => any; + onMarkAsPaid?: () => void; onLegacyPaymentsApiCapture?: () => any; onLegacyPaymentsApiRefund?: () => any; onLegacyPaymentsApiVoid?: () => any; @@ -28,7 +28,8 @@ export const LegacyPaymentsApiButtons = ({ const intl = useIntl(); const showButtons = - order?.status !== OrderStatus.CANCELED && (canCapture || canRefund || canVoid || canMarkAsPaid); + order?.status !== OrderStatus.CANCELED && + (canCapture || canRefund || canVoid || (canMarkAsPaid && onMarkAsPaid)); if (!showButtons) { return null; @@ -64,7 +65,7 @@ export const LegacyPaymentsApiButtons = ({ })} )} - {canMarkAsPaid && ( + {canMarkAsPaid && onMarkAsPaid && ( - ) + ); }; From 301ace82c72ff696e210df5a6f1396d9f32909c3 Mon Sep 17 00:00:00 2001 From: Mirek Mencel Date: Sun, 14 Dec 2025 14:29:48 +0100 Subject: [PATCH 42/84] Update variable name --- .../OrderShippingMethodEditDialog.tsx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/orders/components/OrderShippingMethodEditDialog/OrderShippingMethodEditDialog.tsx b/src/orders/components/OrderShippingMethodEditDialog/OrderShippingMethodEditDialog.tsx index af6cb7506a8..67f88a9f6f1 100644 --- a/src/orders/components/OrderShippingMethodEditDialog/OrderShippingMethodEditDialog.tsx +++ b/src/orders/components/OrderShippingMethodEditDialog/OrderShippingMethodEditDialog.tsx @@ -187,8 +187,10 @@ const OrderShippingMethodEditDialog = (props: OrderShippingMethodEditDialogProps data-test-id="shipping-method-select" value={data.shippingMethod} onChange={({ target }) => { - const newValue = target.value; - const isDisabled = choices.find(choice => choice.value === newValue)?.disabled; + const targetValue = target.value; + const isDisabled = choices.find( + choice => choice.value === targetValue, + )?.disabled; if (isDisabled) { return; @@ -198,7 +200,9 @@ const OrderShippingMethodEditDialog = (props: OrderShippingMethodEditDialogProps target: { name: "shippingMethod", value: - typeof newValue === "string" ? newValue : (newValue as Option)?.value, + typeof targetValue === "string" + ? targetValue + : (targetValue as Option)?.value, }, }); }} From 6178530429ee29da790122d8bb2a9eca82ec5063 Mon Sep 17 00:00:00 2001 From: Mirek Mencel Date: Mon, 15 Dec 2025 09:05:59 -0100 Subject: [PATCH 43/84] Emphasise the totals in the Summary section --- .../OrderSummary/OrderSummaryListAmount.tsx | 2 +- .../OrderSummary/OrderSummaryListItem.tsx | 15 ++++++++++++--- src/orders/components/OrderSummary/OrderValue.tsx | 2 +- .../components/OrderSummary/PaymentsSummary.tsx | 1 + 4 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/orders/components/OrderSummary/OrderSummaryListAmount.tsx b/src/orders/components/OrderSummary/OrderSummaryListAmount.tsx index 64a754526ea..6e0c8b505c7 100644 --- a/src/orders/components/OrderSummary/OrderSummaryListAmount.tsx +++ b/src/orders/components/OrderSummary/OrderSummaryListAmount.tsx @@ -15,7 +15,7 @@ export const OrderSummaryListAmount = ({ amount, showSign = false, ...props }: P const intl = useIntl(); return ( - + {intl.formatNumber(amount, { minimumFractionDigits: 2, signDisplay: showSign ? "exceptZero" : "auto", diff --git a/src/orders/components/OrderSummary/OrderSummaryListItem.tsx b/src/orders/components/OrderSummary/OrderSummaryListItem.tsx index 4c84564f3c5..99433d3c3fb 100644 --- a/src/orders/components/OrderSummary/OrderSummaryListItem.tsx +++ b/src/orders/components/OrderSummary/OrderSummaryListItem.tsx @@ -11,6 +11,7 @@ type Props = PropsWithBox<{ currency?: string; title?: string; amountTitle?: string; + bold?: boolean; }>; export const OrderSummaryListItem = ({ @@ -20,18 +21,26 @@ export const OrderSummaryListItem = ({ currency, title, amountTitle, + bold = false, ...props }: Props): ReactNode => { + const fontWeight = bold ? "bold" : "regular"; + return ( - + {children} - + {currency} {" "} - + ); diff --git a/src/orders/components/OrderSummary/OrderValue.tsx b/src/orders/components/OrderSummary/OrderValue.tsx index e04514c1d9a..10389babdf2 100644 --- a/src/orders/components/OrderSummary/OrderValue.tsx +++ b/src/orders/components/OrderSummary/OrderValue.tsx @@ -473,7 +473,7 @@ export const OrderValue = (props: Props): ReactNode => { }, { currency: ( - + {orderTotal.gross.currency} ), diff --git a/src/orders/components/OrderSummary/PaymentsSummary.tsx b/src/orders/components/OrderSummary/PaymentsSummary.tsx index 1274d24df31..5f310f798a4 100644 --- a/src/orders/components/OrderSummary/PaymentsSummary.tsx +++ b/src/orders/components/OrderSummary/PaymentsSummary.tsx @@ -79,6 +79,7 @@ export const PaymentsSummary = ({ orderAmounts, order, hasNoPayment, ...props }: showSign showCurrency currency={orderAmounts.totalBalance.currency} + bold > {intl.formatMessage({ defaultMessage: "Outstanding balance", From 00596a5d73376b8a4d3a8534d474828ef222363b Mon Sep 17 00:00:00 2001 From: Mirek Mencel Date: Mon, 15 Dec 2025 09:41:21 -0100 Subject: [PATCH 44/84] Add changeset --- .changeset/whole-groups-happen.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/whole-groups-happen.md diff --git a/.changeset/whole-groups-happen.md b/.changeset/whole-groups-happen.md new file mode 100644 index 00000000000..5e45a3239af --- /dev/null +++ b/.changeset/whole-groups-happen.md @@ -0,0 +1,5 @@ +--- +"saleor-dashboard": patch +--- + +Replaces custom toasts with Sonner notifications component From 3f0e8125e3ba36fc6940df5d3a3b550ee15fcfd1 Mon Sep 17 00:00:00 2001 From: Mirek Mencel Date: Mon, 15 Dec 2025 09:41:53 -0100 Subject: [PATCH 45/84] Extract messages --- locale/defaultMessages.json | 8 -------- 1 file changed, 8 deletions(-) diff --git a/locale/defaultMessages.json b/locale/defaultMessages.json index 4304fe9025a..071ade357b1 100644 --- a/locale/defaultMessages.json +++ b/locale/defaultMessages.json @@ -8963,10 +8963,6 @@ "context": "search label", "string": "Search products" }, - "s8FlDW": { - "context": "hide error log label in notification", - "string": "Hide log" - }, "s9sOcC": { "context": "button", "string": "OK" @@ -9660,10 +9656,6 @@ "w7jT4W": { "string": "No channels selected" }, - "w9xgN9": { - "context": "see error log label in notification", - "string": "See error log" - }, "wAGThK": { "context": "dialog header", "string": "Delete structures" From 9eda3bf386f79a32c901b8e69f45c1e86e849114 Mon Sep 17 00:00:00 2001 From: Mirek Mencel Date: Mon, 15 Dec 2025 21:13:21 -0100 Subject: [PATCH 46/84] Remove the unnecessary spread of props --- .../components/OrderSummary/OrderSummary.tsx | 32 ++----------------- 1 file changed, 2 insertions(+), 30 deletions(-) diff --git a/src/orders/components/OrderSummary/OrderSummary.tsx b/src/orders/components/OrderSummary/OrderSummary.tsx index 85b4f0b2afd..9c7f86714fb 100644 --- a/src/orders/components/OrderSummary/OrderSummary.tsx +++ b/src/orders/components/OrderSummary/OrderSummary.tsx @@ -31,13 +31,7 @@ type Props = PropsWithBox< >; export const OrderSummary = (props: Props) => { - const { - order, - onMarkAsPaid, - useLegacyPaymentsApi = false, - isEditable = false, - ...restProps - } = props; + const { order, onMarkAsPaid, useLegacyPaymentsApi = false, isEditable = false } = props; const intl = useIntl(); const giftCardsAmount = OrderDetailsViewModel.getGiftCardsAmountUsed({ id: order.id, @@ -57,32 +51,10 @@ export const OrderSummary = (props: Props) => { const canVoid = OrderDetailsViewModel.canOrderVoid(order.actions); const canRefund = OrderDetailsViewModel.canOrderRefund(order.actions); - // Extract editable props const editableProps = isEditable ? (props as Props & EditableOrderSummary) : null; - // Filter out props that shouldn't be passed to the DOM - const { - isEditable: _isEditable, - onShippingMethodEdit: _onShippingMethodEdit, - errors: _errors, - orderDiscount: _orderDiscount, - addOrderDiscount: _addOrderDiscount, - removeOrderDiscount: _removeOrderDiscount, - openDialog: _openDialog, - closeDialog: _closeDialog, - isDialogOpen: _isDialogOpen, - orderDiscountAddStatus: _orderDiscountAddStatus, - orderDiscountRemoveStatus: _orderDiscountRemoveStatus, - undiscountedPrice: _undiscountedPrice, - discountedPrice: _discountedPrice, - onLegacyPaymentsApiCapture: _onLegacyPaymentsApiCapture, - onLegacyPaymentsApiRefund: _onLegacyPaymentsApiRefund, - onLegacyPaymentsApiVoid: _onLegacyPaymentsApiVoid, - ...boxProps - } = restProps as any; - return ( - + {intl.formatMessage({ From 482099b75f4e6ebb6c706579318e9cf9eafa7217 Mon Sep 17 00:00:00 2001 From: Mirek Mencel Date: Mon, 15 Dec 2025 21:40:29 -0100 Subject: [PATCH 47/84] Move discounts outside of the total lines as the dsicounts apply directly to subtotal --- .../components/OrderSummary/OrderValue.tsx | 29 ++++++++++++------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/src/orders/components/OrderSummary/OrderValue.tsx b/src/orders/components/OrderSummary/OrderValue.tsx index 10389babdf2..6b83bd4cdc1 100644 --- a/src/orders/components/OrderSummary/OrderValue.tsx +++ b/src/orders/components/OrderSummary/OrderValue.tsx @@ -130,6 +130,11 @@ const messages = defineMessages({ defaultMessage: "Gift card amount used", description: "tooltip for gift card amount", }, + fixedAmount: { + id: "YPCB7b", + defaultMessage: "Fixed amount", + description: "label for fixed amount discount type", + }, }); type BaseProps = { @@ -300,13 +305,15 @@ export const OrderValue = (props: Props): ReactNode => { return discounts.map(discount => ( {intl.formatMessage(messages.discount)}{" "} {discount.name} + {" "} + + (applied) )); @@ -358,15 +365,12 @@ export const OrderValue = (props: Props): ReactNode => { ); } - const discountDisplayValue = discountLabel.percentage || discountLabel.value; + const discountDisplayValue = + discountLabel.percentage || intl.formatMessage(messages.fixedAmount); const discountAmount = parseFloat(discountLabel.value) || 0; return ( - 0} - > + {intl.formatMessage(messages.discount)}{" "} { @@ -399,7 +403,10 @@ export const OrderValue = (props: Props): ReactNode => { )} - + {" "} + + (applied) + ); }; @@ -456,8 +463,6 @@ export const OrderValue = (props: Props): ReactNode => { )} - {renderDiscountRow()} - { + {renderDiscountRow()} + {displayGrossPrices && ( Date: Mon, 15 Dec 2025 21:45:13 -0100 Subject: [PATCH 48/84] Fix returned type --- src/orders/components/OrderSummary/OrderSummary.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/orders/components/OrderSummary/OrderSummary.tsx b/src/orders/components/OrderSummary/OrderSummary.tsx index 9c7f86714fb..bbcc665a9f8 100644 --- a/src/orders/components/OrderSummary/OrderSummary.tsx +++ b/src/orders/components/OrderSummary/OrderSummary.tsx @@ -24,9 +24,9 @@ type Props = PropsWithBox< order: OrderDetailsFragment; onMarkAsPaid?: () => void; useLegacyPaymentsApi?: boolean; - onLegacyPaymentsApiCapture?: () => any; - onLegacyPaymentsApiRefund?: () => any; - onLegacyPaymentsApiVoid?: () => any; + onLegacyPaymentsApiCapture?: () => void; + onLegacyPaymentsApiRefund?: () => void; + onLegacyPaymentsApiVoid?: () => void; } & (EditableOrderSummary | ReadOnlyOrderSummary) >; From c1a5e6f0fe949246ae58b53fa8876b3faa9e9fe5 Mon Sep 17 00:00:00 2001 From: Mirek Mencel Date: Mon, 15 Dec 2025 22:14:10 -0100 Subject: [PATCH 49/84] Improve ButtonLink and drop InlineLink --- src/components/ButtonLink/ButtonLink.tsx | 9 +++- .../OrderSummary/OrderSummaryListItem.tsx | 10 +++- .../components/OrderSummary/OrderValue.tsx | 48 ++++--------------- 3 files changed, 26 insertions(+), 41 deletions(-) diff --git a/src/components/ButtonLink/ButtonLink.tsx b/src/components/ButtonLink/ButtonLink.tsx index 4b8c8d784ce..d62c22b32f4 100644 --- a/src/components/ButtonLink/ButtonLink.tsx +++ b/src/components/ButtonLink/ButtonLink.tsx @@ -36,11 +36,18 @@ export const ButtonLink = ({ }} cursor={disabled ? "not-allowed" : "pointer"} style={{ + display: "inline", textUnderlineOffset: vars.spacing[1], padding: 0, + margin: 0, + height: "auto", + minHeight: 0, color, - fontWeight: 400, + fontWeight: "inherit", + fontSize: "inherit", + lineHeight: "inherit", textDecorationColor: color, + verticalAlign: "baseline", }} {...props} > diff --git a/src/orders/components/OrderSummary/OrderSummaryListItem.tsx b/src/orders/components/OrderSummary/OrderSummaryListItem.tsx index 99433d3c3fb..ed54de7c745 100644 --- a/src/orders/components/OrderSummary/OrderSummaryListItem.tsx +++ b/src/orders/components/OrderSummary/OrderSummaryListItem.tsx @@ -27,7 +27,15 @@ export const OrderSummaryListItem = ({ const fontWeight = bold ? "bold" : "regular"; return ( - + {children} diff --git a/src/orders/components/OrderSummary/OrderValue.tsx b/src/orders/components/OrderSummary/OrderValue.tsx index 6b83bd4cdc1..ab44d30fe3c 100644 --- a/src/orders/components/OrderSummary/OrderValue.tsx +++ b/src/orders/components/OrderSummary/OrderValue.tsx @@ -1,3 +1,4 @@ +import { ButtonLink } from "@dashboard/components/ButtonLink"; import { DiscountValueTypeEnum, OrderDetailsFragment, @@ -23,37 +24,6 @@ const emptyDiscount: OrderDiscountCommonInput = { calculationMode: DiscountValueTypeEnum.PERCENTAGE, }; -const InlineLink = ({ - children, - onClick, - title, - "data-test-id": dataTestId, -}: { - children: ReactNode; - onClick?: () => void; - title?: string; - "data-test-id"?: string; -}): ReactNode => ( - { - e.preventDefault(); - onClick?.(); - }} - href="#" - title={title} - data-test-id={dataTestId} - style={{ - cursor: "pointer", - textDecoration: "none", - }} - __textDecoration={{ hover: "underline" }} - > - {children} - -); - const messages = defineMessages({ discount: { id: "+8v1ny", @@ -257,9 +227,9 @@ export const OrderValue = (props: Props): ReactNode => { return ( {intl.formatMessage(messages.shipping)}{" "} - + {shippingMethodName} - + ); } @@ -288,12 +258,12 @@ export const OrderValue = (props: Props): ReactNode => { return ( - {intl.formatMessage(messages.setShippingMethod)} - + ); }; @@ -339,9 +309,9 @@ export const OrderValue = (props: Props): ReactNode => { > - + {intl.formatMessage(messages.addDiscount)} - + @@ -382,9 +352,9 @@ export const OrderValue = (props: Props): ReactNode => { > - + {discountDisplayValue} - + From c4fe0644238e4b7e1a734c3bcbb69c2c264a315f Mon Sep 17 00:00:00 2001 From: Mirek Mencel Date: Mon, 15 Dec 2025 22:19:33 -0100 Subject: [PATCH 50/84] Simplify conditional rendering of shipping messages --- .../components/OrderSummary/OrderValue.tsx | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/src/orders/components/OrderSummary/OrderValue.tsx b/src/orders/components/OrderSummary/OrderValue.tsx index ab44d30fe3c..144e02d1646 100644 --- a/src/orders/components/OrderSummary/OrderValue.tsx +++ b/src/orders/components/OrderSummary/OrderValue.tsx @@ -236,21 +236,13 @@ export const OrderValue = (props: Props): ReactNode => { const hasShippingAddress = !!editableProps?.shippingAddress; - if (!hasShippingAddress) { + if (!hasShippingAddress || !hasShippingMethods) { return ( - {intl.formatMessage(messages.noShippingAddress)} - - - ); - } - - if (!hasShippingMethods) { - return ( - - - {intl.formatMessage(messages.noShippingMethods)} + {intl.formatMessage( + !hasShippingAddress ? messages.noShippingAddress : messages.noShippingMethods, + )} ); From feb3c07a6c60045e9613c4bf3d6ef49fa694a1df Mon Sep 17 00:00:00 2001 From: Mirek Mencel Date: Mon, 15 Dec 2025 22:23:27 -0100 Subject: [PATCH 51/84] Extract messages --- locale/defaultMessages.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/locale/defaultMessages.json b/locale/defaultMessages.json index e64a85f5644..d51fd24931f 100644 --- a/locale/defaultMessages.json +++ b/locale/defaultMessages.json @@ -5825,6 +5825,10 @@ "YIT1XP": { "string": "Create voucher" }, + "YPCB7b": { + "context": "label for fixed amount discount type", + "string": "Fixed amount" + }, "YQ3EXR": { "context": "product types section name", "string": "Product Types" From fbe015afc8d56ed708a53a193eeb1f3ae1538148 Mon Sep 17 00:00:00 2001 From: Mirek Mencel Date: Mon, 15 Dec 2025 22:34:38 -0100 Subject: [PATCH 52/84] When isClearable is true, selecting "No shipping method" is intentional, so the button stays enabled --- locale/defaultMessages.json | 8 ++++---- .../OrderShippingMethodEditDialog.tsx | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/locale/defaultMessages.json b/locale/defaultMessages.json index 8fea85a08d2..1ea3100297a 100644 --- a/locale/defaultMessages.json +++ b/locale/defaultMessages.json @@ -5849,14 +5849,14 @@ "YIT1XP": { "string": "Create voucher" }, - "YPCB7b": { - "context": "label for fixed amount discount type", - "string": "Fixed amount" - }, "YL8K/3": { "context": "automatic completion delay input label", "string": "Delay before completion (minutes). Default is 30." }, + "YPCB7b": { + "context": "label for fixed amount discount type", + "string": "Fixed amount" + }, "YQ3EXR": { "context": "product types section name", "string": "Product Types" diff --git a/src/orders/components/OrderShippingMethodEditDialog/OrderShippingMethodEditDialog.tsx b/src/orders/components/OrderShippingMethodEditDialog/OrderShippingMethodEditDialog.tsx index 67f88a9f6f1..174b6791093 100644 --- a/src/orders/components/OrderShippingMethodEditDialog/OrderShippingMethodEditDialog.tsx +++ b/src/orders/components/OrderShippingMethodEditDialog/OrderShippingMethodEditDialog.tsx @@ -224,6 +224,7 @@ const OrderShippingMethodEditDialog = (props: OrderShippingMethodEditDialogProps data-test-id="confirm-button" transitionState={confirmButtonState} onClick={submit} + disabled={!isClearable && data.shippingMethod === NO_SHIPPING_METHOD_ID} > From 4fac6c6926b1624c5571a1764f68da20f07fb45c Mon Sep 17 00:00:00 2001 From: Mirek Mencel Date: Mon, 15 Dec 2025 22:44:42 -0100 Subject: [PATCH 53/84] Fix tests --- src/orders/components/OrderSummary/OrderValue.test.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/orders/components/OrderSummary/OrderValue.test.tsx b/src/orders/components/OrderSummary/OrderValue.test.tsx index 4840d0045ce..5ade8808c0b 100644 --- a/src/orders/components/OrderSummary/OrderValue.test.tsx +++ b/src/orders/components/OrderSummary/OrderValue.test.tsx @@ -216,7 +216,7 @@ describe("OrderValue", () => { const setShippingLink = screen.getByText("Set shipping method"); expect(setShippingLink).toBeInTheDocument(); - expect(setShippingLink.tagName).toBe("A"); + expect(setShippingLink.tagName).toBe("BUTTON"); }); it("should call onShippingMethodEdit when 'Set shipping method' link is clicked", async () => { @@ -267,7 +267,7 @@ describe("OrderValue", () => { const methodLink = screen.getByText("Standard Shipping"); expect(methodLink).toBeInTheDocument(); - expect(methodLink.tagName).toBe("A"); + expect(methodLink.tagName).toBe("BUTTON"); await userEvent.click(methodLink); expect(onShippingMethodEdit).toHaveBeenCalledTimes(1); @@ -415,7 +415,8 @@ describe("OrderValue", () => { // Assert expect(screen.getByText("Discount")).toBeInTheDocument(); - expect(screen.getByText("15.00")).toBeInTheDocument(); + expect(screen.getByText("Fixed amount")).toBeInTheDocument(); + expect(screen.getByText("15")).toBeInTheDocument(); }); it("should show discount reason as tooltip on existing discount", () => { From c0cf716ea0861ba401a15038ed88c038e8b4de74 Mon Sep 17 00:00:00 2001 From: Lukasz Ostrowski Date: Tue, 16 Dec 2025 10:41:28 +0100 Subject: [PATCH 54/84] cleanup release workflow (#6193) * cleanup release workflow * fix workflow --- .github/workflows/cleanup-after-release.yml | 89 +++++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 .github/workflows/cleanup-after-release.yml diff --git a/.github/workflows/cleanup-after-release.yml b/.github/workflows/cleanup-after-release.yml new file mode 100644 index 00000000000..7961af45f23 --- /dev/null +++ b/.github/workflows/cleanup-after-release.yml @@ -0,0 +1,89 @@ +name: Cleanup After Release + +on: + push: + branches: + - "3.22" + +permissions: + contents: write + pull-requests: write + +jobs: + # When a release PR (changeset-release/3.22) is merged to the release branch, + # we need to sync those changes back to main to keep it up to date. + # This workflow creates a PR from the release branch to main for cleanup. + create-cleanup-pr: + runs-on: ubuntu-22.04 + # Only run if the push is from merging a PR (not direct commits) + if: github.event.head_commit.message != '' && contains(github.event.head_commit.message, 'Merge pull request') + env: + CURRENT_RELEASE: "3.22" + + steps: + - name: Get Token + id: get-token + uses: saleor/saleor-internal-actions/request-vault-token@v1.4.0 + with: + vault-url: ${{ secrets.VAULT_URL }} + vault-jwt: ${{ secrets.VAULT_JWT }} + + - name: Checkout repository + uses: actions/checkout@v4 + with: + token: ${{ steps.get-token.outputs.token }} + fetch-depth: 0 # Fetch all history to get proper version info + + - name: Configure Git + run: | + git config user.name "Saleor Deployments" + git config user.email "deployments@saleor.io" + + - name: Check if merged PR was from changeset-release + id: check-release + run: | + # Get the commit message to check if it's from changeset-release branch + COMMIT_MSG="${{ github.event.head_commit.message }}" + + if echo "$COMMIT_MSG" | grep -q "from saleor/changeset-release/$CURRENT_RELEASE"; then + echo "is_release_merge=true" >> $GITHUB_OUTPUT + echo "✅ This is a release PR merge from changeset-release/$CURRENT_RELEASE" + else + echo "is_release_merge=false" >> $GITHUB_OUTPUT + echo "ℹ️ This is not a release PR merge, skipping cleanup PR creation" + fi + + - name: Get version from package.json + if: steps.check-release.outputs.is_release_merge == 'true' + id: get-version + run: | + VERSION=$(node -p "require('./package.json').version") + echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "📦 Version: $VERSION" + + - name: Create cleanup PR + if: steps.check-release.outputs.is_release_merge == 'true' + env: + GH_TOKEN: ${{ steps.get-token.outputs.token }} + run: | + VERSION="${{ steps.get-version.outputs.version }}" + + # Fetch latest changes + git fetch origin + + echo "Creating new cleanup PR from $CURRENT_RELEASE to main" + + gh pr create \ + --base main \ + --head "$CURRENT_RELEASE" \ + --title "Clean up after release $VERSION" \ + --body "$(cat < Date: Tue, 16 Dec 2025 12:06:13 +0100 Subject: [PATCH 55/84] Fix customer->order filter redirection (#6213) --- .changeset/wide-signs-love.md | 5 +++++ .../components/CustomerDetailsPage/CustomerDetailsPage.tsx | 6 ++---- src/orders/urls.ts | 5 +++++ 3 files changed, 12 insertions(+), 4 deletions(-) create mode 100644 .changeset/wide-signs-love.md diff --git a/.changeset/wide-signs-love.md b/.changeset/wide-signs-love.md new file mode 100644 index 00000000000..116a7186ee9 --- /dev/null +++ b/.changeset/wide-signs-love.md @@ -0,0 +1,5 @@ +--- +"saleor-dashboard": patch +--- + +Fixed redirection between Customer Details -> See all orders to Orders List (which this customer selected). Now filter is properly set on the URL and only relevant orders are displayed diff --git a/src/customers/components/CustomerDetailsPage/CustomerDetailsPage.tsx b/src/customers/components/CustomerDetailsPage/CustomerDetailsPage.tsx index ef2e02e7202..da10fac8dff 100644 --- a/src/customers/components/CustomerDetailsPage/CustomerDetailsPage.tsx +++ b/src/customers/components/CustomerDetailsPage/CustomerDetailsPage.tsx @@ -20,7 +20,7 @@ import { useBackLinkWithState } from "@dashboard/hooks/useBackLinkWithState"; import { SubmitPromise } from "@dashboard/hooks/useForm"; import useNavigator from "@dashboard/hooks/useNavigator"; import { sectionNames } from "@dashboard/intl"; -import { orderListUrl } from "@dashboard/orders/urls"; +import { orderListUrlWithCustomerEmail } from "@dashboard/orders/urls"; import { mapEdgesToItems, mapMetadataItemToInput } from "@dashboard/utils/maps"; import { Divider } from "@saleor/macaw-ui-next"; import { useIntl } from "react-intl"; @@ -111,9 +111,7 @@ const CustomerDetailsPage = ({ diff --git a/src/orders/urls.ts b/src/orders/urls.ts index 550e27a40cc..a0ebbf59dc6 100644 --- a/src/orders/urls.ts +++ b/src/orders/urls.ts @@ -78,6 +78,11 @@ export type OrderListUrlQueryParams = BulkAction & OrderListUrlSort & Pagination & ActiveTab; + +/** + * @deprecated + * This helper is likely broken, at least filters don't work. Either construct url manually or fix it + */ export const orderListUrl = (params?: OrderListUrlQueryParams): string => { const orderList = orderListPath; From 955f177b895f4c2b206b485fba71b7c9a26f7c0e Mon Sep 17 00:00:00 2001 From: Lukasz Ostrowski Date: Tue, 16 Dec 2025 12:06:32 +0100 Subject: [PATCH 56/84] Release 3.22 (#6211) (#6212) Co-authored-by: saleor-deployments[bot] <97954499+saleor-deployments[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] --- .changeset/breezy-steaks-sit.md | 5 ----- .changeset/free-ravens-work.md | 5 ----- .changeset/fruity-hornets-jump.md | 5 ----- .changeset/good-hats-push.md | 5 ----- .changeset/lazy-worlds-prove.md | 5 ----- .changeset/small-baboons-eat.md | 5 ----- .changeset/two-pens-camp.md | 5 ----- .changeset/witty-kiwis-dress.md | 5 ----- CHANGELOG.md | 20 ++++++++++++++++++++ package.json | 2 +- 10 files changed, 21 insertions(+), 41 deletions(-) delete mode 100644 .changeset/breezy-steaks-sit.md delete mode 100644 .changeset/free-ravens-work.md delete mode 100644 .changeset/fruity-hornets-jump.md delete mode 100644 .changeset/good-hats-push.md delete mode 100644 .changeset/lazy-worlds-prove.md delete mode 100644 .changeset/small-baboons-eat.md delete mode 100644 .changeset/two-pens-camp.md delete mode 100644 .changeset/witty-kiwis-dress.md diff --git a/.changeset/breezy-steaks-sit.md b/.changeset/breezy-steaks-sit.md deleted file mode 100644 index a501643ca93..00000000000 --- a/.changeset/breezy-steaks-sit.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"saleor-dashboard": patch ---- - -Invalid event sent from app to dashboard will not throw anymore, but gracefully show notification diff --git a/.changeset/free-ravens-work.md b/.changeset/free-ravens-work.md deleted file mode 100644 index 44f72f6ed36..00000000000 --- a/.changeset/free-ravens-work.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"saleor-dashboard": patch ---- - -Fix and redesign Order history diff --git a/.changeset/fruity-hornets-jump.md b/.changeset/fruity-hornets-jump.md deleted file mode 100644 index 348cebd395a..00000000000 --- a/.changeset/fruity-hornets-jump.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"saleor-dashboard": patch ---- - -Fix main title overflow diff --git a/.changeset/good-hats-push.md b/.changeset/good-hats-push.md deleted file mode 100644 index bb5601a3f50..00000000000 --- a/.changeset/good-hats-push.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"saleor-dashboard": patch ---- - -Cleanup order's Customer Details and Addresses sections diff --git a/.changeset/lazy-worlds-prove.md b/.changeset/lazy-worlds-prove.md deleted file mode 100644 index 4fb9be6de9f..00000000000 --- a/.changeset/lazy-worlds-prove.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"saleor-dashboard": patch ---- - -New label for ConfirmButton error state diff --git a/.changeset/small-baboons-eat.md b/.changeset/small-baboons-eat.md deleted file mode 100644 index e68fc243584..00000000000 --- a/.changeset/small-baboons-eat.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"saleor-dashboard": patch ---- - -Clipboard operations no longer crash the website if browser permissions are not enabled diff --git a/.changeset/two-pens-camp.md b/.changeset/two-pens-camp.md deleted file mode 100644 index ab759ce9175..00000000000 --- a/.changeset/two-pens-camp.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"saleor-dashboard": patch ---- - -Improve icons color consistency with timeline diff --git a/.changeset/witty-kiwis-dress.md b/.changeset/witty-kiwis-dress.md deleted file mode 100644 index 716e0a3a305..00000000000 --- a/.changeset/witty-kiwis-dress.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"saleor-dashboard": patch ---- - -Added UI to control "automatic checkout completion" from the dashboard settings page. What previously was allowed only via graphQL, now can be controlled easily by staff. diff --git a/CHANGELOG.md b/CHANGELOG.md index 622f1f0650b..ca45b29bfa8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,25 @@ # Changelog +## 3.22.21 + +### Patch Changes + +- [#6196](https://github.com/saleor/saleor-dashboard/pull/6196) [`5f22cde`](https://github.com/saleor/saleor-dashboard/commit/5f22cded76c3736447dae769b6564fe6fd05e4d9) Thanks [@lkostrowski](https://github.com/lkostrowski)! - Invalid event sent from app to dashboard will not throw anymore, but gracefully show notification + +- [#6150](https://github.com/saleor/saleor-dashboard/pull/6150) [`e0f798c`](https://github.com/saleor/saleor-dashboard/commit/e0f798cf09b7fb761febdc57b653155644fe6329) Thanks [@mirekm](https://github.com/mirekm)! - Fix and redesign Order history + +- [#6205](https://github.com/saleor/saleor-dashboard/pull/6205) [`fb40e57`](https://github.com/saleor/saleor-dashboard/commit/fb40e57ab0c6a5842c87fbfe03c3e2486019e3b9) Thanks [@mirekm](https://github.com/mirekm)! - Fix main title overflow + +- [#6168](https://github.com/saleor/saleor-dashboard/pull/6168) [`c366f94`](https://github.com/saleor/saleor-dashboard/commit/c366f9487affa218ab04ff0b9c0ebfae54a585ed) Thanks [@mirekm](https://github.com/mirekm)! - Cleanup order's Customer Details and Addresses sections + +- [#6204](https://github.com/saleor/saleor-dashboard/pull/6204) [`dacee7d`](https://github.com/saleor/saleor-dashboard/commit/dacee7de926a45c8c8ee91284882e18fcdb35fae) Thanks [@mirekm](https://github.com/mirekm)! - New label for ConfirmButton error state + +- [#6196](https://github.com/saleor/saleor-dashboard/pull/6196) [`5f22cde`](https://github.com/saleor/saleor-dashboard/commit/5f22cded76c3736447dae769b6564fe6fd05e4d9) Thanks [@lkostrowski](https://github.com/lkostrowski)! - Clipboard operations no longer crash the website if browser permissions are not enabled + +- [#6206](https://github.com/saleor/saleor-dashboard/pull/6206) [`a0893de`](https://github.com/saleor/saleor-dashboard/commit/a0893deb0a4b78ba4c69a6a220d48049ac4af78c) Thanks [@mirekm](https://github.com/mirekm)! - Improve icons color consistency with timeline + +- [#6208](https://github.com/saleor/saleor-dashboard/pull/6208) [`bcb4854`](https://github.com/saleor/saleor-dashboard/commit/bcb485469a0327a518deb3d91dfb68b8501c8e61) Thanks [@lkostrowski](https://github.com/lkostrowski)! - Added UI to control "automatic checkout completion" from the dashboard settings page. What previously was allowed only via graphQL, now can be controlled easily by staff. + ## 3.22.20 ### Patch Changes diff --git a/package.json b/package.json index 3433df156b0..3260e2dcf69 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "saleor-dashboard", - "version": "3.22.20", + "version": "3.22.21", "repository": { "type": "git", "url": "git://github.com/saleor/saleor-dashboard.git" From c6848f5578aa3ef636b4c9d4eb1ad14957f62d91 Mon Sep 17 00:00:00 2001 From: Mirek Mencel Date: Tue, 16 Dec 2025 11:37:44 -0100 Subject: [PATCH 57/84] Update transactions (#6178) * Initial redesign of the history timeline * Improve the timeline view * Use Macau's Textarea for the comment * Fix the "Let's fix the comment's "dirty" state * Fix Order history header size * Clean up avatars a bit * Fix test * Nudges * Extract messages * Add changeset * Refactor and clean up * Add Cmd+Enter support for order notes * Extract messages * Use semantic color token for icons * Refactor * Clean up the design a bit * Fix tests * Fix Copilot's suggestions * Refactor and i18n date grouping * Use simpler `{props; Props}` * Refactor isMessageEmpty * Add hint about Cmd+K * Better error handling and memization of safeStringify * Simplify comment card and get rid of inline styles * More i18n * Add preliminary apps support * Address some more copilot feedback * Extract messages * More improvements * Add grouping by date tests * More tests * Externalise CSS instead of injecting * Drop default export * Refactor into subcomponents * Limit the safeStringify to the essentials considering our logic * Drop `any` assertion * Refactor shortcuts into a standalone component * Address copilot's suggestion * Add KeyboardShortcut tests * Simplify app attribution and drop link to appUrl * Link app to it's Dashboard's view * Add copying component * Further simplify and refactor * Extract messages * Get rid of default exports * Fix importing style * Improve lines handling * Improve list ids uniquness * Dont render date group header if <= 1 * Extract messages * Improve "an order" -> "the order" in event titles * Further refactor Timeline * Drop moment.js * Extract messages * Fix tests * Extract messages * Match Transactions and Refunds header size and weight * Turn transactions into foldable cards * Improve the event lines * Fix highlight color and minor tweaks * Adjust external PSP reference links * Try to fix width for the transaction event columns * More column fixes * Header nudges * Fix actions * Add changeset * Extract messages * Extend the transactions fixture * Fix types * Change linked events highlight to lighter * Icons in Macau are deprecated * Prefer type declarations over assertions * Use CSS modules * More sprinkles * Drop antipattern * Replace Macaw icon with Lucide counterparts * More tests * Add tests for CopyableText component * Improve CopyableText for better accessibility * Drop index file for CopyableText component * Accept also the 2nd key as a property in KeyboardShortcutHint * Turn CSS into CSS module * Simplify the avatar URL passing * Just make the `name` optional * Pass props only needed by the component * Rename component and drop barrle index export * Get rid of the timeout, just use animation * Simplify the date props * Let's drop `// @ts-strict-ignore` * Fix relative dates utils import and remove ts-strict-ignore * Simplify condition in getOrderNumberLink function * Improve keys generation * Refactor transaction fixture into its own file * Refactor the transaction title back to separate variable * Move util outside of the component * Refactor another util into a separate function * Handle invalid date * Move styles out to CSS module * Refactor Copy to clipboard message to the common intl * Make sure the listenre is cleared * Rename folder and file to match the new component name * Finish transition from generic keyboard hint to specific one * Split CSS module into smaller ones between the corresponding components * Move to a single actor union; Refactor some helpers to utils * Drop the barrel imports * Let the parent component decide the format of date display * Drop comment, improve clarity * Refactor to CSS module * Do the link Macaw way * Align chevron in OrderTransaction card title * Extract messages * Improve i18n * Extract messages --------- Co-authored-by: Lukasz Ostrowski --- .changeset/crisp-shoes-care.md | 5 + locale/defaultMessages.json | 11 + src/components/CopyableText/CopyableText.tsx | 7 +- src/intl.ts | 10 + .../OrderTransaction.module.css | 13 + .../OrderTransaction/OrderTransaction.tsx | 72 ++++-- .../components/CardTitle/CardTitle.test.tsx | 165 +++++++++++++ .../components/CardTitle/CardTitle.tsx | 226 ++++++++++-------- .../components/CardTitle/MoneyDisplay.tsx | 12 +- .../TransactionEvents/TransactionEvents.tsx | 25 +- .../components/EventAvatar.tsx | 79 ++++++ .../components/EventItem.test.tsx | 57 ++--- .../components/EventItem.tsx | 121 ++++------ .../components/EventTime.tsx | 15 +- .../components/EventType.module.css | 7 + .../components/EventType.tsx | 34 +-- .../components/PspReference.module.css | 17 ++ .../components/PspReference.tsx | 159 ++++++------ .../components/PspReferenceLink.tsx | 30 +-- src/orders/fixtures/OrderFixture.ts | 204 ++++++++++++---- src/orders/fixtures/TransactionFixture.ts | 188 +++++++++++++++ 21 files changed, 1068 insertions(+), 389 deletions(-) create mode 100644 .changeset/crisp-shoes-care.md create mode 100644 src/orders/components/OrderTransaction/OrderTransaction.module.css create mode 100644 src/orders/components/OrderTransaction/components/CardTitle/CardTitle.test.tsx create mode 100644 src/orders/components/OrderTransaction/components/TransactionEvents/components/EventAvatar.tsx create mode 100644 src/orders/components/OrderTransaction/components/TransactionEvents/components/EventType.module.css create mode 100644 src/orders/components/OrderTransaction/components/TransactionEvents/components/PspReference.module.css create mode 100644 src/orders/fixtures/TransactionFixture.ts diff --git a/.changeset/crisp-shoes-care.md b/.changeset/crisp-shoes-care.md new file mode 100644 index 00000000000..8768c0d52e0 --- /dev/null +++ b/.changeset/crisp-shoes-care.md @@ -0,0 +1,5 @@ +--- +"saleor-dashboard": patch +--- + +Update order Transactions cards diff --git a/locale/defaultMessages.json b/locale/defaultMessages.json index a6659372443..357a8e636db 100644 --- a/locale/defaultMessages.json +++ b/locale/defaultMessages.json @@ -6082,6 +6082,10 @@ "context": "ProductTypeDeleteWarningDialog single assigned items description", "string": "You are about to delete product type {typeName}. It is assigned to {assignedItemsCount} {assignedItemsCount,plural,one{product} other{products}}. Deleting this product type will also delete those products. Are you sure you want to do this?" }, + "ZGmd4h": { + "context": "button", + "string": "Copy to clipboard" + }, "ZHF4Z9": { "context": "delete product dialog subtitle", "string": "Are you sure you want to delete {name}?" @@ -6682,6 +6686,9 @@ "context": "WarehouseSettings no shipping zones assigned", "string": "This warehouse has no shipping zones assigned." }, + "ce2kVF": { + "string": "View in payment provider" + }, "ce5Hp1": { "string": "Failed to copy to clipboard" }, @@ -8405,6 +8412,10 @@ "context": "PageTypeDeleteWarningDialog title", "string": "Delete page {selectedTypesCount,plural,one{type} other{types}}" }, + "oIS3NK": { + "context": "button", + "string": "Show more" + }, "oIvtua": { "context": "attribute's editor component", "string": "Catalog Input type for Store Owner" diff --git a/src/components/CopyableText/CopyableText.tsx b/src/components/CopyableText/CopyableText.tsx index 96e3732734a..a2aaa124e7b 100644 --- a/src/components/CopyableText/CopyableText.tsx +++ b/src/components/CopyableText/CopyableText.tsx @@ -1,4 +1,5 @@ import { useClipboard } from "@dashboard/hooks/useClipboard"; +import { buttonMessages } from "@dashboard/intl"; import { Box, Button, sprinkles, Text } from "@saleor/macaw-ui-next"; import { CheckIcon, CopyIcon } from "lucide-react"; import { useState } from "react"; @@ -42,10 +43,8 @@ export const CopyableText = ({ text }: CopyableTextProps): JSX.Element => { ) } onClick={() => copy(text)} - aria-label={intl.formatMessage({ - defaultMessage: "Copy to clipboard", - id: "aCdAsI", - })} + title={intl.formatMessage(buttonMessages.copyToClipboard)} + aria-label={intl.formatMessage(buttonMessages.copyToClipboard)} /> diff --git a/src/intl.ts b/src/intl.ts index 354590ebe2f..7d1d0ef715e 100644 --- a/src/intl.ts +++ b/src/intl.ts @@ -340,6 +340,11 @@ export const buttonMessages = defineMessages({ defaultMessage: "Manage", description: "button", }, + moreOptions: { + id: "oIS3NK", + defaultMessage: "Show more", + description: "button", + }, nextStep: { id: "wlQTfb", defaultMessage: "Next", @@ -404,6 +409,11 @@ export const buttonMessages = defineMessages({ id: "rbrahO", defaultMessage: "Close", }, + copyToClipboard: { + id: "ZGmd4h", + defaultMessage: "Copy to clipboard", + description: "button", + }, proceed: { id: "VNX4fn", defaultMessage: "Proceed", diff --git a/src/orders/components/OrderTransaction/OrderTransaction.module.css b/src/orders/components/OrderTransaction/OrderTransaction.module.css new file mode 100644 index 00000000000..3ce7adf995d --- /dev/null +++ b/src/orders/components/OrderTransaction/OrderTransaction.module.css @@ -0,0 +1,13 @@ +/* Chevron rotation for accordion */ +.chevron { + display: flex; + align-items: center; + justify-content: center; + transition: transform 0.2s ease; + transform: rotate(-90deg); +} + +button[data-state="open"] .chevron, +[data-state="open"] .chevron { + transform: rotate(0deg); +} diff --git a/src/orders/components/OrderTransaction/OrderTransaction.tsx b/src/orders/components/OrderTransaction/OrderTransaction.tsx index 9b743d428a5..e16beef8811 100644 --- a/src/orders/components/OrderTransaction/OrderTransaction.tsx +++ b/src/orders/components/OrderTransaction/OrderTransaction.tsx @@ -1,11 +1,15 @@ // @ts-strict-ignore -import { DashboardCard } from "@dashboard/components/Card"; +import { iconSize, iconStrokeWidthBySize } from "@dashboard/components/icons"; import { TransactionActionEnum } from "@dashboard/graphql"; import { TransactionFakeEvent } from "@dashboard/orders/types"; +import { Accordion, Box } from "@saleor/macaw-ui-next"; +import { ChevronDown } from "lucide-react"; import * as React from "react"; +import { useState } from "react"; import { OrderTransactionCardTitle } from "./components"; import { TransactionEvents } from "./components/TransactionEvents"; +import styles from "./OrderTransaction.module.css"; import { ExtendedOrderTransaction } from "./types"; import { getTransactionEvents } from "./utils"; @@ -16,6 +20,7 @@ export interface OrderTransactionProps { showActions?: boolean; cardFooter?: React.ReactNode; disabled?: boolean; + defaultExpanded?: boolean; } const OrderTransaction = ({ @@ -25,24 +30,63 @@ const OrderTransaction = ({ showActions, cardFooter, disabled = false, + defaultExpanded = true, }: OrderTransactionProps) => { const events = getTransactionEvents(transaction, fakeEvents); + const [expanded, setExpanded] = useState( + defaultExpanded ? transaction.id : undefined, + ); return ( - - - - + + + + + + + + + + } + /> + + - - - {cardFooter} - - + + + + {cardFooter} + + + + + + ); }; diff --git a/src/orders/components/OrderTransaction/components/CardTitle/CardTitle.test.tsx b/src/orders/components/OrderTransaction/components/CardTitle/CardTitle.test.tsx new file mode 100644 index 00000000000..0c8b9e66368 --- /dev/null +++ b/src/orders/components/OrderTransaction/components/CardTitle/CardTitle.test.tsx @@ -0,0 +1,165 @@ +import { TransactionActionEnum, TransactionItemFragment } from "@dashboard/graphql"; +import { prepareMoney, transactions } from "@dashboard/orders/fixtures"; +import Wrapper from "@test/wrapper"; +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; + +import { ExtendedOrderTransaction } from "../../types"; +import { OrderTransactionCardTitle } from "./CardTitle"; + +const createTransaction = ( + overrides: Partial & { index?: number } = {}, +): ExtendedOrderTransaction => ({ + ...transactions.chargeSuccess[0], + index: 0, + ...overrides, +}); + +describe("OrderTransactionCardTitle", () => { + describe("amounts display logic", () => { + it("only displays amounts greater than zero", () => { + // Arrange + const transaction = createTransaction({ + chargedAmount: prepareMoney(100), + authorizedAmount: prepareMoney(0), + refundedAmount: prepareMoney(50), + canceledAmount: prepareMoney(0), + chargePendingAmount: prepareMoney(0), + authorizePendingAmount: prepareMoney(0), + refundPendingAmount: prepareMoney(0), + cancelPendingAmount: prepareMoney(0), + }); + + // Act + render( + + + , + ); + + // Assert - only non-zero amounts should be displayed + expect(screen.getByText("Charged")).toBeInTheDocument(); + expect(screen.getByText("Refunded")).toBeInTheDocument(); + expect(screen.queryByText("Authorized")).not.toBeInTheDocument(); + expect(screen.queryByText("Canceled")).not.toBeInTheDocument(); + }); + }); + + describe("actions menu logic", () => { + it("filters out REFUND action from menu", async () => { + // Arrange + const user = userEvent.setup(); + const transaction = createTransaction({ + actions: [ + TransactionActionEnum.CHARGE, + TransactionActionEnum.REFUND, + TransactionActionEnum.CANCEL, + ], + }); + + // Act + render( + + + , + ); + await user.click(screen.getByTestId("transaction-menu-button")); + + // Assert - REFUND is handled separately in Send Refund view + expect(screen.getByText("Capture")).toBeInTheDocument(); + expect(screen.getByText("Cancel")).toBeInTheDocument(); + expect(screen.queryByText("Refund")).not.toBeInTheDocument(); + }); + + it("hides menu when showActions is false even if actions exist", () => { + // Arrange + const transaction = createTransaction({ + actions: [TransactionActionEnum.CHARGE, TransactionActionEnum.CANCEL], + }); + + // Act + render( + + + , + ); + + // Assert + expect(screen.queryByTestId("transaction-menu-button")).not.toBeInTheDocument(); + }); + + it("hides menu when actions array is empty", () => { + // Arrange + const transaction = createTransaction({ actions: [] }); + + // Act + render( + + + , + ); + + // Assert + expect(screen.queryByTestId("transaction-menu-button")).not.toBeInTheDocument(); + }); + + it("hides menu when only REFUND action is available", () => { + // Arrange - REFUND gets filtered out, leaving no actions + const transaction = createTransaction({ + actions: [TransactionActionEnum.REFUND], + }); + + // Act + render( + + + , + ); + + // Assert + expect(screen.queryByTestId("transaction-menu-button")).not.toBeInTheDocument(); + }); + + it("calls onTransactionAction with correct transaction id and action type", async () => { + // Arrange + const user = userEvent.setup(); + const onTransactionAction = jest.fn(); + const transaction = createTransaction({ + id: "txn-abc-123", + actions: [TransactionActionEnum.CANCEL], + }); + + // Act + render( + + + , + ); + await user.click(screen.getByTestId("transaction-menu-button")); + await user.click(screen.getByText("Cancel")); + + // Assert + expect(onTransactionAction).toHaveBeenCalledWith("txn-abc-123", TransactionActionEnum.CANCEL); + }); + }); +}); diff --git a/src/orders/components/OrderTransaction/components/CardTitle/CardTitle.tsx b/src/orders/components/OrderTransaction/components/CardTitle/CardTitle.tsx index 27445a86040..9246f1ffec9 100644 --- a/src/orders/components/OrderTransaction/components/CardTitle/CardTitle.tsx +++ b/src/orders/components/OrderTransaction/components/CardTitle/CardTitle.tsx @@ -1,8 +1,8 @@ -import { ButtonLink } from "@dashboard/components/ButtonLink"; -import { iconSize, iconStrokeWidth } from "@dashboard/components/icons"; +import { iconSize, iconStrokeWidthBySize } from "@dashboard/components/icons"; import { TransactionActionEnum } from "@dashboard/graphql"; -import { Box, Button, Text } from "@saleor/macaw-ui-next"; -import { ExternalLink } from "lucide-react"; +import { buttonMessages } from "@dashboard/intl"; +import { Box, Button, Dropdown, List, Text } from "@saleor/macaw-ui-next"; +import { ExternalLink, MoreVertical } from "lucide-react"; import { FormattedMessage, useIntl } from "react-intl"; import { OrderTransactionProps } from "../../OrderTransaction"; @@ -12,18 +12,61 @@ import { EventTime } from "../TransactionEvents/components/EventTime"; import { messages } from "./messages"; import { MoneyDisplay } from "./MoneyDisplay"; +const isDestructiveAction = (action: TransactionActionEnum) => + action === TransactionActionEnum.CANCEL || action === TransactionActionEnum.REFUND; + +const getTransactionAmounts = ({ + chargedAmount, + authorizedAmount, + refundedAmount, + canceledAmount, + chargePendingAmount, + authorizePendingAmount, + refundPendingAmount, + cancelPendingAmount, +}: ExtendedOrderTransaction) => + [ + { label: messages.charged, money: chargedAmount, show: chargedAmount.amount > 0 }, + { label: messages.authorized, money: authorizedAmount, show: authorizedAmount.amount > 0 }, + { label: messages.refunded, money: refundedAmount, show: refundedAmount.amount > 0 }, + { label: messages.canceled, money: canceledAmount, show: canceledAmount.amount > 0 }, + { + label: messages.chargePending, + money: chargePendingAmount, + show: chargePendingAmount.amount > 0, + }, + { + label: messages.authorizePending, + money: authorizePendingAmount, + show: authorizePendingAmount.amount > 0, + }, + { + label: messages.refundPending, + money: refundPendingAmount, + show: refundPendingAmount.amount > 0, + }, + { + label: messages.cancelPending, + money: cancelPendingAmount, + show: cancelPendingAmount.amount > 0, + }, + ].filter(item => item.show); + interface CardTitleProps { transaction: ExtendedOrderTransaction; onTransactionAction: OrderTransactionProps["onTransactionAction"]; showActions?: boolean; + chevron?: React.ReactNode; } const TransactionTitle = ({ transaction, index, + chevron, }: { transaction: ExtendedOrderTransaction; index: number; + chevron?: React.ReactNode; }) => { const intl = useIntl(); @@ -39,10 +82,34 @@ const TransactionTitle = ({ ); return ( - - {transactionTitle} + + + {chevron} + + {transactionTitle} + + {transaction.externalUrl && ( + e.stopPropagation()} + color={{ default: "default2", hover: "default1" }} + __transition="color 0.15s ease-in-out" + title={intl.formatMessage({ + defaultMessage: "View in payment provider", + id: "ce2kVF", + })} + > + + + )} + {transaction.name && ( - + {transaction.name} )} @@ -54,104 +121,67 @@ export const OrderTransactionCardTitle = ({ transaction, onTransactionAction, showActions = true, + chevron, }: CardTitleProps) => { const intl = useIntl(); - const { - refundedAmount, - refundPendingAmount, - authorizePendingAmount, - cancelPendingAmount, - chargePendingAmount, - canceledAmount, - chargedAmount, - authorizedAmount, - index = 0, - } = transaction; + const { index = 0 } = transaction; const actions = transaction.actions.filter(action => action !== TransactionActionEnum.REFUND); - const showActionButtons = showActions && actions.length > 0; + const showActionsMenu = showActions && actions.length > 0; - return ( - - {transaction.externalUrl ? ( - - + // Collect all non-zero amounts for display + const amounts = getTransactionAmounts(transaction); - - - ) : ( - - - - )} - - - {cancelPendingAmount.amount > 0 && ( - - )} - - {canceledAmount.amount > 0 && ( - - )} - - {refundPendingAmount.amount > 0 && ( - - )} - - {refundedAmount.amount > 0 && ( - - )} - - {chargePendingAmount.amount > 0 && ( - - )} - - {chargedAmount.amount > 0 && ( - - )} - - {authorizePendingAmount.amount > 0 && ( - - )} - - {authorizedAmount.amount > 0 && ( - - )} - - {showActionButtons && ( - - {actions.map(action => ( -
- -
- ))} -
+ return ( + + + + + {amounts.map(({ label, money }) => ( + + ))} + + {showActionsMenu && ( + + + )} {canRefund && ( @@ -51,26 +50,17 @@ export const LegacyPaymentsApiButtons = ({ onClick={onLegacyPaymentsApiRefund} data-test-id="refund-button" > - {intl.formatMessage({ - defaultMessage: "Refund", - id: "IeUH3/", - })} + {intl.formatMessage(transactionActionMessages.refund)} )} {canVoid && ( )} {canMarkAsPaid && onMarkAsPaid && ( )} diff --git a/src/orders/components/OrderSummary/TransactionsApiButtons.tsx b/src/orders/components/OrderSummary/TransactionsApiButtons.tsx index a2aeb213376..8eb01823d7f 100644 --- a/src/orders/components/OrderSummary/TransactionsApiButtons.tsx +++ b/src/orders/components/OrderSummary/TransactionsApiButtons.tsx @@ -2,6 +2,8 @@ import { Button } from "@saleor/macaw-ui-next"; import { CheckIcon } from "lucide-react"; import { useIntl } from "react-intl"; +import { transactionActionMessages } from "../OrderTransaction/messages"; + type Props = { hasNoPayment: boolean; canMarkAsPaid: boolean; @@ -18,10 +20,7 @@ export const TransactionsApiButtons = ({ hasNoPayment, canMarkAsPaid, onMarkAsPa return ( ); }; diff --git a/src/orders/components/OrderTransaction/messages.ts b/src/orders/components/OrderTransaction/messages.ts index 077c74cdc86..69e6e095155 100644 --- a/src/orders/components/OrderTransaction/messages.ts +++ b/src/orders/components/OrderTransaction/messages.ts @@ -16,4 +16,14 @@ export const transactionActionMessages = defineMessages({ id: "iIfq2+", description: "Transaction cancel button - return preauthorized amount to client", }, + refund: { + defaultMessage: "Refund", + description: "Transaction refund button - return captured amount to client", + id: "8HmEqK", + }, + markAsPaid: { + defaultMessage: "Mark as Paid", + description: "Button to manually mark order as paid without actual payment", + id: "SDgGcU", + }, }); From fd03505f225a7e00062edacb4dbfa589a9c88414 Mon Sep 17 00:00:00 2001 From: Mirek Mencel Date: Tue, 16 Dec 2025 20:57:38 -0100 Subject: [PATCH 59/84] Let's use Box instead of custom CSS for PaymentsSummaryEmptyState component --- .../PaymentsSummaryEmptyState.module.css | 26 ------------------- .../PaymentsSummaryEmptyState.tsx | 24 +++++++++++++---- 2 files changed, 19 insertions(+), 31 deletions(-) delete mode 100644 src/orders/components/OrderSummary/PaymentsSummaryEmptyState.module.css diff --git a/src/orders/components/OrderSummary/PaymentsSummaryEmptyState.module.css b/src/orders/components/OrderSummary/PaymentsSummaryEmptyState.module.css deleted file mode 100644 index 513aaa5996c..00000000000 --- a/src/orders/components/OrderSummary/PaymentsSummaryEmptyState.module.css +++ /dev/null @@ -1,26 +0,0 @@ -.emptyState { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - padding: 32px 16px; - gap: 0; -} - -.iconWrapper { - display: flex; - align-items: center; - justify-content: center; - width: 56px; - height: 56px; - border-radius: 50%; - background-color: var(--macaw-color-background-default2); -} - -.textContainer { - display: flex; - flex-direction: column; - align-items: center; - gap: 4px; - text-align: center; -} diff --git a/src/orders/components/OrderSummary/PaymentsSummaryEmptyState.tsx b/src/orders/components/OrderSummary/PaymentsSummaryEmptyState.tsx index 4f80454f6a9..b77ce891f5f 100644 --- a/src/orders/components/OrderSummary/PaymentsSummaryEmptyState.tsx +++ b/src/orders/components/OrderSummary/PaymentsSummaryEmptyState.tsx @@ -2,17 +2,31 @@ import { Box, Text } from "@saleor/macaw-ui-next"; import { CreditCard } from "lucide-react"; import { useIntl } from "react-intl"; -import styles from "./PaymentsSummaryEmptyState.module.css"; - export const PaymentsSummaryEmptyState = () => { const intl = useIntl(); return ( - - + + - + {intl.formatMessage({ defaultMessage: "No payment received", From 1de52c705404b7f19f65626ceac9aa731482adae Mon Sep 17 00:00:00 2001 From: Mirek Mencel Date: Tue, 16 Dec 2025 21:32:16 -0100 Subject: [PATCH 60/84] Improve the contract on Mark as Paid --- src/orders/components/OrderSummary/TransactionsApiButtons.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/orders/components/OrderSummary/TransactionsApiButtons.tsx b/src/orders/components/OrderSummary/TransactionsApiButtons.tsx index 8eb01823d7f..6af193fcb11 100644 --- a/src/orders/components/OrderSummary/TransactionsApiButtons.tsx +++ b/src/orders/components/OrderSummary/TransactionsApiButtons.tsx @@ -7,13 +7,13 @@ import { transactionActionMessages } from "../OrderTransaction/messages"; type Props = { hasNoPayment: boolean; canMarkAsPaid: boolean; - onMarkAsPaid?: () => void; + onMarkAsPaid: () => void; }; export const TransactionsApiButtons = ({ hasNoPayment, canMarkAsPaid, onMarkAsPaid }: Props) => { const intl = useIntl(); - if (!hasNoPayment || !canMarkAsPaid || !onMarkAsPaid) { + if (!hasNoPayment || !canMarkAsPaid) { return null; } From 28a013c3636f2cbd94451f0a6b17fb9e557dee5d Mon Sep 17 00:00:00 2001 From: Mirek Mencel Date: Tue, 16 Dec 2025 21:32:56 -0100 Subject: [PATCH 61/84] Improve the change description --- .changeset/lemon-candies-crash.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/lemon-candies-crash.md diff --git a/.changeset/lemon-candies-crash.md b/.changeset/lemon-candies-crash.md new file mode 100644 index 00000000000..25143609154 --- /dev/null +++ b/.changeset/lemon-candies-crash.md @@ -0,0 +1,5 @@ +--- +"saleor-dashboard": patch +--- + +Redesigned Order Summary section with unified payment status display for both (legacy) Payments API and Transactions API From 25301a22e97b3f710005e08ac6bedb89c7907ac4 Mon Sep 17 00:00:00 2001 From: Mirek Mencel Date: Wed, 17 Dec 2025 08:27:09 -0100 Subject: [PATCH 62/84] Remove the prior changelog files --- .changeset/deep-ducks-sniff.md | 5 ----- .changeset/few-bushes-ask.md | 5 ----- 2 files changed, 10 deletions(-) delete mode 100644 .changeset/deep-ducks-sniff.md delete mode 100644 .changeset/few-bushes-ask.md diff --git a/.changeset/deep-ducks-sniff.md b/.changeset/deep-ducks-sniff.md deleted file mode 100644 index 642e6dffc9c..00000000000 --- a/.changeset/deep-ducks-sniff.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"saleor-dashboard": patch ---- - -Improve Order summary section diff --git a/.changeset/few-bushes-ask.md b/.changeset/few-bushes-ask.md deleted file mode 100644 index 2619b8d8bf7..00000000000 --- a/.changeset/few-bushes-ask.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"saleor-dashboard": patch ---- - -Unify Draft and Unconfirmed order info with the rest of the statuses From 70500a9acf8aaf04c5c380e8cb192dd9fd04e000 Mon Sep 17 00:00:00 2001 From: Mirek Mencel Date: Wed, 17 Dec 2025 08:38:48 -0100 Subject: [PATCH 63/84] Render only when onMarkAsPaid is defined --- src/orders/components/OrderSummary/OrderSummary.tsx | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/orders/components/OrderSummary/OrderSummary.tsx b/src/orders/components/OrderSummary/OrderSummary.tsx index bbcc665a9f8..516f9460c1f 100644 --- a/src/orders/components/OrderSummary/OrderSummary.tsx +++ b/src/orders/components/OrderSummary/OrderSummary.tsx @@ -82,11 +82,13 @@ export const OrderSummary = (props: Props) => { } /> ) : ( - + onMarkAsPaid && ( + + ) )} From ee30448b4322c7c7ce501cde828621690bf2a4a2 Mon Sep 17 00:00:00 2001 From: Mirek Mencel Date: Wed, 17 Dec 2025 09:48:11 -0100 Subject: [PATCH 64/84] Improve changelog desc --- .changeset/lemon-candies-crash.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/lemon-candies-crash.md b/.changeset/lemon-candies-crash.md index 25143609154..d6b03bcff17 100644 --- a/.changeset/lemon-candies-crash.md +++ b/.changeset/lemon-candies-crash.md @@ -2,4 +2,4 @@ "saleor-dashboard": patch --- -Redesigned Order Summary section with unified payment status display for both (legacy) Payments API and Transactions API +Introduced a redesigned "Order summary" section that unifies order details and payment information across all order types, including Drafts and Unconfirmed orders. The updated "Order value" breakdown now clearly separates subtotal, taxes, discounts, and shipping. Additionally, a new "Payments summary" section has been added, featuring a dedicated "no data" state when no payments are present. From bf5cd908b71192208b0607368f0c3f140ca91700 Mon Sep 17 00:00:00 2001 From: Mirek Mencel Date: Wed, 17 Dec 2025 23:11:31 -0100 Subject: [PATCH 65/84] Refactor to use defaultZeroMoney constant for default money values --- .../OrderUnconfirmedDetails/index.tsx | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/src/orders/views/OrderDetails/OrderUnconfirmedDetails/index.tsx b/src/orders/views/OrderDetails/OrderUnconfirmedDetails/index.tsx index 2b249a7b740..c833724edee 100644 --- a/src/orders/views/OrderDetails/OrderUnconfirmedDetails/index.tsx +++ b/src/orders/views/OrderDetails/OrderUnconfirmedDetails/index.tsx @@ -169,6 +169,7 @@ export const OrderUnconfirmedDetails = ({ const intl = useIntl(); const [transactionReference, setTransactionReference] = useState(""); const errors = orderUpdate.opts.data?.orderUpdate.errors || []; + const defaultZeroMoney = { amount: 0, currency: "USD" }; const hasOrderFulfillmentsFulFilled = order?.fulfillments.some( fulfillment => fulfillment.status === FulfillmentStatus.FULFILLED, @@ -372,20 +373,14 @@ export const OrderUnconfirmedDetails = ({ t.id === params.id)?.authorizedAmount ?? { - amount: 0, - currency: "USD", - } + order?.transactions?.find(t => t.id === params.id)?.authorizedAmount ?? defaultZeroMoney } chargedAmount={ - order?.transactions?.find(t => t.id === params.id)?.chargedAmount ?? { - amount: 0, - currency: "USD", - } + order?.transactions?.find(t => t.id === params.id)?.chargedAmount ?? defaultZeroMoney } - orderBalance={order?.totalBalance ?? { amount: 0, currency: "USD" }} + orderBalance={order?.totalBalance ?? defaultZeroMoney} isTransaction open={true} onClose={closeModal} @@ -438,8 +433,8 @@ export const OrderUnconfirmedDetails = ({ From 0a8d88ff5f23c170bb57b0f50210b6d5537c8ebf Mon Sep 17 00:00:00 2001 From: Mirek Mencel Date: Wed, 17 Dec 2025 23:18:30 -0100 Subject: [PATCH 66/84] Optimise transaction amount retrieval with useMemo --- .../OrderDetails/OrderUnconfirmedDetails/index.tsx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/orders/views/OrderDetails/OrderUnconfirmedDetails/index.tsx b/src/orders/views/OrderDetails/OrderUnconfirmedDetails/index.tsx index c833724edee..b8c23068d1f 100644 --- a/src/orders/views/OrderDetails/OrderUnconfirmedDetails/index.tsx +++ b/src/orders/views/OrderDetails/OrderUnconfirmedDetails/index.tsx @@ -34,7 +34,7 @@ import { OrderLineDiscountProvider } from "@dashboard/products/components/OrderD import { useOrderVariantSearch } from "@dashboard/searches/useOrderVariantSearch"; import { PartialMutationProviderOutput } from "@dashboard/types"; import { mapEdgesToItems } from "@dashboard/utils/maps"; -import { useState } from "react"; +import { useMemo, useState } from "react"; import { useIntl } from "react-intl"; import { customerUrl } from "../../../../customers/urls"; @@ -170,6 +170,10 @@ export const OrderUnconfirmedDetails = ({ const [transactionReference, setTransactionReference] = useState(""); const errors = orderUpdate.opts.data?.orderUpdate.errors || []; const defaultZeroMoney = { amount: 0, currency: "USD" }; + const selectedTransaction = useMemo( + () => order?.transactions?.find(t => t.id === params.id), + [order?.transactions, params.id], + ); const hasOrderFulfillmentsFulFilled = order?.fulfillments.some( fulfillment => fulfillment.status === FulfillmentStatus.FULFILLED, @@ -374,12 +378,8 @@ export const OrderUnconfirmedDetails = ({ confirmButtonState={orderTransactionAction.opts.status} errors={orderTransactionAction.opts.data?.transactionRequestAction?.errors ?? []} orderTotal={order?.total.gross ?? defaultZeroMoney} - authorizedAmount={ - order?.transactions?.find(t => t.id === params.id)?.authorizedAmount ?? defaultZeroMoney - } - chargedAmount={ - order?.transactions?.find(t => t.id === params.id)?.chargedAmount ?? defaultZeroMoney - } + authorizedAmount={selectedTransaction?.authorizedAmount ?? defaultZeroMoney} + chargedAmount={selectedTransaction?.chargedAmount ?? defaultZeroMoney} orderBalance={order?.totalBalance ?? defaultZeroMoney} isTransaction open={true} From 95360896f862d12ff4aa476dfac6fd6106bf6f57 Mon Sep 17 00:00:00 2001 From: Mirek Mencel Date: Wed, 17 Dec 2025 23:27:17 -0100 Subject: [PATCH 67/84] Add separate transaction-charge-action URL param; Fixed data refetch after transaction actions --- src/orders/urls.ts | 3 ++- .../views/OrderDetails/OrderDetails.tsx | 7 +++++- .../OrderDetails/OrderNormalDetails/index.tsx | 24 ++++++++++++------- .../OrderUnconfirmedDetails/index.tsx | 24 ++++++++++++------- 4 files changed, 38 insertions(+), 20 deletions(-) diff --git a/src/orders/urls.ts b/src/orders/urls.ts index a0ebbf59dc6..fe1755ee71c 100644 --- a/src/orders/urls.ts +++ b/src/orders/urls.ts @@ -209,6 +209,7 @@ export type OrderUrlDialog = | "mark-paid" | "void" | "transaction-action" + | "transaction-charge-action" | "invoice-send" | "add-manual-transaction" | "view-order-line-metadata" @@ -216,7 +217,7 @@ export type OrderUrlDialog = | "view-fulfillment-metadata"; interface TransactionAction { - action: "transaction-action"; + action: "transaction-action" | "transaction-charge-action"; id: string; type: TransactionActionEnum; } diff --git a/src/orders/views/OrderDetails/OrderDetails.tsx b/src/orders/views/OrderDetails/OrderDetails.tsx index 5c5b43609df..cdfb1b2f3e5 100644 --- a/src/orders/views/OrderDetails/OrderDetails.tsx +++ b/src/orders/views/OrderDetails/OrderDetails.tsx @@ -138,7 +138,12 @@ const OrderDetails = ({ id, params }: OrderDetailsProps) => { } }} onInvoiceSend={orderMessages.handleInvoiceSend} - onTransactionActionSend={orderMessages.handleTransactionAction} + onTransactionActionSend={async data => { + await apolloClient.refetchQueries({ + include: [OrderDetailsWithMetadataDocument], + }); + orderMessages.handleTransactionAction(data); + }} onManualTransactionAdded={async data => { await apolloClient.refetchQueries({ include: [OrderDetailsWithMetadataDocument], diff --git a/src/orders/views/OrderDetails/OrderNormalDetails/index.tsx b/src/orders/views/OrderDetails/OrderNormalDetails/index.tsx index 218c30de4f0..dffac2305d1 100644 --- a/src/orders/views/OrderDetails/OrderNormalDetails/index.tsx +++ b/src/orders/views/OrderDetails/OrderNormalDetails/index.tsx @@ -232,11 +232,19 @@ export const OrderNormalDetails = ({ onOrderShowMetadata={() => openModal("view-order-metadata")} onFulfillmentShowMetadata={id => openModal("view-fulfillment-metadata", { id })} onTransactionAction={(id, action) => - openModal("transaction-action", { - type: action, - id, - action: "transaction-action", - }) + openModal( + action === TransactionActionEnum.CHARGE + ? "transaction-charge-action" + : "transaction-action", + { + type: action, + id, + action: + action === TransactionActionEnum.CHARGE + ? "transaction-charge-action" + : "transaction-action", + }, + ) } onOrderFulfill={() => navigate(orderFulfillUrl(id))} onFulfillmentApprove={fulfillmentId => @@ -305,7 +313,7 @@ export const OrderNormalDetails = ({ } /> {/* Transaction Capture Dialog - for CHARGE action */} - {params.action === "transaction-action" && params.type === TransactionActionEnum.CHARGE && ( + {params.action === "transaction-charge-action" && ( orderTransactionAction diff --git a/src/orders/views/OrderDetails/OrderUnconfirmedDetails/index.tsx b/src/orders/views/OrderDetails/OrderUnconfirmedDetails/index.tsx index b8c23068d1f..0340111c657 100644 --- a/src/orders/views/OrderDetails/OrderUnconfirmedDetails/index.tsx +++ b/src/orders/views/OrderDetails/OrderUnconfirmedDetails/index.tsx @@ -219,11 +219,19 @@ export const OrderUnconfirmedDetails = ({ order={order} shop={shop} onTransactionAction={(id, action) => - openModal("transaction-action", { - type: action, - id, - action: "transaction-action", - }) + openModal( + action === TransactionActionEnum.CHARGE + ? "transaction-charge-action" + : "transaction-action", + { + type: action, + id, + action: + action === TransactionActionEnum.CHARGE + ? "transaction-charge-action" + : "transaction-action", + }, + ) } onOrderLineAdd={() => openModal("add-order-line")} onOrderLineChange={(id, data) => @@ -373,7 +381,7 @@ export const OrderUnconfirmedDetails = ({ handleTransactionReference={({ target }) => setTransactionReference(target.value)} /> {/* Transaction Capture Dialog - for CHARGE action */} - {params.action === "transaction-action" && params.type === TransactionActionEnum.CHARGE && ( + {params.action === "transaction-charge-action" && ( orderTransactionAction From a78b453f18894bfa7c29225df8cf85569af7613d Mon Sep 17 00:00:00 2001 From: Mirek Mencel Date: Wed, 17 Dec 2025 23:39:17 -0100 Subject: [PATCH 68/84] Simplify the summary message --- locale/defaultMessages.json | 16 ++--- .../OrderCaptureDialog/OrderCaptureDialog.tsx | 66 +++++++------------ .../components/OrderCaptureDialog/messages.ts | 18 ++--- 3 files changed, 32 insertions(+), 68 deletions(-) diff --git a/locale/defaultMessages.json b/locale/defaultMessages.json index 357a8e636db..a470ecbb363 100644 --- a/locale/defaultMessages.json +++ b/locale/defaultMessages.json @@ -815,6 +815,10 @@ "context": "filter range separator", "string": "and" }, + "34wIzn": { + "context": "outcome prediction showing resulting order status after capture", + "string": "This will result in {status} order" + }, "38dS1A": { "context": "code ending with label", "string": "Code ending with {last4CodeChars}" @@ -2459,10 +2463,6 @@ "DWWw3M": { "string": "Model type Name" }, - "DXaxpH": { - "context": "outcome prediction when order will be overcharged", - "string": "This will result in an {status} order" - }, "DaPGcn": { "string": "Model title" }, @@ -5470,10 +5470,6 @@ "context": "command menu shortcut", "string": "Command menu" }, - "VwCTbx": { - "context": "outcome prediction when order will be fully charged", - "string": "This will result in a {status} order" - }, "VxStyU": { "context": "order history message", "string": "Replacement order was created" @@ -10059,10 +10055,6 @@ "context": "warehouses section name", "string": "Warehouses" }, - "ycg2RR": { - "context": "outcome prediction when order will be partially charged", - "string": "This will result in a {status} order" - }, "ychKsb": { "context": "error message", "string": "Shipping method is required for this order" diff --git a/src/orders/components/OrderCaptureDialog/OrderCaptureDialog.tsx b/src/orders/components/OrderCaptureDialog/OrderCaptureDialog.tsx index 01cf228a148..bed32dd629f 100644 --- a/src/orders/components/OrderCaptureDialog/OrderCaptureDialog.tsx +++ b/src/orders/components/OrderCaptureDialog/OrderCaptureDialog.tsx @@ -489,48 +489,30 @@ export const OrderCaptureDialog = ({ {canSubmit && selectedAmount > 0 && ( - {outcomeStatus === "overcharged" && ( - - ), - }} - /> - )} - {outcomeStatus === "fullyCharged" && ( - - ), - }} - /> - )} - {outcomeStatus === "partiallyCharged" && ( - - ), - }} - /> - )} + + ), + }} + /> )} diff --git a/src/orders/components/OrderCaptureDialog/messages.ts b/src/orders/components/OrderCaptureDialog/messages.ts index 21416d288c6..2a3b21831b4 100644 --- a/src/orders/components/OrderCaptureDialog/messages.ts +++ b/src/orders/components/OrderCaptureDialog/messages.ts @@ -113,20 +113,10 @@ export const messages = defineMessages({ "No payment has been authorized for this order. The full amount of {amount} cannot be captured.", description: "error when no authorization exists", }, - outcomeFullyCharged: { - id: "VwCTbx", - defaultMessage: "This will result in a {status} order", - description: "outcome prediction when order will be fully charged", - }, - outcomePartiallyCharged: { - id: "ycg2RR", - defaultMessage: "This will result in a {status} order", - description: "outcome prediction when order will be partially charged", - }, - outcomeOvercharged: { - id: "DXaxpH", - defaultMessage: "This will result in an {status} order", - description: "outcome prediction when order will be overcharged", + outcomeMessage: { + id: "HSYM17", + defaultMessage: "This will result in {status} order", + description: "outcome prediction showing resulting order status after capture", }, statusFullyCapturedPill: { id: "G9y5Ze", From e6677cd36a134cf7fd915e51f5494f423f0f6e07 Mon Sep 17 00:00:00 2001 From: Mirek Mencel Date: Wed, 17 Dec 2025 23:49:14 -0100 Subject: [PATCH 69/84] Extract messages --- locale/defaultMessages.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/locale/defaultMessages.json b/locale/defaultMessages.json index 6d5952515b8..5f8c7917784 100644 --- a/locale/defaultMessages.json +++ b/locale/defaultMessages.json @@ -812,10 +812,6 @@ "context": "filter range separator", "string": "and" }, - "34wIzn": { - "context": "outcome prediction showing resulting order status after capture", - "string": "This will result in {status} order" - }, "38dS1A": { "context": "code ending with label", "string": "Code ending with {last4CodeChars}" @@ -3131,6 +3127,10 @@ "context": "cta button label", "string": "Get in touch" }, + "HSYM17": { + "context": "outcome prediction showing resulting order status after capture", + "string": "This will result in {status} order" + }, "HSmg1/": { "context": "gift cards section name", "string": "Gift Cards" From e4186e9080b6523631e0b8c407197f21f793fd3e Mon Sep 17 00:00:00 2001 From: Mirek Mencel Date: Wed, 17 Dec 2025 23:49:56 -0100 Subject: [PATCH 70/84] Fix tests --- .../components/OrderCaptureDialog/OrderCaptureDialog.test.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/orders/components/OrderCaptureDialog/OrderCaptureDialog.test.tsx b/src/orders/components/OrderCaptureDialog/OrderCaptureDialog.test.tsx index 171c5b61da6..fecaa540693 100644 --- a/src/orders/components/OrderCaptureDialog/OrderCaptureDialog.test.tsx +++ b/src/orders/components/OrderCaptureDialog/OrderCaptureDialog.test.tsx @@ -431,7 +431,7 @@ describe("OrderCaptureDialog", () => { }); // Assert - outcome prediction message is shown - expect(screen.getByText(/This will result in a/)).toBeInTheDocument(); + expect(screen.getByText(/This will result in/)).toBeInTheDocument(); }); it("shows outcome message when capturing partial balance", () => { @@ -442,7 +442,7 @@ describe("OrderCaptureDialog", () => { }); // Assert - outcome prediction message is shown - expect(screen.getByText(/This will result in a/)).toBeInTheDocument(); + expect(screen.getByText(/This will result in/)).toBeInTheDocument(); }); }); From 60a9c5ce37ee5762629dc04fd546aad235d5607c Mon Sep 17 00:00:00 2001 From: Mirek Mencel Date: Sun, 21 Dec 2025 20:59:19 -0100 Subject: [PATCH 71/84] Rename messages to notifications --- src/auth/hooks/useAuthProvider.ts | 4 ++-- src/auth/utils.ts | 8 ++++---- .../NotificationProvider.test.tsx} | 12 ++++++------ .../NotificationProvider.tsx} | 10 +++++----- .../{messages => notifications}/ToastDebug.tsx | 0 src/components/{messages => notifications}/index.ts | 12 ++++++------ .../{messages => notifications}/sonnerStyles.css | 0 .../BackgroundTasks/BackgroundTasksProvider.tsx | 4 ++-- src/containers/BackgroundTasks/tasks.ts | 6 +++--- .../GiftCardBulkCreateDialog.tsx | 4 ++-- src/giftCards/GiftCardCreateDialog/utils.ts | 8 ++++---- .../GiftCardResendCodeDialog.tsx | 4 ++-- .../GiftCardUpdateBalanceDialog.tsx | 4 ++-- .../GiftCardListBulkActions.tsx | 6 +++--- src/hooks/useHandleFormSubmit.ts | 8 ++++---- src/hooks/useNotifier/useNotifier.tsx | 6 +++--- src/hooks/useNotifier/utils.ts | 7 +++++-- src/index.tsx | 6 +++--- .../views/OrderTransactionRefundCreate/handlers.ts | 4 ++-- .../views/OrderTransactionRefundEdit/handlers.ts | 4 ++-- 20 files changed, 60 insertions(+), 57 deletions(-) rename src/components/{messages/MessageManagerProvider.test.tsx => notifications/NotificationProvider.test.tsx} (68%) rename src/components/{messages/MessageManagerProvider.tsx => notifications/NotificationProvider.tsx} (85%) rename src/components/{messages => notifications}/ToastDebug.tsx (100%) rename src/components/{messages => notifications}/index.ts (52%) rename src/components/{messages => notifications}/sonnerStyles.css (100%) diff --git a/src/auth/hooks/useAuthProvider.ts b/src/auth/hooks/useAuthProvider.ts index 8b4b0d00da9..b1c3d1fa05e 100644 --- a/src/auth/hooks/useAuthProvider.ts +++ b/src/auth/hooks/useAuthProvider.ts @@ -1,5 +1,5 @@ import { ApolloClient, ApolloError } from "@apollo/client"; -import { IMessageContext } from "@dashboard/components/messages"; +import { INotificationCallback } from "@dashboard/components/notifications"; import { AccountErrorCode, useUserDetailsQuery } from "@dashboard/graphql"; import useLocalStorage from "@dashboard/hooks/useLocalStorage"; import useNavigator from "@dashboard/hooks/useNavigator"; @@ -29,7 +29,7 @@ import { useLastLoginMethod } from "./useLastLoginMethod"; interface UseAuthProviderOpts { intl: IntlShape; - notify: IMessageContext; + notify: INotificationCallback; apolloClient: ApolloClient; } type AuthErrorCodes = `${AccountErrorCode}`; diff --git a/src/auth/utils.ts b/src/auth/utils.ts index 19efcb42f03..61fc3dd29bb 100644 --- a/src/auth/utils.ts +++ b/src/auth/utils.ts @@ -1,5 +1,5 @@ import { ApolloError, ServerError } from "@apollo/client/core"; -import { IMessage, IMessageContext } from "@dashboard/components/messages"; +import { INotification, INotificationCallback } from "@dashboard/components/notifications"; import { commonMessages } from "@dashboard/intl"; import { getMutationErrors, parseLogMessage } from "@dashboard/misc"; import { getAppMountUriForRedirect } from "@dashboard/utils/urls"; @@ -41,7 +41,7 @@ export const showAllErrors = ({ notify, error, }: { - notify: IMessageContext; + notify: INotificationCallback; error: ApolloError; }) => { getAllErrorMessages(error).forEach(message => { @@ -60,7 +60,7 @@ export const handleNestedMutationErrors = ({ }: { data: any; intl: IntlShape; - notify: (message: IMessage) => void; + notify: (notification: INotification) => void; }) => { const mutationErrors = getMutationErrors({ data }); @@ -82,7 +82,7 @@ export const handleNestedMutationErrors = ({ export async function handleQueryAuthError( error: ApolloError, - notify: IMessageContext, + notify: INotificationCallback, logout: () => void, intl: IntlShape, ) { diff --git a/src/components/messages/MessageManagerProvider.test.tsx b/src/components/notifications/NotificationProvider.test.tsx similarity index 68% rename from src/components/messages/MessageManagerProvider.test.tsx rename to src/components/notifications/NotificationProvider.test.tsx index 0abdd2669fc..04b4d021dc0 100644 --- a/src/components/messages/MessageManagerProvider.test.tsx +++ b/src/components/notifications/NotificationProvider.test.tsx @@ -2,8 +2,8 @@ import { ThemeProvider } from "@saleor/macaw-ui-next"; import { renderHook } from "@testing-library/react-hooks"; import { PropsWithChildren, useContext } from "react"; -import { MessageContext } from "."; -import MessageManagerProvider from "./MessageManagerProvider"; +import { NotificationContext } from "."; +import NotificationProvider from "./NotificationProvider"; // Mock sonner jest.mock("sonner", () => ({ @@ -17,16 +17,16 @@ jest.mock("sonner", () => ({ Toaster: () => null, })); -describe("MessageManagerProvider", () => { +describe("NotificationProvider", () => { const wrapper = ({ children }: PropsWithChildren) => ( - {children} + {children} ); - test("should provide MessageContext to children", () => { + test("should provide NotificationContext to children", () => { // Arrange & Act - const { result } = renderHook(() => useContext(MessageContext), { wrapper }); + const { result } = renderHook(() => useContext(NotificationContext), { wrapper }); // Assert expect(result.current).toBeTruthy(); diff --git a/src/components/messages/MessageManagerProvider.tsx b/src/components/notifications/NotificationProvider.tsx similarity index 85% rename from src/components/messages/MessageManagerProvider.tsx rename to src/components/notifications/NotificationProvider.tsx index 2c4960a9d88..7de4b757c2c 100644 --- a/src/components/messages/MessageManagerProvider.tsx +++ b/src/components/notifications/NotificationProvider.tsx @@ -5,10 +5,10 @@ import { AlertTriangle, CheckCircle2, Info, XCircle } from "lucide-react"; import { CSSProperties, PropsWithChildren } from "react"; import { toast, Toaster } from "sonner"; -import { INotificationContext, MessageContext } from "."; +import { INotificationContext, NotificationContext } from "."; import { ToastDebug } from "./ToastDebug"; -const MessageManagerProvider = ({ children }: PropsWithChildren) => { +const NotificationProvider = ({ children }: PropsWithChildren) => { const { theme, themeValues } = useTheme(); const isDark = theme === "defaultDark"; @@ -33,7 +33,7 @@ const MessageManagerProvider = ({ children }: PropsWithChildren) => { } as CSSProperties; return ( - + {children} { }} /> - + ); }; -export default MessageManagerProvider; +export default NotificationProvider; diff --git a/src/components/messages/ToastDebug.tsx b/src/components/notifications/ToastDebug.tsx similarity index 100% rename from src/components/messages/ToastDebug.tsx rename to src/components/notifications/ToastDebug.tsx diff --git a/src/components/messages/index.ts b/src/components/notifications/index.ts similarity index 52% rename from src/components/messages/index.ts rename to src/components/notifications/index.ts index 2bc178c118f..d8166c390bd 100644 --- a/src/components/messages/index.ts +++ b/src/components/notifications/index.ts @@ -2,7 +2,7 @@ import { createContext } from "react"; type Status = "success" | "error" | "info" | "warning"; -export interface IMessage { +export interface INotification { actionBtn?: { label: string; action: () => void; @@ -15,13 +15,13 @@ export interface IMessage { } export interface INotificationContext { - show: (message: IMessage, timeout?: number | null) => void; + show: (notification: INotification, timeout?: number | null) => void; remove: (notificationId: number) => void; clearErrorNotifications: () => void; } -export type IMessageContext = (message: IMessage) => void; -export const MessageContext = createContext(null); +export type INotificationCallback = (notification: INotification) => void; +export const NotificationContext = createContext(null); -export * from "./MessageManagerProvider"; -export { default } from "./MessageManagerProvider"; +export * from "./NotificationProvider"; +export { default } from "./NotificationProvider"; diff --git a/src/components/messages/sonnerStyles.css b/src/components/notifications/sonnerStyles.css similarity index 100% rename from src/components/messages/sonnerStyles.css rename to src/components/notifications/sonnerStyles.css diff --git a/src/containers/BackgroundTasks/BackgroundTasksProvider.tsx b/src/containers/BackgroundTasks/BackgroundTasksProvider.tsx index 41ec289b81e..bfb16986321 100644 --- a/src/containers/BackgroundTasks/BackgroundTasksProvider.tsx +++ b/src/containers/BackgroundTasks/BackgroundTasksProvider.tsx @@ -1,6 +1,6 @@ // @ts-strict-ignore import { ApolloClient, useApolloClient } from "@apollo/client"; -import { IMessageContext } from "@dashboard/components/messages"; +import { INotificationCallback } from "@dashboard/components/notifications"; import useNotifier from "@dashboard/hooks/useNotifier"; import { ReactNode, useEffect, useRef } from "react"; import { IntlShape, useIntl } from "react-intl"; @@ -14,7 +14,7 @@ export const backgroundTasksRefreshTime = 15 * 1000; export function useBackgroundTasks( apolloClient: Pick, "query">, - notify: IMessageContext, + notify: INotificationCallback, intl: IntlShape, ) { const idCounter = useRef(0); diff --git a/src/containers/BackgroundTasks/tasks.ts b/src/containers/BackgroundTasks/tasks.ts index 64e50ba8ea9..790b7014a8e 100644 --- a/src/containers/BackgroundTasks/tasks.ts +++ b/src/containers/BackgroundTasks/tasks.ts @@ -1,6 +1,6 @@ // @ts-strict-ignore import { ApolloQueryResult } from "@apollo/client"; -import { IMessageContext } from "@dashboard/components/messages"; +import { INotificationCallback } from "@dashboard/components/notifications"; import { CheckExportFileStatusQuery, CheckOrderInvoicesStatusQuery, @@ -76,7 +76,7 @@ export function queueInvoiceGenerate( generateInvoice: InvoiceGenerateParams, tasks: React.MutableRefObject, fetch: () => Promise>, - notify: IMessageContext, + notify: INotificationCallback, intl: IntlShape, ) { if (!generateInvoice) { @@ -117,7 +117,7 @@ export function queueExport( id: number, tasks: React.MutableRefObject, fetch: () => Promise>, - notify: IMessageContext, + notify: INotificationCallback, intl: IntlShape, ) { tasks.current = [ diff --git a/src/giftCards/GiftCardBulkCreateDialog/GiftCardBulkCreateDialog.tsx b/src/giftCards/GiftCardBulkCreateDialog/GiftCardBulkCreateDialog.tsx index 5107cc04f2b..f2f77a425f2 100644 --- a/src/giftCards/GiftCardBulkCreateDialog/GiftCardBulkCreateDialog.tsx +++ b/src/giftCards/GiftCardBulkCreateDialog/GiftCardBulkCreateDialog.tsx @@ -1,5 +1,5 @@ -import { IMessage } from "@dashboard/components/messages"; import { DashboardModal } from "@dashboard/components/Modal"; +import { INotification } from "@dashboard/components/notifications"; import { GiftCardBulkCreateInput, useGiftCardBulkCreateMutation } from "@dashboard/graphql"; import useCurrentDate from "@dashboard/hooks/useCurrentDate"; import useNotifier from "@dashboard/hooks/useNotifier"; @@ -54,7 +54,7 @@ export const GiftCardBulkCreateDialog = ({ onClose, open }: DialogProps) => { onCompleted: data => { const errors = data?.giftCardBulkCreate?.errors; const cardsAmount = data?.giftCardBulkCreate?.giftCards?.length || 0; - const giftCardsBulkIssueSuccessMessage: IMessage = { + const giftCardsBulkIssueSuccessMessage: INotification = { status: "success", title: intl.formatMessage(messages.createdSuccessAlertTitle), text: intl.formatMessage(messages.createdSuccessAlertDescription, { diff --git a/src/giftCards/GiftCardCreateDialog/utils.ts b/src/giftCards/GiftCardCreateDialog/utils.ts index 1623e0bc7d6..1714e1ce9a5 100644 --- a/src/giftCards/GiftCardCreateDialog/utils.ts +++ b/src/giftCards/GiftCardCreateDialog/utils.ts @@ -1,5 +1,5 @@ // @ts-strict-ignore -import { IMessage } from "@dashboard/components/messages"; +import { INotification } from "@dashboard/components/notifications"; import { GiftCardCreateMutation, TimePeriodTypeEnum } from "@dashboard/graphql"; import commonErrorMessages from "@dashboard/utils/errors/common"; import moment from "moment-timezone"; @@ -34,7 +34,7 @@ export const getExpiryPeriodTerminationDate = ( } }; -const getGiftCardExpiryError = (intl: IntlShape): IMessage => ({ +const getGiftCardExpiryError = (intl: IntlShape): INotification => ({ title: intl.formatMessage(giftCardUpdateFormMessages.giftCardInvalidExpiryDateHeader), text: intl.formatMessage(giftCardUpdateFormMessages.giftCardInvalidExpiryDateContent), status: "error", @@ -43,8 +43,8 @@ const getGiftCardExpiryError = (intl: IntlShape): IMessage => ({ export const getGiftCardCreateOnCompletedMessage = ( errors: GiftCardCreateMutation["giftCardCreate"]["errors"], intl: IntlShape, - successMessage?: IMessage, -): IMessage => { + successMessage?: INotification, +): INotification => { const hasExpiryError = errors.some(error => error.field === "expiryDate"); const successGiftCardMessage = successMessage || { status: "success", diff --git a/src/giftCards/GiftCardUpdate/GiftCardResendCodeDialog/GiftCardResendCodeDialog.tsx b/src/giftCards/GiftCardUpdate/GiftCardResendCodeDialog/GiftCardResendCodeDialog.tsx index 898a32c50d4..502dcafbf4a 100644 --- a/src/giftCards/GiftCardUpdate/GiftCardResendCodeDialog/GiftCardResendCodeDialog.tsx +++ b/src/giftCards/GiftCardUpdate/GiftCardResendCodeDialog/GiftCardResendCodeDialog.tsx @@ -2,7 +2,7 @@ import ActionDialog from "@dashboard/components/ActionDialog"; import { useChannelsSearch } from "@dashboard/components/ChannelsAvailabilityDialog/utils"; import { Combobox } from "@dashboard/components/Combobox"; -import { IMessage } from "@dashboard/components/messages"; +import { INotification } from "@dashboard/components/notifications"; import { useGiftCardPermissions } from "@dashboard/giftCards/hooks/useGiftCardPermissions"; import { useChannelsQuery, useGiftCardResendMutation } from "@dashboard/graphql"; import useForm from "@dashboard/hooks/useForm"; @@ -63,7 +63,7 @@ const GiftCardResendCodeDialog = ({ open, onClose }: DialogProps) => { const [resendGiftCardCode, resendGiftCardCodeOpts] = useGiftCardResendMutation({ onCompleted: data => { const errors = data?.giftCardResend?.errors; - const notifierData: IMessage = errors?.length + const notifierData: INotification = errors?.length ? { status: "error", text: intl.formatMessage(commonErrorMessages.unknownError), diff --git a/src/giftCards/GiftCardUpdate/GiftCardUpdateBalanceDialog/GiftCardUpdateBalanceDialog.tsx b/src/giftCards/GiftCardUpdate/GiftCardUpdateBalanceDialog/GiftCardUpdateBalanceDialog.tsx index d92abf0c630..0c42e050251 100644 --- a/src/giftCards/GiftCardUpdate/GiftCardUpdateBalanceDialog/GiftCardUpdateBalanceDialog.tsx +++ b/src/giftCards/GiftCardUpdate/GiftCardUpdateBalanceDialog/GiftCardUpdateBalanceDialog.tsx @@ -1,7 +1,7 @@ // @ts-strict-ignore import ActionDialog from "@dashboard/components/ActionDialog"; import CardSpacer from "@dashboard/components/CardSpacer"; -import { IMessage } from "@dashboard/components/messages"; +import { INotification } from "@dashboard/components/notifications"; import { useGiftCardPermissions } from "@dashboard/giftCards/hooks/useGiftCardPermissions"; import { useGiftCardUpdateMutation } from "@dashboard/graphql"; import useForm from "@dashboard/hooks/useForm"; @@ -37,7 +37,7 @@ const GiftCardUpdateBalanceDialog = ({ open, onClose }: DialogProps) => { const [updateGiftCardBalance, updateGiftCardBalanceOpts] = useGiftCardUpdateMutation({ onCompleted: data => { const errors = data?.giftCardUpdate?.errors; - const notifierData: IMessage = errors?.length + const notifierData: INotification = errors?.length ? { status: "error", text: intl.formatMessage(commonErrorMessages.unknownError), diff --git a/src/giftCards/GiftCardsList/GiftCardListBulkActions/GiftCardListBulkActions.tsx b/src/giftCards/GiftCardsList/GiftCardListBulkActions/GiftCardListBulkActions.tsx index 36d6c2eb65a..f0ccf67ac92 100644 --- a/src/giftCards/GiftCardsList/GiftCardListBulkActions/GiftCardListBulkActions.tsx +++ b/src/giftCards/GiftCardsList/GiftCardListBulkActions/GiftCardListBulkActions.tsx @@ -1,5 +1,5 @@ import { ConfirmButton } from "@dashboard/components/ConfirmButton"; -import { IMessage } from "@dashboard/components/messages"; +import { INotification } from "@dashboard/components/notifications"; import { useGiftCardBulkActivateMutation, useGiftCardBulkDeactivateMutation, @@ -30,7 +30,7 @@ export const GiftCardListBulkActions = () => { .every(({ isActive }) => !isActive); const [activateGiftCards, activateGiftCardsOpts] = useGiftCardBulkActivateMutation({ onCompleted: data => { - const notifierData: IMessage = data?.giftCardBulkActivate?.errors?.length + const notifierData: INotification = data?.giftCardBulkActivate?.errors?.length ? { status: "error", text: intl.formatMessage(messages.errorActivateAlertText, { @@ -54,7 +54,7 @@ export const GiftCardListBulkActions = () => { }); const [deactivateGiftCards, deactivateGiftCardsOpts] = useGiftCardBulkDeactivateMutation({ onCompleted: data => { - const notifierData: IMessage = data?.giftCardBulkDeactivate?.errors?.length + const notifierData: INotification = data?.giftCardBulkDeactivate?.errors?.length ? { status: "error", text: intl.formatMessage(messages.errorDeactivateAlertText, { diff --git a/src/hooks/useHandleFormSubmit.ts b/src/hooks/useHandleFormSubmit.ts index 6368652e731..0ccc6b97715 100644 --- a/src/hooks/useHandleFormSubmit.ts +++ b/src/hooks/useHandleFormSubmit.ts @@ -1,5 +1,5 @@ import { FormId, useExitFormDialog } from "@dashboard/components/Form"; -import { MessageContext } from "@dashboard/components/messages"; +import { NotificationContext } from "@dashboard/components/notifications"; import { SubmitPromise } from "@dashboard/hooks/useForm"; import { useContext } from "react"; @@ -15,13 +15,13 @@ function useHandleFormSubmit({ const { setIsSubmitting, setIsDirty } = useExitFormDialog({ formId, }); - const messageContext = useContext(MessageContext); + const notificationContext = useContext(NotificationContext); async function handleFormSubmit(data: TData): Promise { setIsSubmitting(true); - if (messageContext?.clearErrorNotifications) { - messageContext.clearErrorNotifications(); + if (notificationContext?.clearErrorNotifications) { + notificationContext.clearErrorNotifications(); } const result = onSubmit ? onSubmit(data) : null; diff --git a/src/hooks/useNotifier/useNotifier.tsx b/src/hooks/useNotifier/useNotifier.tsx index 45dca36d5bc..4edeecf9d36 100644 --- a/src/hooks/useNotifier/useNotifier.tsx +++ b/src/hooks/useNotifier/useNotifier.tsx @@ -1,11 +1,11 @@ -import { IMessage, IMessageContext } from "@dashboard/components/messages"; +import { INotification, INotificationCallback } from "@dashboard/components/notifications"; import { DEFAULT_NOTIFICATION_SHOW_TIME } from "@dashboard/config"; import { toast } from "sonner"; -export type UseNotifierResult = IMessageContext; +export type UseNotifierResult = INotificationCallback; function useNotifier(): UseNotifierResult { - const notify = (options: IMessage) => { + const notify = (options: INotification) => { const duration = options.status === "error" ? Infinity : (options.autohide ?? DEFAULT_NOTIFICATION_SHOW_TIME); diff --git a/src/hooks/useNotifier/utils.ts b/src/hooks/useNotifier/utils.ts index 9c4ab96cca3..8e8ad37ea0f 100644 --- a/src/hooks/useNotifier/utils.ts +++ b/src/hooks/useNotifier/utils.ts @@ -1,9 +1,12 @@ -import { IMessage } from "@dashboard/components/messages"; +import { INotification } from "@dashboard/components/notifications"; import { commonMessages } from "@dashboard/intl"; import commonErrorMessages from "@dashboard/utils/errors/common"; import { IntlShape } from "react-intl"; -export const getDefaultNotifierSuccessErrorData = (errors: any[], intl: IntlShape): IMessage => +export const getDefaultNotifierSuccessErrorData = ( + errors: any[], + intl: IntlShape, +): INotification => !errors.length ? { status: "success", diff --git a/src/index.tsx b/src/index.tsx index ca7a62d4201..09c7554af2a 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -36,8 +36,8 @@ import ErrorPage from "./components/ErrorPage"; import ExitFormDialogProvider from "./components/Form/ExitFormDialogProvider"; import { legacyRedirects } from "./components/LegacyRedirects"; import { LocaleProvider } from "./components/Locale"; -import MessageManagerProvider from "./components/messages"; import { NavigatorSearchProvider } from "./components/NavigatorSearch/NavigatorSearchProvider"; +import NotificationProvider from "./components/notifications"; import { ProductAnalytics } from "./components/ProductAnalytics"; import { SavebarRefProvider } from "./components/Savebar/SavebarRefContext"; import { ShopProvider } from "./components/Shop"; @@ -124,7 +124,7 @@ const App = () => ( - + @@ -150,7 +150,7 @@ const App = () => ( - + diff --git a/src/orders/views/OrderTransactionRefundCreate/handlers.ts b/src/orders/views/OrderTransactionRefundCreate/handlers.ts index d776ebf53af..0caef0888d8 100644 --- a/src/orders/views/OrderTransactionRefundCreate/handlers.ts +++ b/src/orders/views/OrderTransactionRefundCreate/handlers.ts @@ -1,4 +1,4 @@ -import { IMessage } from "@dashboard/components/messages"; +import { INotification } from "@dashboard/components/notifications"; import { OrderDetailsGrantRefundQuery, OrderGrantRefundAddMutation, @@ -25,7 +25,7 @@ export const handleRefundCreateComplete = ({ orderId, }: { submitData: OrderGrantRefundAddMutation; - notify: (message: IMessage) => void; + notify: (notification: INotification) => void; setLinesErrors: (value: React.SetStateAction) => void; navigate: UseNavigatorResult; intl: IntlShape; diff --git a/src/orders/views/OrderTransactionRefundEdit/handlers.ts b/src/orders/views/OrderTransactionRefundEdit/handlers.ts index 611b3afa340..3de42687af5 100644 --- a/src/orders/views/OrderTransactionRefundEdit/handlers.ts +++ b/src/orders/views/OrderTransactionRefundEdit/handlers.ts @@ -1,4 +1,4 @@ -import { IMessage } from "@dashboard/components/messages"; +import { INotification } from "@dashboard/components/notifications"; import { OrderGrantRefundEditMutation, OrderGrantRefundUpdateErrorCode, @@ -16,7 +16,7 @@ export const handleRefundEditComplete = ({ intl, }: { submitData: OrderGrantRefundEditMutation; - notify: (message: IMessage) => void; + notify: (notification: INotification) => void; setLinesErrors: (value: React.SetStateAction) => void; intl: IntlShape; orderId: string; From f3089138710de48b61ac7fdfc6b1d78bdcdfffa1 Mon Sep 17 00:00:00 2001 From: Mirek Mencel Date: Sun, 21 Dec 2025 21:06:04 -0100 Subject: [PATCH 72/84] Fix i18n and consilidate common messages --- src/components/Callout/messages.ts | 19 ++++++------------- src/hooks/useNotifier/useNotifier.tsx | 12 ++++++++---- src/intl.ts | 12 ++++++++++++ 3 files changed, 26 insertions(+), 17 deletions(-) diff --git a/src/components/Callout/messages.ts b/src/components/Callout/messages.ts index 012b5169465..33c76528603 100644 --- a/src/components/Callout/messages.ts +++ b/src/components/Callout/messages.ts @@ -1,14 +1,7 @@ -import { defineMessages } from "react-intl"; +import { commonMessages } from "@dashboard/intl"; -export const calloutTitleMessages = defineMessages({ - info: { - defaultMessage: "Info", - description: "callout, title", - id: "BnB/7Y", - }, - warning: { - defaultMessage: "Warning", - description: "callout, title", - id: "UUVUyy", - }, -}); +// Re-export common messages for backward compatibility +export const calloutTitleMessages = { + info: commonMessages.info, + warning: commonMessages.warning, +}; diff --git a/src/hooks/useNotifier/useNotifier.tsx b/src/hooks/useNotifier/useNotifier.tsx index 4edeecf9d36..979ee426e4e 100644 --- a/src/hooks/useNotifier/useNotifier.tsx +++ b/src/hooks/useNotifier/useNotifier.tsx @@ -1,10 +1,14 @@ import { INotification, INotificationCallback } from "@dashboard/components/notifications"; import { DEFAULT_NOTIFICATION_SHOW_TIME } from "@dashboard/config"; +import { commonMessages } from "@dashboard/intl"; +import { useIntl } from "react-intl"; import { toast } from "sonner"; export type UseNotifierResult = INotificationCallback; function useNotifier(): UseNotifierResult { + const intl = useIntl(); + const notify = (options: INotification) => { const duration = options.status === "error" ? Infinity : (options.autohide ?? DEFAULT_NOTIFICATION_SHOW_TIME); @@ -25,17 +29,17 @@ function useNotifier(): UseNotifierResult { switch (options.status) { case "success": - toast.success(options.title || "Success", toastOptions); + toast.success(options.title || intl.formatMessage(commonMessages.success), toastOptions); break; case "error": - toast.error(options.title || "Error", toastOptions); + toast.error(options.title || intl.formatMessage(commonMessages.error), toastOptions); break; case "warning": - toast.warning(options.title || "Warning", toastOptions); + toast.warning(options.title || intl.formatMessage(commonMessages.warning), toastOptions); break; case "info": default: - toast.info(options.title || "Info", toastOptions); + toast.info(options.title || intl.formatMessage(commonMessages.info), toastOptions); break; } }; diff --git a/src/intl.ts b/src/intl.ts index 354590ebe2f..c991f76da35 100644 --- a/src/intl.ts +++ b/src/intl.ts @@ -5,6 +5,18 @@ export const commonMessages = defineMessages({ id: "hOxIeP", defaultMessage: "Availability", }, + success: { + id: "xrKHS6", + defaultMessage: "Success", + }, + info: { + id: "we4Lby", + defaultMessage: "Info", + }, + warning: { + id: "3SVI5p", + defaultMessage: "Warning", + }, products: { id: "7NFfmz", defaultMessage: "Products", From 865c657b2e04cd0f751b00db9b9d11bde0d6d993 Mon Sep 17 00:00:00 2001 From: Mirek Mencel Date: Sun, 21 Dec 2025 21:27:40 -0100 Subject: [PATCH 73/84] Provide custom notification component/toast for sonner in headless mode --- .../NotificationProvider.test.tsx | 5 +- .../notifications/NotificationProvider.tsx | 34 +--- src/components/notifications/Toast.tsx | 132 +++++++++++++ src/components/notifications/ToastDebug.tsx | 180 +++++++++++------- src/components/notifications/index.ts | 2 + src/components/notifications/sonnerStyles.css | 117 ------------ src/hooks/useNotifier/useNotifier.tsx | 62 +++--- 7 files changed, 286 insertions(+), 246 deletions(-) create mode 100644 src/components/notifications/Toast.tsx delete mode 100644 src/components/notifications/sonnerStyles.css diff --git a/src/components/notifications/NotificationProvider.test.tsx b/src/components/notifications/NotificationProvider.test.tsx index 04b4d021dc0..9af923a6acc 100644 --- a/src/components/notifications/NotificationProvider.test.tsx +++ b/src/components/notifications/NotificationProvider.test.tsx @@ -8,10 +8,7 @@ import NotificationProvider from "./NotificationProvider"; // Mock sonner jest.mock("sonner", () => ({ toast: { - success: jest.fn(), - error: jest.fn(), - warning: jest.fn(), - info: jest.fn(), + custom: jest.fn(), dismiss: jest.fn(), }, Toaster: () => null, diff --git a/src/components/notifications/NotificationProvider.tsx b/src/components/notifications/NotificationProvider.tsx index 7de4b757c2c..f170a48c71a 100644 --- a/src/components/notifications/NotificationProvider.tsx +++ b/src/components/notifications/NotificationProvider.tsx @@ -1,17 +1,10 @@ -import "./sonnerStyles.css"; - -import { useTheme } from "@saleor/macaw-ui-next"; -import { AlertTriangle, CheckCircle2, Info, XCircle } from "lucide-react"; -import { CSSProperties, PropsWithChildren } from "react"; +import { PropsWithChildren } from "react"; import { toast, Toaster } from "sonner"; import { INotificationContext, NotificationContext } from "."; import { ToastDebug } from "./ToastDebug"; const NotificationProvider = ({ children }: PropsWithChildren) => { - const { theme, themeValues } = useTheme(); - const isDark = theme === "defaultDark"; - const context: INotificationContext = { show: () => {}, remove: (id: number) => { @@ -22,33 +15,10 @@ const NotificationProvider = ({ children }: PropsWithChildren) => { }, }; - const toasterStyle = { - "--toast-bg": themeValues?.colors.background.default1, - "--toast-border": themeValues?.colors.border.default1, - "--toast-text": themeValues?.colors.text.default1, - "--toast-text-secondary": themeValues?.colors.text.default2, - // Success toast colors - light vs dark - "--toast-success-bg": isDark ? "oklch(25% 0.04 145)" : "oklch(97% 0.04 145)", - "--toast-success-border": isDark ? "oklch(35% 0.06 145)" : "oklch(90% 0.06 145)", - } as CSSProperties; - return ( {children} - , - error: , - warning: , - info: , - }} - /> + ); diff --git a/src/components/notifications/Toast.tsx b/src/components/notifications/Toast.tsx new file mode 100644 index 00000000000..a681b9227ae --- /dev/null +++ b/src/components/notifications/Toast.tsx @@ -0,0 +1,132 @@ +import { Box, Button, Text, useTheme } from "@saleor/macaw-ui-next"; +import { AlertTriangle, CheckCircle2, Info, LucideIcon, X, XCircle } from "lucide-react"; +import { ReactNode } from "react"; +import { toast } from "sonner"; + +type ToastType = "success" | "error" | "warning" | "info"; + +interface ToastStyles { + iconColor: "success1" | "critical1" | "warning1" | "default1"; + Icon: LucideIcon; +} + +const toastStylesMap: Record = { + success: { + iconColor: "success1", + Icon: CheckCircle2, + }, + error: { + iconColor: "critical1", + Icon: XCircle, + }, + warning: { + iconColor: "warning1", + Icon: AlertTriangle, + }, + info: { + iconColor: "default1", + Icon: Info, + }, +}; + +export interface ToastProps { + id: string | number; + type: ToastType; + title: string; + description?: ReactNode; + action?: { + label: string; + onClick: () => void; + }; +} + +export const Toast = ({ id, type, title, description, action }: ToastProps) => { + const { theme, themeValues } = useTheme(); + const isDark = theme === "defaultDark"; + const { iconColor, Icon } = toastStylesMap[type]; + + // Use the same background colors as the original non-headless version: + // - Success: custom oklch green + // - Others: default UI background + const getBackgroundColor = () => { + if (type === "success") { + return isDark ? "oklch(25% 0.04 145)" : "oklch(97% 0.04 145)"; + } + + return themeValues?.colors.background.default1; + }; + + const handleDismiss = () => { + toast.dismiss(id); + }; + + return ( + + {/* Icon */} + + + + + {/* Content */} + + + {title} + + {description && ( + + {description} + + )} + {action && ( + + + + )} + + + {/* Close button */} + + + + + ); +}; diff --git a/src/components/notifications/ToastDebug.tsx b/src/components/notifications/ToastDebug.tsx index 221182b75d1..404c9a76883 100644 --- a/src/components/notifications/ToastDebug.tsx +++ b/src/components/notifications/ToastDebug.tsx @@ -7,6 +7,8 @@ import { GripVertical } from "lucide-react"; import { useRef, useState } from "react"; import { toast } from "sonner"; +import { Toast } from "./Toast"; + export const ToastDebug = () => { const [position, setPosition] = useState({ x: 20, y: 20 }); const [isDragging, setIsDragging] = useState(false); @@ -36,67 +38,18 @@ export const ToastDebug = () => { document.addEventListener("mouseup", handleMouseUp); }; - const showSavedChanges = () => { - toast.success("Saved changes", { - description: "Product details have been updated successfully.", - }); - }; - - const showOrderConfirmed = () => { - toast.success("Order confirmed", { - description: "Order #12345 has been confirmed and is ready for fulfillment.", - }); - }; - - const showSimpleError = () => { - toast.error("Something went wrong", { - description: "Unable to save changes. Please try again.", - duration: Infinity, - }); - }; - - const showPermissionError = () => { - toast.error("Permission denied", { - description: "You don't have permission to perform this action.", - duration: Infinity, - }); - }; - - const showGraphQLError = () => { - toast.error("Failed to update product", { - description: "Product with this SKU already exists.", - duration: Infinity, - }); - }; - - const showNetworkError = () => { - toast.error("Network error", { - description: "Unable to connect to the server. Please try again later.", - duration: Infinity, - }); - }; - - const showSessionExpiring = () => { - toast.warning("Session expiring soon", { - description: "Your session will expire in 5 minutes. Save your work.", - }); - }; - - const showExportStarted = () => { - toast.info("Export started", { - description: "Your export is being processed. You'll be notified when it's ready.", - }); - }; - - const showUndoAction = () => { - toast.success("Product deleted", { - description: "The product has been moved to trash.", - action: { - label: "Undo", - onClick: () => toast.success("Restored", { description: "Product has been restored." }), + const showToast = ( + type: "success" | "error" | "warning" | "info", + title: string, + description?: string, + action?: { label: string; onClick: () => void }, + ) => { + toast.custom( + id => , + { + duration: type === "error" ? Infinity : 5000, }, - duration: 8000, - }); + ); }; return ( @@ -137,13 +90,42 @@ export const ToastDebug = () => { SUCCESS
- - - @@ -154,16 +136,56 @@ export const ToastDebug = () => { ERRORS
- - - - @@ -174,10 +196,30 @@ export const ToastDebug = () => { OTHER
- - diff --git a/src/components/notifications/index.ts b/src/components/notifications/index.ts index d8166c390bd..1d23b2c70b4 100644 --- a/src/components/notifications/index.ts +++ b/src/components/notifications/index.ts @@ -25,3 +25,5 @@ export const NotificationContext = createContext(nu export * from "./NotificationProvider"; export { default } from "./NotificationProvider"; +export type { ToastProps } from "./Toast"; +export { Toast } from "./Toast"; diff --git a/src/components/notifications/sonnerStyles.css b/src/components/notifications/sonnerStyles.css deleted file mode 100644 index 1a341c89c5b..00000000000 --- a/src/components/notifications/sonnerStyles.css +++ /dev/null @@ -1,117 +0,0 @@ -/* - * Sonner toast styles for Saleor Dashboard - * - * CSS custom properties are set on [data-sonner-toaster] from React: - * --toast-bg, --toast-border, --toast-text, --toast-text-secondary - * --toast-success-bg, --toast-success-border - */ - -[data-sonner-toaster] { - --width: 380px; - font-family: - Inter, - -apple-system, - BlinkMacSystemFont, - "Segoe UI", - Roboto, - sans-serif; -} - -[data-sonner-toast] { - background: var(--toast-bg) !important; - border-color: var(--toast-border) !important; - color: var(--toast-text) !important; - border-radius: 12px; - box-shadow: - 0 4px 6px -1px rgb(0 0 0 / 0.05), - 0 2px 4px -2px rgb(0 0 0 / 0.05); - position: relative; - gap: 16px !important; - font-family: inherit; - padding: 16px 36px 16px 20px !important; -} - -/* Toast title */ -[data-sonner-toast] [data-title] { - font-size: 14px !important; - font-weight: 500 !important; - line-height: 1.4 !important; - color: var(--toast-text) !important; -} - -/* Toast description */ -[data-sonner-toast] [data-description] { - font-size: 13px !important; - font-weight: 400 !important; - line-height: 1.4 !important; - color: var(--toast-text-secondary) !important; - opacity: 0.9; -} - -/* Success - green background */ -[data-sonner-toast][data-type="success"] { - background: var(--toast-success-bg) !important; - border-color: var(--toast-success-border) !important; -} - -/* Icon colors - light theme */ -[data-sonner-toast][data-type="success"] [data-icon] svg { - color: oklch(45% 0.16 145) !important; -} - -[data-sonner-toast][data-type="error"] [data-icon] svg { - color: oklch(50% 0.2 20) !important; -} - -[data-sonner-toast][data-type="warning"] [data-icon] svg { - color: oklch(55% 0.16 70) !important; -} - -[data-sonner-toast][data-type="info"] [data-icon] svg { - color: oklch(50% 0.15 250) !important; -} - -/* Icon colors - dark theme */ -[data-sonner-toaster][data-theme="dark"] [data-sonner-toast][data-type="success"] [data-icon] svg { - color: oklch(65% 0.18 145) !important; -} - -[data-sonner-toaster][data-theme="dark"] [data-sonner-toast][data-type="error"] [data-icon] svg { - color: oklch(65% 0.2 20) !important; -} - -[data-sonner-toaster][data-theme="dark"] [data-sonner-toast][data-type="warning"] [data-icon] svg { - color: oklch(70% 0.18 70) !important; -} - -[data-sonner-toaster][data-theme="dark"] [data-sonner-toast][data-type="info"] [data-icon] svg { - color: oklch(65% 0.17 250) !important; -} - -/* Close button */ -[data-sonner-toast] [data-close-button] { - position: absolute !important; - top: 8px !important; - right: 8px !important; - left: auto !important; - transform: none !important; - background: transparent !important; - border: 1px solid rgba(0, 0, 0, 0.1) !important; - border-radius: 4px !important; - opacity: 0.6; - transition: all 0.15s; -} - -[data-sonner-toast] [data-close-button]:hover { - opacity: 1; - background: rgba(0, 0, 0, 0.05) !important; -} - -/* Dark theme close button */ -[data-sonner-toaster][data-theme="dark"] [data-sonner-toast] [data-close-button] { - border-color: rgba(255, 255, 255, 0.15) !important; -} - -[data-sonner-toaster][data-theme="dark"] [data-sonner-toast] [data-close-button]:hover { - background: rgba(255, 255, 255, 0.1) !important; -} diff --git a/src/hooks/useNotifier/useNotifier.tsx b/src/hooks/useNotifier/useNotifier.tsx index 979ee426e4e..3881046a9a2 100644 --- a/src/hooks/useNotifier/useNotifier.tsx +++ b/src/hooks/useNotifier/useNotifier.tsx @@ -1,4 +1,5 @@ import { INotification, INotificationCallback } from "@dashboard/components/notifications"; +import { Toast } from "@dashboard/components/notifications/Toast"; import { DEFAULT_NOTIFICATION_SHOW_TIME } from "@dashboard/config"; import { commonMessages } from "@dashboard/intl"; import { useIntl } from "react-intl"; @@ -16,32 +17,45 @@ function useNotifier(): UseNotifierResult { // Build description - use apiMessage as fallback if no text const description = options.text || options.apiMessage; - const toastOptions = { - description: description as string | undefined, - duration, - ...(options.actionBtn && { - action: { - label: options.actionBtn.label, - onClick: options.actionBtn.action, - }, - }), + // Determine title with fallback to localized default + const getDefaultTitle = () => { + switch (options.status) { + case "success": + return intl.formatMessage(commonMessages.success); + case "error": + return intl.formatMessage(commonMessages.error); + case "warning": + return intl.formatMessage(commonMessages.warning); + case "info": + default: + return intl.formatMessage(commonMessages.info); + } }; - switch (options.status) { - case "success": - toast.success(options.title || intl.formatMessage(commonMessages.success), toastOptions); - break; - case "error": - toast.error(options.title || intl.formatMessage(commonMessages.error), toastOptions); - break; - case "warning": - toast.warning(options.title || intl.formatMessage(commonMessages.warning), toastOptions); - break; - case "info": - default: - toast.info(options.title || intl.formatMessage(commonMessages.info), toastOptions); - break; - } + const title = options.title || getDefaultTitle(); + const type = options.status || "info"; + + toast.custom( + id => ( + + ), + { + duration, + }, + ); }; return notify; From 7bd3600f77ef60d9f82dec61660d25207c181b07 Mon Sep 17 00:00:00 2001 From: Mirek Mencel Date: Sun, 21 Dec 2025 21:34:15 -0100 Subject: [PATCH 74/84] Memoize notify function --- .../notifications/NotificationProvider.tsx | 23 +++-- src/hooks/useNotifier/useNotifier.tsx | 92 ++++++++++--------- 2 files changed, 62 insertions(+), 53 deletions(-) diff --git a/src/components/notifications/NotificationProvider.tsx b/src/components/notifications/NotificationProvider.tsx index f170a48c71a..e619518cc9e 100644 --- a/src/components/notifications/NotificationProvider.tsx +++ b/src/components/notifications/NotificationProvider.tsx @@ -1,19 +1,22 @@ -import { PropsWithChildren } from "react"; +import { PropsWithChildren, useMemo } from "react"; import { toast, Toaster } from "sonner"; import { INotificationContext, NotificationContext } from "."; import { ToastDebug } from "./ToastDebug"; const NotificationProvider = ({ children }: PropsWithChildren) => { - const context: INotificationContext = { - show: () => {}, - remove: (id: number) => { - toast.dismiss(id); - }, - clearErrorNotifications: () => { - toast.dismiss(); - }, - }; + const context = useMemo( + () => ({ + show: () => {}, + remove: (id: number) => { + toast.dismiss(id); + }, + clearErrorNotifications: () => { + toast.dismiss(); + }, + }), + [], + ); return ( diff --git a/src/hooks/useNotifier/useNotifier.tsx b/src/hooks/useNotifier/useNotifier.tsx index 3881046a9a2..cc8523aa8c6 100644 --- a/src/hooks/useNotifier/useNotifier.tsx +++ b/src/hooks/useNotifier/useNotifier.tsx @@ -2,6 +2,7 @@ import { INotification, INotificationCallback } from "@dashboard/components/noti import { Toast } from "@dashboard/components/notifications/Toast"; import { DEFAULT_NOTIFICATION_SHOW_TIME } from "@dashboard/config"; import { commonMessages } from "@dashboard/intl"; +import { useCallback } from "react"; import { useIntl } from "react-intl"; import { toast } from "sonner"; @@ -10,53 +11,58 @@ export type UseNotifierResult = INotificationCallback; function useNotifier(): UseNotifierResult { const intl = useIntl(); - const notify = (options: INotification) => { - const duration = - options.status === "error" ? Infinity : (options.autohide ?? DEFAULT_NOTIFICATION_SHOW_TIME); + const notify = useCallback( + (options: INotification) => { + const duration = + options.status === "error" + ? Infinity + : (options.autohide ?? DEFAULT_NOTIFICATION_SHOW_TIME); - // Build description - use apiMessage as fallback if no text - const description = options.text || options.apiMessage; + // Build description - use apiMessage as fallback if no text + const description = options.text || options.apiMessage; - // Determine title with fallback to localized default - const getDefaultTitle = () => { - switch (options.status) { - case "success": - return intl.formatMessage(commonMessages.success); - case "error": - return intl.formatMessage(commonMessages.error); - case "warning": - return intl.formatMessage(commonMessages.warning); - case "info": - default: - return intl.formatMessage(commonMessages.info); - } - }; + // Determine title with fallback to localized default + const getDefaultTitle = () => { + switch (options.status) { + case "success": + return intl.formatMessage(commonMessages.success); + case "error": + return intl.formatMessage(commonMessages.error); + case "warning": + return intl.formatMessage(commonMessages.warning); + case "info": + default: + return intl.formatMessage(commonMessages.info); + } + }; - const title = options.title || getDefaultTitle(); - const type = options.status || "info"; + const title = options.title || getDefaultTitle(); + const type = options.status || "info"; - toast.custom( - id => ( - - ), - { - duration, - }, - ); - }; + toast.custom( + id => ( + + ), + { + duration, + }, + ); + }, + [intl], + ); return notify; } From 7d98687969364a03912ea41069448213c4c2871d Mon Sep 17 00:00:00 2001 From: Mirek Mencel Date: Thu, 25 Dec 2025 13:47:22 +0100 Subject: [PATCH 75/84] Clamp long descriptions two two lines with ellipsis --- .../notifications/NotificationProvider.tsx | 2 + src/components/notifications/Toast.tsx | 133 +++++++++++++++++- src/components/notifications/ToastDebug.tsx | 37 +++++ 3 files changed, 166 insertions(+), 6 deletions(-) diff --git a/src/components/notifications/NotificationProvider.tsx b/src/components/notifications/NotificationProvider.tsx index e619518cc9e..1dba55be196 100644 --- a/src/components/notifications/NotificationProvider.tsx +++ b/src/components/notifications/NotificationProvider.tsx @@ -1,3 +1,5 @@ +import "./notifications.css"; + import { PropsWithChildren, useMemo } from "react"; import { toast, Toaster } from "sonner"; diff --git a/src/components/notifications/Toast.tsx b/src/components/notifications/Toast.tsx index a681b9227ae..4f836888f7e 100644 --- a/src/components/notifications/Toast.tsx +++ b/src/components/notifications/Toast.tsx @@ -1,6 +1,6 @@ import { Box, Button, Text, useTheme } from "@saleor/macaw-ui-next"; import { AlertTriangle, CheckCircle2, Info, LucideIcon, X, XCircle } from "lucide-react"; -import { ReactNode } from "react"; +import { ReactNode, useEffect, useRef, useState } from "react"; import { toast } from "sonner"; type ToastType = "success" | "error" | "warning" | "info"; @@ -40,10 +40,60 @@ export interface ToastProps { }; } +const MAX_LINES = 2; + export const Toast = ({ id, type, title, description, action }: ToastProps) => { const { theme, themeValues } = useTheme(); const isDark = theme === "defaultDark"; const { iconColor, Icon } = toastStylesMap[type]; + const [isExpanded, setIsExpanded] = useState(false); + const [isTruncated, setIsTruncated] = useState(false); + const [isHovering, setIsHovering] = useState(false); + const containerRef = useRef(null); + const textRef = useRef(null); + + // Check if description text is truncated (only on initial render) + useEffect(() => { + const textEl = textRef.current; + + if (textEl) { + // Use requestAnimationFrame to ensure measurement after render + requestAnimationFrame(() => { + if (textEl) { + // Check if text is clamped by comparing scrollHeight with clientHeight + const isOverflowing = textEl.scrollHeight > textEl.clientHeight + 1; + + if (isOverflowing) { + setIsTruncated(true); + } + } + }); + } + }, [description]); + + // Auto-collapse text when Sonner stack collapses (peek mode) + useEffect(() => { + const el = containerRef.current; + const toastWrapper = el?.closest("[data-sonner-toast]"); + + if (!toastWrapper) return; + + const observer = new MutationObserver(mutations => { + mutations.forEach(mutation => { + if (mutation.attributeName === "data-expanded") { + const isStackExpanded = toastWrapper.getAttribute("data-expanded") === "true"; + + if (!isStackExpanded && isExpanded) { + setIsExpanded(false); + } + } + }); + }); + + observer.observe(toastWrapper, { attributes: true }); + + return () => observer.disconnect(); + }, [isExpanded]); // Use the same background colors as the original non-headless version: // - Success: custom oklch green @@ -73,7 +123,6 @@ export const Toast = ({ id, type, title, description, action }: ToastProps) => { borderStyle="solid" __backgroundColor={getBackgroundColor()} __width="380px" - position="relative" > {/* Icon */} @@ -86,12 +135,84 @@ export const Toast = ({ id, type, title, description, action }: ToastProps) => { {title}
{description && ( - - {description} - + setIsHovering(true)} + onMouseLeave={() => setIsHovering(false)} + position="relative" + style={{ + overflow: "hidden", + maxHeight: isExpanded ? "500px" : "3.2em", + transition: "max-height 0.15s cubic-bezier(0.4, 0, 0.2, 1)", + cursor: isTruncated && !isExpanded ? "pointer" : "default", + }} + onClick={() => { + if (isTruncated && !isExpanded) { + setIsExpanded(true); + } + }} + > + } + size={2} + color="default2" + style={{ + transition: "opacity 0.15s", + opacity: isTruncated && !isExpanded && isHovering ? 0.6 : 1, + ...(!isExpanded && { + display: "-webkit-box", + WebkitLineClamp: MAX_LINES, + WebkitBoxOrient: "vertical", + overflow: "hidden", + }), + }} + > + {description} + + {/* Gradient fade hint for truncated content */} + {isTruncated && !isExpanded && ( + + )} + {((isTruncated && isHovering) || isExpanded) && ( + { + e.stopPropagation(); + setIsExpanded(!isExpanded); + }} + style={{ + cursor: "pointer", + border: "none", + background: "none", + padding: 0, + marginTop: "4px", + outline: "none", + opacity: 0.7, + transition: "opacity 0.15s", + display: "block", + }} + onMouseEnter={e => (e.currentTarget.style.opacity = "1")} + onMouseLeave={e => (e.currentTarget.style.opacity = "0.7")} + > + {isExpanded ? "Show less" : "Show more"} + + )} + )} {action && ( - + + + + + + From 5acd68482c8c63bc089a797ad88f04ed3fa22aee Mon Sep 17 00:00:00 2001 From: Mirek Mencel Date: Thu, 25 Dec 2025 14:14:29 +0100 Subject: [PATCH 76/84] Refactor --- .../notifications/NotificationProvider.tsx | 2 - src/components/notifications/Toast.tsx | 188 +++++++++--------- 2 files changed, 94 insertions(+), 96 deletions(-) diff --git a/src/components/notifications/NotificationProvider.tsx b/src/components/notifications/NotificationProvider.tsx index 1dba55be196..e619518cc9e 100644 --- a/src/components/notifications/NotificationProvider.tsx +++ b/src/components/notifications/NotificationProvider.tsx @@ -1,5 +1,3 @@ -import "./notifications.css"; - import { PropsWithChildren, useMemo } from "react"; import { toast, Toaster } from "sonner"; diff --git a/src/components/notifications/Toast.tsx b/src/components/notifications/Toast.tsx index 4f836888f7e..449af681b83 100644 --- a/src/components/notifications/Toast.tsx +++ b/src/components/notifications/Toast.tsx @@ -1,34 +1,27 @@ import { Box, Button, Text, useTheme } from "@saleor/macaw-ui-next"; import { AlertTriangle, CheckCircle2, Info, LucideIcon, X, XCircle } from "lucide-react"; -import { ReactNode, useEffect, useRef, useState } from "react"; +import { ReactNode, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { toast } from "sonner"; type ToastType = "success" | "error" | "warning" | "info"; -interface ToastStyles { +interface ToastStyleConfig { iconColor: "success1" | "critical1" | "warning1" | "default1"; Icon: LucideIcon; } -const toastStylesMap: Record = { - success: { - iconColor: "success1", - Icon: CheckCircle2, - }, - error: { - iconColor: "critical1", - Icon: XCircle, - }, - warning: { - iconColor: "warning1", - Icon: AlertTriangle, - }, - info: { - iconColor: "default1", - Icon: Info, - }, +const TOAST_STYLES: Record = { + success: { iconColor: "success1", Icon: CheckCircle2 }, + error: { iconColor: "critical1", Icon: XCircle }, + warning: { iconColor: "warning1", Icon: AlertTriangle }, + info: { iconColor: "default1", Icon: Info }, }; +const MAX_LINES = 2; +const COLLAPSED_MAX_HEIGHT = "3.2em"; +const EXPANDED_MAX_HEIGHT = "500px"; +const ANIMATION_TIMING = "0.15s cubic-bezier(0.4, 0, 0.2, 1)"; + export interface ToastProps { id: string | number; type: ToastType; @@ -40,75 +33,92 @@ export interface ToastProps { }; } -const MAX_LINES = 2; - export const Toast = ({ id, type, title, description, action }: ToastProps) => { const { theme, themeValues } = useTheme(); - const isDark = theme === "defaultDark"; - const { iconColor, Icon } = toastStylesMap[type]; + const { iconColor, Icon } = TOAST_STYLES[type]; + const [isExpanded, setIsExpanded] = useState(false); const [isTruncated, setIsTruncated] = useState(false); const [isHovering, setIsHovering] = useState(false); + const [isToggleHovering, setIsToggleHovering] = useState(false); + const containerRef = useRef(null); const textRef = useRef(null); - // Check if description text is truncated (only on initial render) + const isDark = theme === "defaultDark"; + + const backgroundColor = useMemo(() => { + if (type === "success") { + return isDark ? "oklch(25% 0.04 145)" : "oklch(97% 0.04 145)"; + } + + return themeValues?.colors.background.default1; + }, [type, isDark, themeValues]); + + const handleDismiss = useCallback(() => { + toast.dismiss(id); + }, [id]); + + const handleExpand = useCallback(() => { + if (isTruncated && !isExpanded) { + setIsExpanded(true); + } + }, [isTruncated, isExpanded]); + + const handleToggleExpand = useCallback((e: React.MouseEvent) => { + e.stopPropagation(); + setIsExpanded(prev => !prev); + }, []); + + const handleActionClick = useCallback(() => { + action?.onClick(); + handleDismiss(); + }, [action, handleDismiss]); + + const handleMouseEnter = useCallback(() => setIsHovering(true), []); + const handleMouseLeave = useCallback(() => setIsHovering(false), []); + const handleToggleMouseEnter = useCallback(() => setIsToggleHovering(true), []); + const handleToggleMouseLeave = useCallback(() => setIsToggleHovering(false), []); + + // Check if description text is truncated useEffect(() => { const textEl = textRef.current; - if (textEl) { - // Use requestAnimationFrame to ensure measurement after render - requestAnimationFrame(() => { - if (textEl) { - // Check if text is clamped by comparing scrollHeight with clientHeight - const isOverflowing = textEl.scrollHeight > textEl.clientHeight + 1; + if (!textEl) return; - if (isOverflowing) { - setIsTruncated(true); - } - } - }); - } + requestAnimationFrame(() => { + const isOverflowing = textEl.scrollHeight > textEl.clientHeight + 1; + + if (isOverflowing) { + setIsTruncated(true); + } + }); }, [description]); // Auto-collapse text when Sonner stack collapses (peek mode) useEffect(() => { - const el = containerRef.current; - const toastWrapper = el?.closest("[data-sonner-toast]"); + const toastWrapper = containerRef.current?.closest("[data-sonner-toast]"); if (!toastWrapper) return; const observer = new MutationObserver(mutations => { - mutations.forEach(mutation => { + for (const mutation of mutations) { if (mutation.attributeName === "data-expanded") { const isStackExpanded = toastWrapper.getAttribute("data-expanded") === "true"; - if (!isStackExpanded && isExpanded) { + if (!isStackExpanded) { setIsExpanded(false); } } - }); + } }); observer.observe(toastWrapper, { attributes: true }); return () => observer.disconnect(); - }, [isExpanded]); - - // Use the same background colors as the original non-headless version: - // - Success: custom oklch green - // - Others: default UI background - const getBackgroundColor = () => { - if (type === "success") { - return isDark ? "oklch(25% 0.04 145)" : "oklch(97% 0.04 145)"; - } - - return themeValues?.colors.background.default1; - }; + }, []); - const handleDismiss = () => { - toast.dismiss(id); - }; + const showToggle = (isTruncated && isHovering) || isExpanded; return ( { borderColor="default1" borderWidth={1} borderStyle="solid" - __backgroundColor={getBackgroundColor()} + __backgroundColor={backgroundColor} __width="380px" + __minHeight="94px" > {/* Icon */} @@ -134,42 +145,39 @@ export const Toast = ({ id, type, title, description, action }: ToastProps) => { {title} + {description && ( setIsHovering(true)} - onMouseLeave={() => setIsHovering(false)} + onMouseEnter={handleMouseEnter} + onMouseLeave={handleMouseLeave} + onClick={handleExpand} position="relative" style={{ overflow: "hidden", - maxHeight: isExpanded ? "500px" : "3.2em", - transition: "max-height 0.15s cubic-bezier(0.4, 0, 0.2, 1)", + maxHeight: isExpanded ? EXPANDED_MAX_HEIGHT : COLLAPSED_MAX_HEIGHT, + transition: `max-height ${ANIMATION_TIMING}`, cursor: isTruncated && !isExpanded ? "pointer" : "default", }} - onClick={() => { - if (isTruncated && !isExpanded) { - setIsExpanded(true); - } - }} > } size={2} color="default2" style={{ - transition: "opacity 0.15s", - opacity: isTruncated && !isExpanded && isHovering ? 0.6 : 1, + transition: `opacity ${ANIMATION_TIMING}`, + opacity: isTruncated && !isExpanded && isHovering ? 0.7 : 1, ...(!isExpanded && { display: "-webkit-box", WebkitLineClamp: MAX_LINES, - WebkitBoxOrient: "vertical", + WebkitBoxOrient: "vertical" as const, overflow: "hidden", }), }} > {description} - {/* Gradient fade hint for truncated content */} + {isTruncated && !isExpanded && ( { __right="0" __height="1.2em" style={{ - background: `linear-gradient(transparent, ${getBackgroundColor()})`, + background: `linear-gradient(transparent, ${backgroundColor})`, pointerEvents: "none", }} /> )} - {((isTruncated && isHovering) || isExpanded) && ( - { - e.stopPropagation(); - setIsExpanded(!isExpanded); - }} + onClick={handleToggleExpand} + onMouseEnter={handleToggleMouseEnter} + onMouseLeave={handleToggleMouseLeave} style={{ cursor: "pointer", border: "none", @@ -199,28 +205,22 @@ export const Toast = ({ id, type, title, description, action }: ToastProps) => { padding: 0, marginTop: "4px", outline: "none", - opacity: 0.7, - transition: "opacity 0.15s", + opacity: isToggleHovering ? 1 : 0.7, + transition: `opacity ${ANIMATION_TIMING}`, display: "block", }} - onMouseEnter={e => (e.currentTarget.style.opacity = "1")} - onMouseLeave={e => (e.currentTarget.style.opacity = "0.7")} > - {isExpanded ? "Show less" : "Show more"} - + + {isExpanded ? "Show less" : "Show more"} + + )} )} + {action && ( - @@ -243,7 +243,7 @@ export const Toast = ({ id, type, title, description, action }: ToastProps) => { style={{ outline: "none", opacity: 0.6, - transition: "opacity 0.15s", + transition: `opacity ${ANIMATION_TIMING}`, }} > From 39768bf3c6bd9157ab709bbf57c4d851b1223f60 Mon Sep 17 00:00:00 2001 From: Mirek Mencel Date: Thu, 25 Dec 2025 14:30:46 +0100 Subject: [PATCH 77/84] Clean up the debug panel for toast notifications (moved to separate PR) --- .../notifications/NotificationProvider.tsx | 2 - src/components/notifications/ToastDebug.tsx | 270 ------------------ 2 files changed, 272 deletions(-) delete mode 100644 src/components/notifications/ToastDebug.tsx diff --git a/src/components/notifications/NotificationProvider.tsx b/src/components/notifications/NotificationProvider.tsx index e619518cc9e..feac7766ade 100644 --- a/src/components/notifications/NotificationProvider.tsx +++ b/src/components/notifications/NotificationProvider.tsx @@ -2,7 +2,6 @@ import { PropsWithChildren, useMemo } from "react"; import { toast, Toaster } from "sonner"; import { INotificationContext, NotificationContext } from "."; -import { ToastDebug } from "./ToastDebug"; const NotificationProvider = ({ children }: PropsWithChildren) => { const context = useMemo( @@ -22,7 +21,6 @@ const NotificationProvider = ({ children }: PropsWithChildren) => { {children} - ); }; diff --git a/src/components/notifications/ToastDebug.tsx b/src/components/notifications/ToastDebug.tsx deleted file mode 100644 index b9e60a831ac..00000000000 --- a/src/components/notifications/ToastDebug.tsx +++ /dev/null @@ -1,270 +0,0 @@ -/** - * Temporary debug component to preview toast variants - * Remove this file after styling is finalized - */ -import { Box, Button, Text } from "@saleor/macaw-ui-next"; -import { GripVertical } from "lucide-react"; -import { useRef, useState } from "react"; -import { toast } from "sonner"; - -import { Toast } from "./Toast"; - -export const ToastDebug = () => { - const [position, setPosition] = useState({ x: 20, y: 20 }); - const [isDragging, setIsDragging] = useState(false); - const dragOffset = useRef({ x: 0, y: 0 }); - - const handleMouseDown = (e: React.MouseEvent) => { - setIsDragging(true); - dragOffset.current = { - x: e.clientX - position.x, - y: window.innerHeight - e.clientY - position.y, - }; - - const handleMouseMove = (e: MouseEvent) => { - setPosition({ - x: e.clientX - dragOffset.current.x, - y: window.innerHeight - e.clientY - dragOffset.current.y, - }); - }; - - const handleMouseUp = () => { - setIsDragging(false); - document.removeEventListener("mousemove", handleMouseMove); - document.removeEventListener("mouseup", handleMouseUp); - }; - - document.addEventListener("mousemove", handleMouseMove); - document.addEventListener("mouseup", handleMouseUp); - }; - - const showToast = ( - type: "success" | "error" | "warning" | "info", - title: string, - description?: string, - action?: { label: string; onClick: () => void }, - ) => { - toast.custom( - id => , - { - duration: type === "error" ? Infinity : 5000, - }, - ); - }; - - return ( - - {/* Drag handle */} - - - - 🍞 Toast Preview - - - - - - SUCCESS - - - - - - - - - - - ERRORS - - - - - - - - - - - - OTHER - - - - - - - - - - LENGTH TEST - - - - - - - - - - - ); -}; From 976d5770ddf8282bce10ce3d0772d05d11cdd1b8 Mon Sep 17 00:00:00 2001 From: Mirek Mencel Date: Thu, 25 Dec 2025 14:31:42 +0100 Subject: [PATCH 78/84] Extract messages --- locale/defaultMessages.json | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/locale/defaultMessages.json b/locale/defaultMessages.json index 071ade357b1..dac88068678 100644 --- a/locale/defaultMessages.json +++ b/locale/defaultMessages.json @@ -2136,10 +2136,6 @@ "Bl6896": { "string": "Refund amount" }, - "BnB/7Y": { - "context": "callout, title", - "string": "Info" - }, "Bphmwe": { "context": "header", "string": "Translation Collection \"{collectionName}\" - {languageCode}" @@ -5225,10 +5221,6 @@ "context": "filters error messages unknown error", "string": "Unknown error occurred" }, - "UUVUyy": { - "context": "callout, title", - "string": "Warning" - }, "UVDfTs": { "context": "discount type shipping", "string": "Shipping" @@ -9719,6 +9711,9 @@ "wbsq7O": { "string": "Usage" }, + "we4Lby": { + "string": "Info" + }, "werrDz": { "context": "refunded event title", "string": "Products were refunded" From 19113c283442c0c09f4b677bbe9441b6be316920 Mon Sep 17 00:00:00 2001 From: Mirek Mencel Date: Fri, 26 Dec 2025 13:57:11 +0100 Subject: [PATCH 79/84] Add test coverage for notifications --- src/components/notifications/Toast.test.tsx | 226 +++++++++++ src/hooks/useNotifier/useNotifier.test.tsx | 401 ++++++++++++++++++++ 2 files changed, 627 insertions(+) create mode 100644 src/components/notifications/Toast.test.tsx create mode 100644 src/hooks/useNotifier/useNotifier.test.tsx diff --git a/src/components/notifications/Toast.test.tsx b/src/components/notifications/Toast.test.tsx new file mode 100644 index 00000000000..b4f5b53325a --- /dev/null +++ b/src/components/notifications/Toast.test.tsx @@ -0,0 +1,226 @@ +import { ThemeProvider } from "@saleor/macaw-ui-next"; +import { fireEvent, render, screen, waitFor } from "@testing-library/react"; +import { toast } from "sonner"; + +import { Toast, ToastProps } from "./Toast"; + +jest.mock("sonner", () => ({ + toast: { + dismiss: jest.fn(), + }, +})); + +const mockToastDismiss = toast.dismiss as jest.Mock; + +const defaultProps: ToastProps = { + id: "test-toast-1", + type: "info", + title: "Test Title", +}; + +const renderToast = (props: Partial = {}) => + render( + + + , + ); + +describe("Toast", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe("rendering", () => { + it("renders toast with title", () => { + // Arrange & Act + renderToast({ title: "Success Message" }); + + // Assert + expect(screen.getByText("Success Message")).toBeInTheDocument(); + }); + + it("renders toast with description", () => { + // Arrange & Act + renderToast({ + title: "Title", + description: "This is a detailed description", + }); + + // Assert + expect(screen.getByText("This is a detailed description")).toBeInTheDocument(); + }); + + it("renders without description when not provided", () => { + // Arrange & Act + renderToast({ title: "Title Only" }); + + // Assert + expect(screen.getByText("Title Only")).toBeInTheDocument(); + expect(screen.queryByText("description")).not.toBeInTheDocument(); + }); + + it("renders action button when action is provided", () => { + // Arrange & Act + renderToast({ + action: { + label: "Undo", + onClick: jest.fn(), + }, + }); + + // Assert + expect(screen.getByRole("button", { name: "Undo" })).toBeInTheDocument(); + }); + + it("does not render action button when action is not provided", () => { + // Arrange & Act + renderToast(); + + // Assert + expect(screen.queryByRole("button", { name: "Undo" })).not.toBeInTheDocument(); + }); + }); + + describe("toast types", () => { + it.each([ + ["success", "Success"], + ["error", "Error"], + ["warning", "Warning"], + ["info", "Info"], + ] as const)("renders %s toast with title", (type, title) => { + // Arrange & Act + renderToast({ type, title }); + + // Assert + expect(screen.getByText(title)).toBeInTheDocument(); + }); + }); + + describe("close functionality", () => { + it("calls toast.dismiss when close button is clicked", () => { + // Arrange + renderToast({ id: "dismiss-test-id" }); + + // Act + const closeButtons = screen.getAllByRole("button"); + const closeButton = closeButtons.find(btn => btn.querySelector("svg")); + + fireEvent.click(closeButton!); + + // Assert + expect(mockToastDismiss).toHaveBeenCalledWith("dismiss-test-id"); + }); + }); + + describe("action button", () => { + it("calls action onClick when action button is clicked", () => { + // Arrange + const onClickMock = jest.fn(); + + renderToast({ + action: { + label: "Retry", + onClick: onClickMock, + }, + }); + + // Act + const actionButton = screen.getByRole("button", { name: "Retry" }); + + fireEvent.click(actionButton); + + // Assert + expect(onClickMock).toHaveBeenCalledTimes(1); + }); + + it("dismisses toast after action button is clicked", () => { + // Arrange + const onClickMock = jest.fn(); + + renderToast({ + id: "action-dismiss-test", + action: { + label: "Retry", + onClick: onClickMock, + }, + }); + + // Act + const actionButton = screen.getByRole("button", { name: "Retry" }); + + fireEvent.click(actionButton); + + // Assert + expect(mockToastDismiss).toHaveBeenCalledWith("action-dismiss-test"); + }); + }); + + describe("description truncation", () => { + it("renders long description", () => { + // Arrange + const longDescription = + "This is a very long description that might get truncated when displayed in the toast notification. It contains multiple sentences to ensure it exceeds the maximum allowed lines for display."; + + // Act + renderToast({ description: longDescription }); + + // Assert + expect(screen.getByText(longDescription)).toBeInTheDocument(); + }); + + it("renders description container with overflow styles", () => { + // Arrange + const longDescription = "This is a very long description that spans multiple lines. ".repeat( + 10, + ); + + // Act + renderToast({ description: longDescription.trim() }); + + // Assert - verify the container has the truncation styles applied + const descriptionElement = screen.getByText(longDescription.trim()); + + expect(descriptionElement).toBeInTheDocument(); + expect(descriptionElement).toHaveStyle({ overflow: "hidden" }); + }); + + it("responds to mouse enter events on description container", async () => { + // Arrange + const description = "A description that can be hovered"; + + renderToast({ description }); + + // Act + const descriptionElement = screen.getByText(description); + const descriptionContainer = descriptionElement.closest('[class*="_18fs8ps"]'); + + if (descriptionContainer) { + fireEvent.mouseEnter(descriptionContainer); + fireEvent.mouseLeave(descriptionContainer); + } + + // Assert - the component handles mouse events without errors + await waitFor(() => { + expect(descriptionElement).toBeInTheDocument(); + }); + }); + }); + + describe("ReactNode description", () => { + it("renders JSX elements as description", () => { + // Arrange + const jsxDescription = ( + + Bold and italic text + + ); + + // Act + renderToast({ description: jsxDescription }); + + // Assert + expect(screen.getByText("Bold")).toBeInTheDocument(); + expect(screen.getByText("italic")).toBeInTheDocument(); + }); + }); +}); diff --git a/src/hooks/useNotifier/useNotifier.test.tsx b/src/hooks/useNotifier/useNotifier.test.tsx new file mode 100644 index 00000000000..062b23ebfd9 --- /dev/null +++ b/src/hooks/useNotifier/useNotifier.test.tsx @@ -0,0 +1,401 @@ +import { INotification } from "@dashboard/components/notifications"; +import { DEFAULT_NOTIFICATION_SHOW_TIME } from "@dashboard/config"; +import { ThemeProvider } from "@saleor/macaw-ui-next"; +import { act, renderHook } from "@testing-library/react-hooks"; +import { PropsWithChildren } from "react"; +import { IntlProvider } from "react-intl"; +import { toast } from "sonner"; + +import useNotifier from "./useNotifier"; + +jest.mock("sonner", () => ({ + toast: { + custom: jest.fn(), + }, +})); + +const mockToastCustom = toast.custom as jest.Mock; + +const wrapper = ({ children }: PropsWithChildren) => ( + + {children} + +); + +describe("useNotifier", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe("basic functionality", () => { + it("returns notify function", () => { + // Arrange & Act + const { result } = renderHook(() => useNotifier(), { wrapper }); + + // Assert + expect(typeof result.current).toBe("function"); + }); + + it("calls toast.custom when notify is invoked", () => { + // Arrange + const { result } = renderHook(() => useNotifier(), { wrapper }); + const notification: INotification = { + title: "Test", + status: "success", + }; + + // Act + act(() => { + result.current(notification); + }); + + // Assert + expect(mockToastCustom).toHaveBeenCalledTimes(1); + }); + }); + + describe("duration handling", () => { + it("uses DEFAULT_NOTIFICATION_SHOW_TIME for success notifications", () => { + // Arrange + const { result } = renderHook(() => useNotifier(), { wrapper }); + + // Act + act(() => { + result.current({ status: "success", title: "Success" }); + }); + + // Assert + expect(mockToastCustom).toHaveBeenCalledWith(expect.any(Function), { + duration: DEFAULT_NOTIFICATION_SHOW_TIME, + }); + }); + + it("uses Infinity duration for error notifications", () => { + // Arrange + const { result } = renderHook(() => useNotifier(), { wrapper }); + + // Act + act(() => { + result.current({ status: "error", title: "Error" }); + }); + + // Assert + expect(mockToastCustom).toHaveBeenCalledWith(expect.any(Function), { + duration: Infinity, + }); + }); + + it("uses custom autohide value when provided", () => { + // Arrange + const { result } = renderHook(() => useNotifier(), { wrapper }); + const customAutohide = 5000; + + // Act + act(() => { + result.current({ + status: "success", + title: "Custom Duration", + autohide: customAutohide, + }); + }); + + // Assert + expect(mockToastCustom).toHaveBeenCalledWith(expect.any(Function), { + duration: customAutohide, + }); + }); + + it("uses DEFAULT_NOTIFICATION_SHOW_TIME for info notifications", () => { + // Arrange + const { result } = renderHook(() => useNotifier(), { wrapper }); + + // Act + act(() => { + result.current({ status: "info", title: "Info" }); + }); + + // Assert + expect(mockToastCustom).toHaveBeenCalledWith(expect.any(Function), { + duration: DEFAULT_NOTIFICATION_SHOW_TIME, + }); + }); + + it("uses DEFAULT_NOTIFICATION_SHOW_TIME for warning notifications", () => { + // Arrange + const { result } = renderHook(() => useNotifier(), { wrapper }); + + // Act + act(() => { + result.current({ status: "warning", title: "Warning" }); + }); + + // Assert + expect(mockToastCustom).toHaveBeenCalledWith(expect.any(Function), { + duration: DEFAULT_NOTIFICATION_SHOW_TIME, + }); + }); + }); + + describe("title fallback", () => { + it("uses provided title when given", () => { + // Arrange + const { result } = renderHook(() => useNotifier(), { wrapper }); + + // Act + act(() => { + result.current({ title: "Custom Title", status: "success" }); + }); + + // Assert + const renderFn = mockToastCustom.mock.calls[0][0]; + const renderedElement = renderFn("test-id"); + + expect(renderedElement.props.title).toBe("Custom Title"); + }); + + it("falls back to 'Success' for success status without title", () => { + // Arrange + const { result } = renderHook(() => useNotifier(), { wrapper }); + + // Act + act(() => { + result.current({ status: "success" }); + }); + + // Assert + const renderFn = mockToastCustom.mock.calls[0][0]; + const renderedElement = renderFn("test-id"); + + expect(renderedElement.props.title).toBe("Success"); + }); + + it("falls back to 'Error' for error status without title", () => { + // Arrange + const { result } = renderHook(() => useNotifier(), { wrapper }); + + // Act + act(() => { + result.current({ status: "error" }); + }); + + // Assert + const renderFn = mockToastCustom.mock.calls[0][0]; + const renderedElement = renderFn("test-id"); + + expect(renderedElement.props.title).toBe("Error"); + }); + + it("falls back to 'Warning' for warning status without title", () => { + // Arrange + const { result } = renderHook(() => useNotifier(), { wrapper }); + + // Act + act(() => { + result.current({ status: "warning" }); + }); + + // Assert + const renderFn = mockToastCustom.mock.calls[0][0]; + const renderedElement = renderFn("test-id"); + + expect(renderedElement.props.title).toBe("Warning"); + }); + + it("falls back to 'Info' for info status without title", () => { + // Arrange + const { result } = renderHook(() => useNotifier(), { wrapper }); + + // Act + act(() => { + result.current({ status: "info" }); + }); + + // Assert + const renderFn = mockToastCustom.mock.calls[0][0]; + const renderedElement = renderFn("test-id"); + + expect(renderedElement.props.title).toBe("Info"); + }); + + it("falls back to 'Info' when no status is provided", () => { + // Arrange + const { result } = renderHook(() => useNotifier(), { wrapper }); + + // Act + act(() => { + result.current({}); + }); + + // Assert + const renderFn = mockToastCustom.mock.calls[0][0]; + const renderedElement = renderFn("test-id"); + + expect(renderedElement.props.title).toBe("Info"); + }); + }); + + describe("description handling", () => { + it("uses text as description when provided", () => { + // Arrange + const { result } = renderHook(() => useNotifier(), { wrapper }); + + // Act + act(() => { + result.current({ + title: "Title", + text: "Description text", + status: "info", + }); + }); + + // Assert + const renderFn = mockToastCustom.mock.calls[0][0]; + const renderedElement = renderFn("test-id"); + + expect(renderedElement.props.description).toBe("Description text"); + }); + + it("uses apiMessage as description fallback when text is not provided", () => { + // Arrange + const { result } = renderHook(() => useNotifier(), { wrapper }); + + // Act + act(() => { + result.current({ + title: "Title", + apiMessage: "API error message", + status: "error", + }); + }); + + // Assert + const renderFn = mockToastCustom.mock.calls[0][0]; + const renderedElement = renderFn("test-id"); + + expect(renderedElement.props.description).toBe("API error message"); + }); + + it("prefers text over apiMessage when both are provided", () => { + // Arrange + const { result } = renderHook(() => useNotifier(), { wrapper }); + + // Act + act(() => { + result.current({ + title: "Title", + text: "Primary description", + apiMessage: "Fallback description", + status: "info", + }); + }); + + // Assert + const renderFn = mockToastCustom.mock.calls[0][0]; + const renderedElement = renderFn("test-id"); + + expect(renderedElement.props.description).toBe("Primary description"); + }); + }); + + describe("action button", () => { + it("passes action configuration to Toast", () => { + // Arrange + const { result } = renderHook(() => useNotifier(), { wrapper }); + const actionFn = jest.fn(); + + // Act + act(() => { + result.current({ + title: "Title", + status: "info", + actionBtn: { + label: "Undo", + action: actionFn, + }, + }); + }); + + // Assert + const renderFn = mockToastCustom.mock.calls[0][0]; + const renderedElement = renderFn("test-id"); + + expect(renderedElement.props.action).toEqual({ + label: "Undo", + onClick: actionFn, + }); + }); + + it("does not pass action when actionBtn is not provided", () => { + // Arrange + const { result } = renderHook(() => useNotifier(), { wrapper }); + + // Act + act(() => { + result.current({ title: "Title", status: "info" }); + }); + + // Assert + const renderFn = mockToastCustom.mock.calls[0][0]; + const renderedElement = renderFn("test-id"); + + expect(renderedElement.props.action).toBeUndefined(); + }); + }); + + describe("toast type mapping", () => { + it.each([ + ["success", "success"], + ["error", "error"], + ["warning", "warning"], + ["info", "info"], + ] as const)("maps status '%s' to toast type '%s'", (status, expectedType) => { + // Arrange + const { result } = renderHook(() => useNotifier(), { wrapper }); + + // Act + act(() => { + result.current({ title: "Test", status }); + }); + + // Assert + const renderFn = mockToastCustom.mock.calls[0][0]; + const renderedElement = renderFn("test-id"); + + expect(renderedElement.props.type).toBe(expectedType); + }); + + it("defaults to 'info' type when status is not provided", () => { + // Arrange + const { result } = renderHook(() => useNotifier(), { wrapper }); + + // Act + act(() => { + result.current({ title: "Test" }); + }); + + // Assert + const renderFn = mockToastCustom.mock.calls[0][0]; + const renderedElement = renderFn("test-id"); + + expect(renderedElement.props.type).toBe("info"); + }); + }); + + describe("toast id", () => { + it("passes toast id to Toast component", () => { + // Arrange + const { result } = renderHook(() => useNotifier(), { wrapper }); + + // Act + act(() => { + result.current({ title: "Test", status: "success" }); + }); + + // Assert + const renderFn = mockToastCustom.mock.calls[0][0]; + const renderedElement = renderFn("unique-toast-id"); + + expect(renderedElement.props.id).toBe("unique-toast-id"); + }); + }); +}); From e5d670fdcd417d7a5c3889d53e20285f18b7c52a Mon Sep 17 00:00:00 2001 From: Mirek Mencel Date: Fri, 26 Dec 2025 14:24:25 +0100 Subject: [PATCH 80/84] Improve changelog --- .changeset/whole-groups-happen.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.changeset/whole-groups-happen.md b/.changeset/whole-groups-happen.md index 5e45a3239af..4b9cc8c97ce 100644 --- a/.changeset/whole-groups-happen.md +++ b/.changeset/whole-groups-happen.md @@ -2,4 +2,11 @@ "saleor-dashboard": patch --- -Replaces custom toasts with Sonner notifications component +Redesigned toast notifications for better user experience: + +- **Notifications now stack compactly** instead of flooding the screen +- **Refined visual design** that's less disruptive and better aligned with the dashboard aesthetic +- **Long messages expand on hover** with "Show more/less" toggle instead of being truncated +- **Error notifications persist** until manually dismissed + +**Next up:** Refining notification message copy for clarity and consistency across the dashboard. From 9899877acb1d152e68615da6ab8fe45cb4f8391e Mon Sep 17 00:00:00 2001 From: Mirek Mencel Date: Tue, 20 Jan 2026 14:48:52 +0100 Subject: [PATCH 81/84] Refactor to not use the default import --- .../components/AttributeSwatchField/useFileProcessing.ts | 2 +- src/attributes/views/AttributeCreate/AttributeCreate.tsx | 2 +- src/attributes/views/AttributeDetails/AttributeDetails.tsx | 2 +- src/attributes/views/AttributeList/AttributeList.tsx | 2 +- src/auth/AuthProvider.test.tsx | 2 +- src/auth/AuthProvider.tsx | 2 +- src/categories/views/CategoryCreate.tsx | 2 +- src/categories/views/CategoryDetails.tsx | 2 +- src/channels/views/ChannelCreate/ChannelCreate.tsx | 2 +- src/channels/views/ChannelDetails/ChannelDetails.tsx | 2 +- .../views/ChannelDetails/useChannelWarehouseReorder.ts | 2 +- src/channels/views/ChannelsList/ChannelsList.tsx | 2 +- .../components/CollectionProducts/CollectionProducts.tsx | 2 +- .../components/CollectionProducts/useProductReorder.test.ts | 2 +- .../components/CollectionProducts/useProductReorder.ts | 2 +- src/collections/views/CollectionCreate.tsx | 2 +- src/collections/views/CollectionDetails.tsx | 2 +- src/collections/views/CollectionList/CollectionList.tsx | 2 +- .../API/initialState/product/useProductInitialAPIState.tsx | 2 +- src/components/MetadataDialog/useHandleMetadataSubmit.ts | 2 +- src/containers/BackgroundTasks/BackgroundTasksProvider.tsx | 2 +- src/customers/views/CustomerAddresses.tsx | 2 +- src/customers/views/CustomerCreate.tsx | 2 +- src/customers/views/CustomerDetails.tsx | 2 +- src/customers/views/CustomerList/CustomerList.tsx | 2 +- src/discounts/components/DiscountSavebar/DiscountSavebar.tsx | 2 +- src/discounts/views/DiscountCreate/DiscountCreate.tsx | 2 +- .../views/DiscountDetails/hooks/usePromotionDelete.ts | 2 +- .../views/DiscountDetails/hooks/usePromotionRuleCreate.ts | 2 +- .../views/DiscountDetails/hooks/usePromotionRuleDelete.ts | 2 +- .../views/DiscountDetails/hooks/usePromotionRuleUpdate.ts | 2 +- .../views/DiscountDetails/hooks/usePromotionUpdate.ts | 2 +- src/discounts/views/VoucherCreate/VoucherCreate.tsx | 2 +- src/discounts/views/VoucherDetails/VoucherDetails.tsx | 2 +- src/discounts/views/VoucherList/VoucherList.tsx | 2 +- .../AddCustomExtension/hooks/useHandleCreateAppSubmit.ts | 2 +- .../AddCustomExtensionWebhook/AddCustomExtensionWebhook.tsx | 2 +- src/extensions/views/EditCustomExtension/EditCustomApp.tsx | 2 +- .../components/TokenCreateDialog/useClipboardCopy.ts | 2 +- .../EditCustomExtensionWebhook/EditCustomExtensionWebhook.tsx | 2 +- src/extensions/views/EditManifestExtension/AppManageView.tsx | 2 +- .../components/AppPermissionsDialog/AppPermissionsDialog.tsx | 2 +- .../views/EditPluginExtension/EditPluginExtension.tsx | 2 +- .../views/InstallCustomExtension/hooks/useInstallApp.test.ts | 2 +- .../views/InstallCustomExtension/hooks/useInstallApp.ts | 2 +- .../views/InstalledExtensions/hooks/useInstallationNotify.ts | 2 +- .../ViewManifestExtension/ViewManifestExtensionIframe.tsx | 2 +- .../components/AppFrame/appActionsHandler.ts | 2 +- .../components/AppFrame/useAppActions.ts | 2 +- .../GiftCardBulkCreateDialog/GiftCardBulkCreateDialog.tsx | 2 +- .../GiftCardCreateDialog/GiftCardCreateDialogCodeContent.tsx | 2 +- .../GiftCardCreateDialog/GiftCardCreateDialogContent.tsx | 2 +- .../GiftCardExportDialogContent.tsx | 2 +- .../GiftCardUpdate/GiftCardHistory/GiftCardHistory.tsx | 2 +- .../GiftCardResendCodeDialog/GiftCardResendCodeDialog.tsx | 2 +- .../GiftCardUpdateBalanceDialog.tsx | 2 +- .../hooks/useGiftCardActivateToggle.ts | 2 +- .../GiftCardUpdateFormProvider/GiftCardUpdateFormProvider.tsx | 2 +- .../GiftCardListBulkActions/GiftCardListBulkActions.tsx | 2 +- .../providers/GiftCardListProvider/GiftCardListProvider.tsx | 2 +- .../components/GiftCardDeleteDialog/useGiftCardBulkDelete.tsx | 2 +- .../GiftCardDeleteDialog/useGiftCardSingleDelete.tsx | 2 +- src/hooks/makeMutation.ts | 2 +- src/hooks/makeQuery.ts | 2 +- src/hooks/useNotifier/index.ts | 4 ++-- src/hooks/useNotifier/useNotifier.test.tsx | 2 +- src/hooks/useNotifier/useNotifier.tsx | 2 +- src/hooks/useOnSetDefaultVariant.ts | 2 +- src/modelTypes/views/PageTypeCreate.tsx | 2 +- src/modelTypes/views/PageTypeDetails.tsx | 2 +- src/modelTypes/views/PageTypeList/PageTypeList.tsx | 2 +- src/modeling/views/PageCreate.tsx | 2 +- src/modeling/views/PageDetails.tsx | 2 +- src/modeling/views/PageList/PageList.tsx | 2 +- .../components/OrderLineMetadataDialog/useHandleSubmit.ts | 2 +- .../OrderManualTransactionRefundPage.test.tsx | 2 +- .../OrderManualTransactionRefundForm.tsx | 2 +- src/orders/views/OrderDetails/OrderDetails.tsx | 2 +- src/orders/views/OrderDetails/OrderDetailsMessages.tsx | 2 +- src/orders/views/OrderDraftList/OrderDraftList.tsx | 2 +- src/orders/views/OrderDraftList/useBulkDeletion.ts | 2 +- .../views/OrderEditGrantRefund/OrderEditGrantRefund.tsx | 2 +- src/orders/views/OrderFulfill/OrderFulfill.tsx | 2 +- src/orders/views/OrderGrantRefund/OrderGrantRefund.tsx | 2 +- src/orders/views/OrderList/OrderList.tsx | 2 +- src/orders/views/OrderRefund/OrderRefund.tsx | 2 +- src/orders/views/OrderReturn/OrderReturn.tsx | 2 +- src/orders/views/OrderSendRefund/OrderSendRefund.tsx | 2 +- src/orders/views/OrderSettings.tsx | 2 +- .../OrderTransactionRefundCreate.tsx | 2 +- .../OrderTransactionRefundEdit/OrderTransactionRefundEdit.tsx | 2 +- .../views/PermissionGroupCreate/PermissionGroupCreate.tsx | 2 +- .../views/PermissionGroupDetails/PermissionGroupDetails.tsx | 2 +- src/productTypes/views/ProductTypeCreate.tsx | 2 +- src/productTypes/views/ProductTypeList/ProductTypeList.tsx | 2 +- src/productTypes/views/ProductTypeUpdate/index.tsx | 2 +- .../OrderDiscountProviders/OrderDiscountProvider.tsx | 2 +- .../OrderDiscountProviders/OrderLineDiscountProvider.tsx | 2 +- src/products/views/ProductCreate/ProductCreate.tsx | 2 +- src/products/views/ProductImage.tsx | 2 +- src/products/views/ProductList/ProductList.tsx | 2 +- src/products/views/ProductUpdate/ProductUpdate.tsx | 2 +- .../views/ProductUpdate/handlers/useProductUpdateHandler.ts | 2 +- src/products/views/ProductVariant/ProductVariant.tsx | 2 +- src/products/views/ProductVariantCreate.tsx | 2 +- .../components/RefundsSettingsPage/RefundsSettingsPage.tsx | 2 +- src/shipping/handlers.ts | 2 +- src/shipping/views/RateUpdate.tsx | 2 +- src/shipping/views/ShippingZoneCreate.tsx | 2 +- src/shipping/views/ShippingZoneDetails/index.tsx | 2 +- src/shipping/views/ShippingZonesList.tsx | 2 +- src/siteSettings/views/index.tsx | 2 +- .../StaffPasswordResetDialog/StaffPasswordResetDialog.tsx | 2 +- src/staff/hooks/useProfileOperations.ts | 2 +- src/staff/hooks/useStaffUserOperations.ts | 2 +- src/staff/views/StaffList/StaffList.tsx | 2 +- src/structures/views/MenuDetails/index.tsx | 2 +- src/structures/views/MenuList/MenuList.tsx | 2 +- src/taxes/views/TaxChannelsList.tsx | 2 +- src/taxes/views/TaxClassesList.tsx | 2 +- src/taxes/views/TaxCountriesList.tsx | 2 +- src/translations/views/TranslationsAttributes.tsx | 2 +- src/translations/views/TranslationsCategories.tsx | 2 +- src/translations/views/TranslationsCollections.tsx | 2 +- src/translations/views/TranslationsMenuItem.tsx | 2 +- src/translations/views/TranslationsPages.tsx | 2 +- src/translations/views/TranslationsProductVariants.tsx | 2 +- src/translations/views/TranslationsProducts.tsx | 2 +- src/translations/views/TranslationsSales.tsx | 2 +- src/translations/views/TranslationsShippingMethod.tsx | 2 +- src/translations/views/TranslationsVouchers.tsx | 2 +- src/warehouses/views/WarehouseCreate/WarehouseCreate.tsx | 2 +- src/warehouses/views/WarehouseDetails/WarehouseDetails.tsx | 2 +- src/warehouses/views/WarehouseList/WarehouseList.tsx | 2 +- 134 files changed, 135 insertions(+), 135 deletions(-) diff --git a/src/attributes/components/AttributeSwatchField/useFileProcessing.ts b/src/attributes/components/AttributeSwatchField/useFileProcessing.ts index e993ff9f2ac..5974ecf0fb3 100644 --- a/src/attributes/components/AttributeSwatchField/useFileProcessing.ts +++ b/src/attributes/components/AttributeSwatchField/useFileProcessing.ts @@ -1,6 +1,6 @@ import { AttributeValueEditDialogFormData } from "@dashboard/attributes/utils/data"; import { useFileUploadMutation } from "@dashboard/graphql"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import { errorMessages } from "@dashboard/intl"; import { useState } from "react"; import { useIntl } from "react-intl"; diff --git a/src/attributes/views/AttributeCreate/AttributeCreate.tsx b/src/attributes/views/AttributeCreate/AttributeCreate.tsx index 005ad9321e2..c478ad47c8e 100644 --- a/src/attributes/views/AttributeCreate/AttributeCreate.tsx +++ b/src/attributes/views/AttributeCreate/AttributeCreate.tsx @@ -9,7 +9,7 @@ import { import useListSettings from "@dashboard/hooks/useListSettings"; import useLocalPageInfo, { getMaxPage } from "@dashboard/hooks/useLocalPageInfo"; import useNavigator from "@dashboard/hooks/useNavigator"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import { getMutationErrors, getStringOrPlaceholder } from "@dashboard/misc"; import { ListViews, ReorderEvent } from "@dashboard/types"; import createDialogActionHandlers from "@dashboard/utils/handlers/dialogActionHandlers"; diff --git a/src/attributes/views/AttributeDetails/AttributeDetails.tsx b/src/attributes/views/AttributeDetails/AttributeDetails.tsx index d9de8cc3a8a..1fd33ccb987 100644 --- a/src/attributes/views/AttributeDetails/AttributeDetails.tsx +++ b/src/attributes/views/AttributeDetails/AttributeDetails.tsx @@ -13,7 +13,7 @@ import { import useListSettings from "@dashboard/hooks/useListSettings"; import useLocalPaginator, { useLocalPaginationState } from "@dashboard/hooks/useLocalPaginator"; import useNavigator from "@dashboard/hooks/useNavigator"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import { commonMessages } from "@dashboard/intl"; import { extractMutationErrors, getStringOrPlaceholder } from "@dashboard/misc"; import { ListViews, ReorderEvent } from "@dashboard/types"; diff --git a/src/attributes/views/AttributeList/AttributeList.tsx b/src/attributes/views/AttributeList/AttributeList.tsx index c25e7773a76..6b4b8ef65c9 100644 --- a/src/attributes/views/AttributeList/AttributeList.tsx +++ b/src/attributes/views/AttributeList/AttributeList.tsx @@ -11,7 +11,7 @@ import { useAttributeBulkDeleteMutation, useAttributeListQuery } from "@dashboar import { useFilterPresets } from "@dashboard/hooks/useFilterPresets"; import useListSettings from "@dashboard/hooks/useListSettings"; import useNavigator from "@dashboard/hooks/useNavigator"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import { usePaginationReset } from "@dashboard/hooks/usePaginationReset"; import usePaginator, { createPaginationState, diff --git a/src/auth/AuthProvider.test.tsx b/src/auth/AuthProvider.test.tsx index e57bbff22c3..3c4c5b8c077 100644 --- a/src/auth/AuthProvider.test.tsx +++ b/src/auth/AuthProvider.test.tsx @@ -1,6 +1,6 @@ import { useApolloClient } from "@apollo/client"; import { useUserDetailsQuery } from "@dashboard/graphql"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import { useAuth, useAuthState } from "@saleor/sdk"; import { act, renderHook } from "@testing-library/react-hooks"; import { useIntl } from "react-intl"; diff --git a/src/auth/AuthProvider.tsx b/src/auth/AuthProvider.tsx index 399c9ef55d5..2cdd1185a83 100644 --- a/src/auth/AuthProvider.tsx +++ b/src/auth/AuthProvider.tsx @@ -1,5 +1,5 @@ import { useApolloClient } from "@apollo/client"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import { PropsWithChildren } from "react"; import { useIntl } from "react-intl"; diff --git a/src/categories/views/CategoryCreate.tsx b/src/categories/views/CategoryCreate.tsx index ec7ddc266ea..2691d111e20 100644 --- a/src/categories/views/CategoryCreate.tsx +++ b/src/categories/views/CategoryCreate.tsx @@ -6,7 +6,7 @@ import { useUpdatePrivateMetadataMutation, } from "@dashboard/graphql"; import useNavigator from "@dashboard/hooks/useNavigator"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import { getMutationErrors } from "@dashboard/misc"; import createMetadataCreateHandler from "@dashboard/utils/handlers/metadataCreateHandler"; import { getParsedDataForJsonStringField } from "@dashboard/utils/richText/misc"; diff --git a/src/categories/views/CategoryDetails.tsx b/src/categories/views/CategoryDetails.tsx index 4dd2671ebfd..b1752084611 100644 --- a/src/categories/views/CategoryDetails.tsx +++ b/src/categories/views/CategoryDetails.tsx @@ -19,7 +19,7 @@ import useLocalPaginator, { useSectionLocalPaginationState, } from "@dashboard/hooks/useLocalPaginator"; import useNavigator from "@dashboard/hooks/useNavigator"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import { PaginatorContext } from "@dashboard/hooks/usePaginator"; import { useRowSelection } from "@dashboard/hooks/useRowSelection"; import { commonMessages, errorMessages } from "@dashboard/intl"; diff --git a/src/channels/views/ChannelCreate/ChannelCreate.tsx b/src/channels/views/ChannelCreate/ChannelCreate.tsx index 0b6a5c543bf..708fbd46c02 100644 --- a/src/channels/views/ChannelCreate/ChannelCreate.tsx +++ b/src/channels/views/ChannelCreate/ChannelCreate.tsx @@ -12,7 +12,7 @@ import { import { ChannelCreateInput as ChannelCreateInputWithAllowLegacyGiftCardUse } from "@dashboard/graphql/staging"; import { getSearchFetchMoreProps } from "@dashboard/hooks/makeTopLevelSearch/utils"; import useNavigator from "@dashboard/hooks/useNavigator"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import useShop from "@dashboard/hooks/useShop"; import { commonMessages } from "@dashboard/intl"; import getChannelsErrorMessage from "@dashboard/utils/errors/channels"; diff --git a/src/channels/views/ChannelDetails/ChannelDetails.tsx b/src/channels/views/ChannelDetails/ChannelDetails.tsx index 9c16d01edb3..ddabb760d84 100644 --- a/src/channels/views/ChannelDetails/ChannelDetails.tsx +++ b/src/channels/views/ChannelDetails/ChannelDetails.tsx @@ -24,7 +24,7 @@ import { } from "@dashboard/graphql/staging"; import { getSearchFetchMoreProps } from "@dashboard/hooks/makeTopLevelSearch/utils"; import useNavigator from "@dashboard/hooks/useNavigator"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import { getDefaultNotifierSuccessErrorData } from "@dashboard/hooks/useNotifier/utils"; import useShop from "@dashboard/hooks/useShop"; import { extractMutationErrors } from "@dashboard/misc"; diff --git a/src/channels/views/ChannelDetails/useChannelWarehouseReorder.ts b/src/channels/views/ChannelDetails/useChannelWarehouseReorder.ts index c27dcd4a959..34e2fdbe177 100644 --- a/src/channels/views/ChannelDetails/useChannelWarehouseReorder.ts +++ b/src/channels/views/ChannelDetails/useChannelWarehouseReorder.ts @@ -1,7 +1,7 @@ import { ChannelWarehouse } from "@dashboard/channels/pages/ChannelDetailsPage/types"; import { calculateItemsOrderMoves } from "@dashboard/channels/views/ChannelDetails/handlers"; import { useChannelReorderWarehousesMutation, WarehouseFragment } from "@dashboard/graphql"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import getChannelsErrorMessage from "@dashboard/utils/errors/channels"; import { useIntl } from "react-intl"; diff --git a/src/channels/views/ChannelsList/ChannelsList.tsx b/src/channels/views/ChannelsList/ChannelsList.tsx index 430741af570..c6dcbf59ea1 100644 --- a/src/channels/views/ChannelsList/ChannelsList.tsx +++ b/src/channels/views/ChannelsList/ChannelsList.tsx @@ -7,7 +7,7 @@ import { useChannelsQuery, } from "@dashboard/graphql"; import useNavigator from "@dashboard/hooks/useNavigator"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import getChannelsErrorMessage from "@dashboard/utils/errors/channels"; import createDialogActionHandlers from "@dashboard/utils/handlers/dialogActionHandlers"; import { useIntl } from "react-intl"; diff --git a/src/collections/components/CollectionProducts/CollectionProducts.tsx b/src/collections/components/CollectionProducts/CollectionProducts.tsx index ecc49bc0d3e..c3a7c50ecf1 100644 --- a/src/collections/components/CollectionProducts/CollectionProducts.tsx +++ b/src/collections/components/CollectionProducts/CollectionProducts.tsx @@ -23,7 +23,7 @@ import useBulkActions from "@dashboard/hooks/useBulkActions"; import useListSettings from "@dashboard/hooks/useListSettings"; import useLocalPaginator, { useLocalPaginationState } from "@dashboard/hooks/useLocalPaginator"; import useNavigator from "@dashboard/hooks/useNavigator"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import { PaginatorContext } from "@dashboard/hooks/usePaginator"; import useProductSearch from "@dashboard/searches/useProductSearch"; import { Container } from "@dashboard/types"; diff --git a/src/collections/components/CollectionProducts/useProductReorder.test.ts b/src/collections/components/CollectionProducts/useProductReorder.test.ts index 5b207a606e1..a2a56138cc1 100644 --- a/src/collections/components/CollectionProducts/useProductReorder.test.ts +++ b/src/collections/components/CollectionProducts/useProductReorder.test.ts @@ -1,6 +1,6 @@ import { useReorderProductsInCollectionMutation } from "@dashboard/graphql"; import { useLocalPaginationState } from "@dashboard/hooks/useLocalPaginator"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import { act, renderHook } from "@testing-library/react-hooks"; import { Product } from "./types"; diff --git a/src/collections/components/CollectionProducts/useProductReorder.ts b/src/collections/components/CollectionProducts/useProductReorder.ts index 133c2f80c5d..2d82a77089f 100644 --- a/src/collections/components/CollectionProducts/useProductReorder.ts +++ b/src/collections/components/CollectionProducts/useProductReorder.ts @@ -1,6 +1,6 @@ import { useReorderProductsInCollectionMutation } from "@dashboard/graphql"; import { PaginationState } from "@dashboard/hooks/useLocalPaginator"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import { useIntl } from "react-intl"; import { Product } from "./types"; diff --git a/src/collections/views/CollectionCreate.tsx b/src/collections/views/CollectionCreate.tsx index 155a30357ae..e6a7eaa50c4 100644 --- a/src/collections/views/CollectionCreate.tsx +++ b/src/collections/views/CollectionCreate.tsx @@ -13,7 +13,7 @@ import { } from "@dashboard/graphql"; import useChannels from "@dashboard/hooks/useChannels"; import useNavigator from "@dashboard/hooks/useNavigator"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import { commonMessages } from "@dashboard/intl"; import { getMutationErrors } from "@dashboard/misc"; import createDialogActionHandlers from "@dashboard/utils/handlers/dialogActionHandlers"; diff --git a/src/collections/views/CollectionDetails.tsx b/src/collections/views/CollectionDetails.tsx index 226008870c1..d296c5013e0 100644 --- a/src/collections/views/CollectionDetails.tsx +++ b/src/collections/views/CollectionDetails.tsx @@ -18,7 +18,7 @@ import { import useChannels from "@dashboard/hooks/useChannels"; import useLocalStorage from "@dashboard/hooks/useLocalStorage"; import useNavigator from "@dashboard/hooks/useNavigator"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import { commonMessages, errorMessages } from "@dashboard/intl"; import { arrayDiff } from "@dashboard/utils/arrays"; import createDialogActionHandlers from "@dashboard/utils/handlers/dialogActionHandlers"; diff --git a/src/collections/views/CollectionList/CollectionList.tsx b/src/collections/views/CollectionList/CollectionList.tsx index 16bc0df4945..aafff5c2d8d 100644 --- a/src/collections/views/CollectionList/CollectionList.tsx +++ b/src/collections/views/CollectionList/CollectionList.tsx @@ -9,7 +9,7 @@ import { useCollectionBulkDeleteMutation, useCollectionListQuery } from "@dashbo import { useFilterPresets } from "@dashboard/hooks/useFilterPresets"; import useListSettings from "@dashboard/hooks/useListSettings"; import useNavigator from "@dashboard/hooks/useNavigator"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import { usePaginationReset } from "@dashboard/hooks/usePaginationReset"; import usePaginator, { createPaginationState, diff --git a/src/components/ConditionalFilter/API/initialState/product/useProductInitialAPIState.tsx b/src/components/ConditionalFilter/API/initialState/product/useProductInitialAPIState.tsx index 657078fb7d8..99105441d69 100644 --- a/src/components/ConditionalFilter/API/initialState/product/useProductInitialAPIState.tsx +++ b/src/components/ConditionalFilter/API/initialState/product/useProductInitialAPIState.tsx @@ -26,7 +26,7 @@ import { _SearchProductVariantOperandsQueryVariables, AttributeEntityTypeEnum, } from "@dashboard/graphql"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import { useCallback, useState } from "react"; import { useIntl } from "react-intl"; diff --git a/src/components/MetadataDialog/useHandleMetadataSubmit.ts b/src/components/MetadataDialog/useHandleMetadataSubmit.ts index 64741e66870..932ed738f5e 100644 --- a/src/components/MetadataDialog/useHandleMetadataSubmit.ts +++ b/src/components/MetadataDialog/useHandleMetadataSubmit.ts @@ -1,7 +1,7 @@ import { DocumentNode, useApolloClient } from "@apollo/client"; import { MetadataFormData } from "@dashboard/components/Metadata"; import { useUpdateMetadataMutation, useUpdatePrivateMetadataMutation } from "@dashboard/graphql"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import { commonMessages } from "@dashboard/intl"; import createMetadataUpdateHandler from "@dashboard/utils/handlers/metadataUpdateHandler"; import { useMemo, useRef } from "react"; diff --git a/src/containers/BackgroundTasks/BackgroundTasksProvider.tsx b/src/containers/BackgroundTasks/BackgroundTasksProvider.tsx index bfb16986321..fe0e38505c4 100644 --- a/src/containers/BackgroundTasks/BackgroundTasksProvider.tsx +++ b/src/containers/BackgroundTasks/BackgroundTasksProvider.tsx @@ -1,7 +1,7 @@ // @ts-strict-ignore import { ApolloClient, useApolloClient } from "@apollo/client"; import { INotificationCallback } from "@dashboard/components/notifications"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import { ReactNode, useEffect, useRef } from "react"; import { IntlShape, useIntl } from "react-intl"; diff --git a/src/customers/views/CustomerAddresses.tsx b/src/customers/views/CustomerAddresses.tsx index b96bd24ce18..f307a5a36af 100644 --- a/src/customers/views/CustomerAddresses.tsx +++ b/src/customers/views/CustomerAddresses.tsx @@ -9,7 +9,7 @@ import { useUpdateCustomerAddressMutation, } from "@dashboard/graphql"; import useNavigator from "@dashboard/hooks/useNavigator"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import useShop from "@dashboard/hooks/useShop"; import { commonMessages } from "@dashboard/intl"; import createDialogActionHandlers from "@dashboard/utils/handlers/dialogActionHandlers"; diff --git a/src/customers/views/CustomerCreate.tsx b/src/customers/views/CustomerCreate.tsx index 2236c021e92..a87fac6b67c 100644 --- a/src/customers/views/CustomerCreate.tsx +++ b/src/customers/views/CustomerCreate.tsx @@ -2,7 +2,7 @@ import { WindowTitle } from "@dashboard/components/WindowTitle"; import { useCreateCustomerMutation, useCustomerCreateDataQuery } from "@dashboard/graphql"; import useNavigator from "@dashboard/hooks/useNavigator"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import { useIntl } from "react-intl"; import { extractMutationErrors, maybe } from "../../misc"; diff --git a/src/customers/views/CustomerDetails.tsx b/src/customers/views/CustomerDetails.tsx index 65aec5b5b88..cbbd0db04c2 100644 --- a/src/customers/views/CustomerDetails.tsx +++ b/src/customers/views/CustomerDetails.tsx @@ -4,7 +4,7 @@ import NotFoundPage from "@dashboard/components/NotFoundPage"; import { WindowTitle } from "@dashboard/components/WindowTitle"; import { useRemoveCustomerMutation, useUpdateCustomerMutation } from "@dashboard/graphql"; import useNavigator from "@dashboard/hooks/useNavigator"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import { commonMessages } from "@dashboard/intl"; import { extractMutationErrors, getStringOrPlaceholder } from "@dashboard/misc"; import { FormattedMessage, useIntl } from "react-intl"; diff --git a/src/customers/views/CustomerList/CustomerList.tsx b/src/customers/views/CustomerList/CustomerList.tsx index 92580de7d90..f51be5f1d40 100644 --- a/src/customers/views/CustomerList/CustomerList.tsx +++ b/src/customers/views/CustomerList/CustomerList.tsx @@ -8,7 +8,7 @@ import { useBulkRemoveCustomersMutation, useListCustomersQuery } from "@dashboar import { useFilterPresets } from "@dashboard/hooks/useFilterPresets"; import useListSettings from "@dashboard/hooks/useListSettings"; import useNavigator from "@dashboard/hooks/useNavigator"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import { usePaginationReset } from "@dashboard/hooks/usePaginationReset"; import usePaginator, { createPaginationState, diff --git a/src/discounts/components/DiscountSavebar/DiscountSavebar.tsx b/src/discounts/components/DiscountSavebar/DiscountSavebar.tsx index 1fc79ab1021..d1504d9dd92 100644 --- a/src/discounts/components/DiscountSavebar/DiscountSavebar.tsx +++ b/src/discounts/components/DiscountSavebar/DiscountSavebar.tsx @@ -1,6 +1,6 @@ import { ConfirmButtonTransitionState } from "@dashboard/components/ConfirmButton"; import { Savebar } from "@dashboard/components/Savebar"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import { commonMessages } from "@dashboard/intl"; import { useFormContext } from "react-hook-form"; import { useIntl } from "react-intl"; diff --git a/src/discounts/views/DiscountCreate/DiscountCreate.tsx b/src/discounts/views/DiscountCreate/DiscountCreate.tsx index df7d73374dd..f222b11d418 100644 --- a/src/discounts/views/DiscountCreate/DiscountCreate.tsx +++ b/src/discounts/views/DiscountCreate/DiscountCreate.tsx @@ -4,7 +4,7 @@ import { DiscountCreatePage } from "@dashboard/discounts/components/DiscountCrea import { discountListUrl, discountUrl } from "@dashboard/discounts/discountsUrls"; import { usePromotionCreateMutation } from "@dashboard/graphql"; import useNavigator from "@dashboard/hooks/useNavigator"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import { commonMessages } from "@dashboard/intl"; import { getMutationErrors } from "@dashboard/misc"; import { useIntl } from "react-intl"; diff --git a/src/discounts/views/DiscountDetails/hooks/usePromotionDelete.ts b/src/discounts/views/DiscountDetails/hooks/usePromotionDelete.ts index 9f90c3ada33..05167dd47f7 100644 --- a/src/discounts/views/DiscountDetails/hooks/usePromotionDelete.ts +++ b/src/discounts/views/DiscountDetails/hooks/usePromotionDelete.ts @@ -1,7 +1,7 @@ import { discountListUrl } from "@dashboard/discounts/discountsUrls"; import { usePromotionDeleteMutation } from "@dashboard/graphql"; import useNavigator from "@dashboard/hooks/useNavigator"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import { useIntl } from "react-intl"; export const usePromotionDelete = () => { diff --git a/src/discounts/views/DiscountDetails/hooks/usePromotionRuleCreate.ts b/src/discounts/views/DiscountDetails/hooks/usePromotionRuleCreate.ts index adc7a8ca16d..451b4cdfd95 100644 --- a/src/discounts/views/DiscountDetails/hooks/usePromotionRuleCreate.ts +++ b/src/discounts/views/DiscountDetails/hooks/usePromotionRuleCreate.ts @@ -6,7 +6,7 @@ import { PromotionRuleDetailsFragment, usePromotionRuleCreateMutation, } from "@dashboard/graphql"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import { commonMessages } from "@dashboard/intl"; import { useIntl } from "react-intl"; diff --git a/src/discounts/views/DiscountDetails/hooks/usePromotionRuleDelete.ts b/src/discounts/views/DiscountDetails/hooks/usePromotionRuleDelete.ts index b6e180dd636..319cf3b2dad 100644 --- a/src/discounts/views/DiscountDetails/hooks/usePromotionRuleDelete.ts +++ b/src/discounts/views/DiscountDetails/hooks/usePromotionRuleDelete.ts @@ -6,7 +6,7 @@ import { PromotionRuleDetailsFragment, usePromotionRuleDeleteMutation, } from "@dashboard/graphql"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import { commonMessages } from "@dashboard/intl"; import { useIntl } from "react-intl"; diff --git a/src/discounts/views/DiscountDetails/hooks/usePromotionRuleUpdate.ts b/src/discounts/views/DiscountDetails/hooks/usePromotionRuleUpdate.ts index a222f0993c0..c07a76a83e4 100644 --- a/src/discounts/views/DiscountDetails/hooks/usePromotionRuleUpdate.ts +++ b/src/discounts/views/DiscountDetails/hooks/usePromotionRuleUpdate.ts @@ -6,7 +6,7 @@ import { PromotionRuleUpdateMutation, usePromotionRuleUpdateMutation, } from "@dashboard/graphql"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import { commonMessages } from "@dashboard/intl"; import { useIntl } from "react-intl"; diff --git a/src/discounts/views/DiscountDetails/hooks/usePromotionUpdate.ts b/src/discounts/views/DiscountDetails/hooks/usePromotionUpdate.ts index 14b72e55d08..f3c2035634e 100644 --- a/src/discounts/views/DiscountDetails/hooks/usePromotionUpdate.ts +++ b/src/discounts/views/DiscountDetails/hooks/usePromotionUpdate.ts @@ -3,7 +3,7 @@ import { PromotionDetailsFragment, usePromotionUpdateMutation, } from "@dashboard/graphql"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import { commonMessages } from "@dashboard/intl"; import { useIntl } from "react-intl"; diff --git a/src/discounts/views/VoucherCreate/VoucherCreate.tsx b/src/discounts/views/VoucherCreate/VoucherCreate.tsx index 6a169b6d497..9c902089548 100644 --- a/src/discounts/views/VoucherCreate/VoucherCreate.tsx +++ b/src/discounts/views/VoucherCreate/VoucherCreate.tsx @@ -14,7 +14,7 @@ import { import useBulkActions from "@dashboard/hooks/useBulkActions"; import useChannels from "@dashboard/hooks/useChannels"; import useNavigator from "@dashboard/hooks/useNavigator"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import useShop from "@dashboard/hooks/useShop"; import { sectionNames } from "@dashboard/intl"; import { useCategoryWithTotalProductsSearch } from "@dashboard/searches/useCategorySearch"; diff --git a/src/discounts/views/VoucherDetails/VoucherDetails.tsx b/src/discounts/views/VoucherDetails/VoucherDetails.tsx index 64a2d476bc0..0332f318096 100644 --- a/src/discounts/views/VoucherDetails/VoucherDetails.tsx +++ b/src/discounts/views/VoucherDetails/VoucherDetails.tsx @@ -48,7 +48,7 @@ import useLocalPaginator, { useSectionLocalPaginationState, } from "@dashboard/hooks/useLocalPaginator"; import useNavigator from "@dashboard/hooks/useNavigator"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import { PaginatorContext } from "@dashboard/hooks/usePaginator"; import useShop from "@dashboard/hooks/useShop"; import { commonMessages, sectionNames } from "@dashboard/intl"; diff --git a/src/discounts/views/VoucherList/VoucherList.tsx b/src/discounts/views/VoucherList/VoucherList.tsx index ab1302f2515..fb6b4761d98 100644 --- a/src/discounts/views/VoucherList/VoucherList.tsx +++ b/src/discounts/views/VoucherList/VoucherList.tsx @@ -10,7 +10,7 @@ import { useVoucherBulkDeleteMutation, useVoucherListQuery } from "@dashboard/gr import { useFilterPresets } from "@dashboard/hooks/useFilterPresets"; import useListSettings from "@dashboard/hooks/useListSettings"; import useNavigator from "@dashboard/hooks/useNavigator"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import { usePaginationReset } from "@dashboard/hooks/usePaginationReset"; import usePaginator, { createPaginationState, diff --git a/src/extensions/views/AddCustomExtension/hooks/useHandleCreateAppSubmit.ts b/src/extensions/views/AddCustomExtension/hooks/useHandleCreateAppSubmit.ts index 23358d57cdb..160e236d694 100644 --- a/src/extensions/views/AddCustomExtension/hooks/useHandleCreateAppSubmit.ts +++ b/src/extensions/views/AddCustomExtension/hooks/useHandleCreateAppSubmit.ts @@ -1,7 +1,7 @@ import { ExtensionsUrls } from "@dashboard/extensions/urls"; import { AppErrorCode, PermissionEnum, useAppCreateMutation } from "@dashboard/graphql"; import useNavigator from "@dashboard/hooks/useNavigator"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import { commonMessages } from "@dashboard/intl"; import { SubmitHandler, UseFormSetError } from "react-hook-form"; import { useIntl } from "react-intl"; diff --git a/src/extensions/views/AddCustomExtensionWebhook/AddCustomExtensionWebhook.tsx b/src/extensions/views/AddCustomExtensionWebhook/AddCustomExtensionWebhook.tsx index bef12121cdb..f8ce747760a 100644 --- a/src/extensions/views/AddCustomExtensionWebhook/AddCustomExtensionWebhook.tsx +++ b/src/extensions/views/AddCustomExtensionWebhook/AddCustomExtensionWebhook.tsx @@ -6,7 +6,7 @@ import { WebhookEventTypeAsyncEnum, } from "@dashboard/graphql"; import useNavigator from "@dashboard/hooks/useNavigator"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import { commonMessages } from "@dashboard/intl"; import { extractMutationErrors } from "@dashboard/misc"; import { useIntl } from "react-intl"; diff --git a/src/extensions/views/EditCustomExtension/EditCustomApp.tsx b/src/extensions/views/EditCustomExtension/EditCustomApp.tsx index 2db1f393c0f..a8ff073d479 100644 --- a/src/extensions/views/EditCustomExtension/EditCustomApp.tsx +++ b/src/extensions/views/EditCustomExtension/EditCustomApp.tsx @@ -24,7 +24,7 @@ import { } from "@dashboard/graphql"; import { useHasManagedAppsPermission } from "@dashboard/hooks/useHasManagedAppsPermission"; import useNavigator from "@dashboard/hooks/useNavigator"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import useShop from "@dashboard/hooks/useShop"; import { commonMessages } from "@dashboard/intl"; import { extractMutationErrors, getStringOrPlaceholder, parseLogMessage } from "@dashboard/misc"; diff --git a/src/extensions/views/EditCustomExtension/components/TokenCreateDialog/useClipboardCopy.ts b/src/extensions/views/EditCustomExtension/components/TokenCreateDialog/useClipboardCopy.ts index bb2b73befcd..8946c541810 100644 --- a/src/extensions/views/EditCustomExtension/components/TokenCreateDialog/useClipboardCopy.ts +++ b/src/extensions/views/EditCustomExtension/components/TokenCreateDialog/useClipboardCopy.ts @@ -1,5 +1,5 @@ import { ConfirmButtonTransitionState } from "@dashboard/components/ConfirmButton"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import { useEffect, useRef, useState } from "react"; import { useIntl } from "react-intl"; diff --git a/src/extensions/views/EditCustomExtensionWebhook/EditCustomExtensionWebhook.tsx b/src/extensions/views/EditCustomExtensionWebhook/EditCustomExtensionWebhook.tsx index 328abf18fdb..e051b3bcb6f 100644 --- a/src/extensions/views/EditCustomExtensionWebhook/EditCustomExtensionWebhook.tsx +++ b/src/extensions/views/EditCustomExtensionWebhook/EditCustomExtensionWebhook.tsx @@ -6,7 +6,7 @@ import { useWebhookUpdateMutation, WebhookEventTypeAsyncEnum, } from "@dashboard/graphql"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import { commonMessages } from "@dashboard/intl"; import { extractMutationErrors, getStringOrPlaceholder } from "@dashboard/misc"; import { useIntl } from "react-intl"; diff --git a/src/extensions/views/EditManifestExtension/AppManageView.tsx b/src/extensions/views/EditManifestExtension/AppManageView.tsx index 3b9d2688297..ae5b6761835 100644 --- a/src/extensions/views/EditManifestExtension/AppManageView.tsx +++ b/src/extensions/views/EditManifestExtension/AppManageView.tsx @@ -19,7 +19,7 @@ import { } from "@dashboard/graphql"; import { useHasManagedAppsPermission } from "@dashboard/hooks/useHasManagedAppsPermission"; import useNavigator from "@dashboard/hooks/useNavigator"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import getAppErrorMessage from "@dashboard/utils/errors/app"; import createDialogActionHandlers from "@dashboard/utils/handlers/dialogActionHandlers"; import { useIntl } from "react-intl"; diff --git a/src/extensions/views/EditManifestExtension/components/AppPermissionsDialog/AppPermissionsDialog.tsx b/src/extensions/views/EditManifestExtension/components/AppPermissionsDialog/AppPermissionsDialog.tsx index 9d623492253..71b4466ff06 100644 --- a/src/extensions/views/EditManifestExtension/components/AppPermissionsDialog/AppPermissionsDialog.tsx +++ b/src/extensions/views/EditManifestExtension/components/AppPermissionsDialog/AppPermissionsDialog.tsx @@ -2,7 +2,7 @@ import { DashboardModal } from "@dashboard/components/Modal"; import { getCustomAppErrorMessage } from "@dashboard/extensions/utils"; import { useGetAvailableAppPermissions } from "@dashboard/extensions/views/EditManifestExtension/hooks/useGetAvailableAppPermissions"; import { PermissionEnum, useAppQuery, useAppUpdatePermissionsMutation } from "@dashboard/graphql"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import { Box, Skeleton, Text } from "@saleor/macaw-ui-next"; import { useEffect } from "react"; import { useIntl } from "react-intl"; diff --git a/src/extensions/views/EditPluginExtension/EditPluginExtension.tsx b/src/extensions/views/EditPluginExtension/EditPluginExtension.tsx index f3faaf13e3f..8b5a4a1c58e 100644 --- a/src/extensions/views/EditPluginExtension/EditPluginExtension.tsx +++ b/src/extensions/views/EditPluginExtension/EditPluginExtension.tsx @@ -9,7 +9,7 @@ import { usePluginUpdateMutation, } from "@dashboard/graphql"; import useNavigator from "@dashboard/hooks/useNavigator"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import useStateFromProps from "@dashboard/hooks/useStateFromProps"; import { commonMessages } from "@dashboard/intl"; import { extractMutationErrors } from "@dashboard/misc"; diff --git a/src/extensions/views/InstallCustomExtension/hooks/useInstallApp.test.ts b/src/extensions/views/InstallCustomExtension/hooks/useInstallApp.test.ts index 1a84e638760..f37c698a1ed 100644 --- a/src/extensions/views/InstallCustomExtension/hooks/useInstallApp.test.ts +++ b/src/extensions/views/InstallCustomExtension/hooks/useInstallApp.test.ts @@ -1,7 +1,7 @@ import { AppManifestFragment, PermissionEnum, useAppInstallMutation } from "@dashboard/graphql"; import useLocalStorage from "@dashboard/hooks/useLocalStorage"; import useNavigator from "@dashboard/hooks/useNavigator"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import { extractMutationErrors } from "@dashboard/misc"; import { act, renderHook } from "@testing-library/react-hooks"; import { useIntl } from "react-intl"; diff --git a/src/extensions/views/InstallCustomExtension/hooks/useInstallApp.ts b/src/extensions/views/InstallCustomExtension/hooks/useInstallApp.ts index 1b59a3c297c..f2c50889016 100644 --- a/src/extensions/views/InstallCustomExtension/hooks/useInstallApp.ts +++ b/src/extensions/views/InstallCustomExtension/hooks/useInstallApp.ts @@ -2,7 +2,7 @@ import { getAppInstallErrorMessage } from "@dashboard/extensions/utils"; import { useAppInstallMutation } from "@dashboard/graphql"; import useLocalStorage from "@dashboard/hooks/useLocalStorage"; import useNavigator from "@dashboard/hooks/useNavigator"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import { extractMutationErrors } from "@dashboard/misc"; import { UseFormGetValues } from "react-hook-form"; import { useIntl } from "react-intl"; diff --git a/src/extensions/views/InstalledExtensions/hooks/useInstallationNotify.ts b/src/extensions/views/InstalledExtensions/hooks/useInstallationNotify.ts index b891d180dea..4ce88db1281 100644 --- a/src/extensions/views/InstalledExtensions/hooks/useInstallationNotify.ts +++ b/src/extensions/views/InstalledExtensions/hooks/useInstallationNotify.ts @@ -1,5 +1,5 @@ import { notifyMessages } from "@dashboard/extensions/messages"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import { useIntl } from "react-intl"; export const useInstallationNotify = () => { diff --git a/src/extensions/views/ViewManifestExtension/ViewManifestExtensionIframe.tsx b/src/extensions/views/ViewManifestExtension/ViewManifestExtensionIframe.tsx index 6e679090a58..5a09d7fb9bb 100644 --- a/src/extensions/views/ViewManifestExtension/ViewManifestExtensionIframe.tsx +++ b/src/extensions/views/ViewManifestExtension/ViewManifestExtensionIframe.tsx @@ -4,7 +4,7 @@ import { ExtensionsUrls } from "@dashboard/extensions/urls"; import { useAppQuery } from "@dashboard/graphql"; import { useHasManagedAppsPermission } from "@dashboard/hooks/useHasManagedAppsPermission"; import useNavigator from "@dashboard/hooks/useNavigator"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import { useCallback } from "react"; import { useIntl } from "react-intl"; import { useLocation } from "react-router"; diff --git a/src/extensions/views/ViewManifestExtension/components/AppFrame/appActionsHandler.ts b/src/extensions/views/ViewManifestExtension/components/AppFrame/appActionsHandler.ts index f4343327d92..1778317e3f9 100644 --- a/src/extensions/views/ViewManifestExtension/components/AppFrame/appActionsHandler.ts +++ b/src/extensions/views/ViewManifestExtension/components/AppFrame/appActionsHandler.ts @@ -2,7 +2,7 @@ import { getAppMountUri } from "@dashboard/config"; import { useActiveAppExtension } from "@dashboard/extensions/components/AppExtensionContext/AppExtensionContextProvider"; import { ExtensionsUrls, LegacyAppPaths } from "@dashboard/extensions/urls"; import useNavigator from "@dashboard/hooks/useNavigator"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import { DashboardEventFactory, DispatchResponseEvent, diff --git a/src/extensions/views/ViewManifestExtension/components/AppFrame/useAppActions.ts b/src/extensions/views/ViewManifestExtension/components/AppFrame/useAppActions.ts index e6cf6cfc1c9..ba35e62734b 100644 --- a/src/extensions/views/ViewManifestExtension/components/AppFrame/useAppActions.ts +++ b/src/extensions/views/ViewManifestExtension/components/AppFrame/useAppActions.ts @@ -1,4 +1,4 @@ -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import { Actions, DispatchResponseEvent } from "@saleor/app-sdk/app-bridge"; import { captureMessage } from "@sentry/react"; import { useEffect, useState } from "react"; diff --git a/src/giftCards/GiftCardBulkCreateDialog/GiftCardBulkCreateDialog.tsx b/src/giftCards/GiftCardBulkCreateDialog/GiftCardBulkCreateDialog.tsx index 35cee9859e5..0923e2754ba 100644 --- a/src/giftCards/GiftCardBulkCreateDialog/GiftCardBulkCreateDialog.tsx +++ b/src/giftCards/GiftCardBulkCreateDialog/GiftCardBulkCreateDialog.tsx @@ -2,7 +2,7 @@ import { DashboardModal } from "@dashboard/components/Modal"; import { INotification } from "@dashboard/components/notifications"; import { GiftCardBulkCreateInput, useGiftCardBulkCreateMutation } from "@dashboard/graphql"; import { useCurrentDate } from "@dashboard/hooks/useCurrentDate"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import { DialogProps } from "@dashboard/types"; import { getFormErrors } from "@dashboard/utils/errors"; import { useEffect, useState } from "react"; diff --git a/src/giftCards/GiftCardCreateDialog/GiftCardCreateDialogCodeContent.tsx b/src/giftCards/GiftCardCreateDialog/GiftCardCreateDialogCodeContent.tsx index 08611f9a134..2cc466e2f8f 100644 --- a/src/giftCards/GiftCardCreateDialog/GiftCardCreateDialogCodeContent.tsx +++ b/src/giftCards/GiftCardCreateDialog/GiftCardCreateDialogCodeContent.tsx @@ -1,6 +1,6 @@ import { DashboardModal } from "@dashboard/components/Modal"; import { useClipboard } from "@dashboard/hooks/useClipboard"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import { buttonMessages } from "@dashboard/intl"; import { Button, Text } from "@saleor/macaw-ui-next"; import { useIntl } from "react-intl"; diff --git a/src/giftCards/GiftCardCreateDialog/GiftCardCreateDialogContent.tsx b/src/giftCards/GiftCardCreateDialog/GiftCardCreateDialogContent.tsx index 615abb428f6..ecfc9dedfb6 100644 --- a/src/giftCards/GiftCardCreateDialog/GiftCardCreateDialogContent.tsx +++ b/src/giftCards/GiftCardCreateDialog/GiftCardCreateDialogContent.tsx @@ -2,7 +2,7 @@ import { DashboardModal } from "@dashboard/components/Modal"; import { GiftCardCreateInput, useGiftCardCreateMutation } from "@dashboard/graphql"; import { useCurrentDate } from "@dashboard/hooks/useCurrentDate"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import { DialogProps } from "@dashboard/types"; import { useState } from "react"; import { useIntl } from "react-intl"; diff --git a/src/giftCards/GiftCardExportDialogContent/GiftCardExportDialogContent.tsx b/src/giftCards/GiftCardExportDialogContent/GiftCardExportDialogContent.tsx index 75afa29de99..7b30e25beec 100644 --- a/src/giftCards/GiftCardExportDialogContent/GiftCardExportDialogContent.tsx +++ b/src/giftCards/GiftCardExportDialogContent/GiftCardExportDialogContent.tsx @@ -4,7 +4,7 @@ import { Task } from "@dashboard/containers/BackgroundTasks/types"; import { useExportGiftCardsMutation, useGiftCardTotalCountQuery } from "@dashboard/graphql"; import useBackgroundTask from "@dashboard/hooks/useBackgroundTask"; import useForm from "@dashboard/hooks/useForm"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import ExportDialogSettings from "@dashboard/products/components/ProductExportDialog/ExportDialogSettings"; import { ExportSettingsFormData, diff --git a/src/giftCards/GiftCardUpdate/GiftCardHistory/GiftCardHistory.tsx b/src/giftCards/GiftCardUpdate/GiftCardHistory/GiftCardHistory.tsx index deb029a4c7f..6fccd88ef64 100644 --- a/src/giftCards/GiftCardUpdate/GiftCardHistory/GiftCardHistory.tsx +++ b/src/giftCards/GiftCardUpdate/GiftCardHistory/GiftCardHistory.tsx @@ -6,7 +6,7 @@ import { TimelineNote } from "@dashboard/components/Timeline/TimelineNote"; import { toActor } from "@dashboard/components/Timeline/utils"; import { useGiftCardDetails } from "@dashboard/giftCards/GiftCardUpdate/providers/GiftCardDetailsProvider"; import { GiftCardEventsEnum, useGiftCardAddNoteMutation } from "@dashboard/graphql"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import { HistoryComponentLoader } from "@dashboard/orders/components/OrderHistory/HistoryComponentLoader"; import { FormattedMessage, useIntl } from "react-intl"; diff --git a/src/giftCards/GiftCardUpdate/GiftCardResendCodeDialog/GiftCardResendCodeDialog.tsx b/src/giftCards/GiftCardUpdate/GiftCardResendCodeDialog/GiftCardResendCodeDialog.tsx index 502dcafbf4a..e92f22d71ac 100644 --- a/src/giftCards/GiftCardUpdate/GiftCardResendCodeDialog/GiftCardResendCodeDialog.tsx +++ b/src/giftCards/GiftCardUpdate/GiftCardResendCodeDialog/GiftCardResendCodeDialog.tsx @@ -6,7 +6,7 @@ import { INotification } from "@dashboard/components/notifications"; import { useGiftCardPermissions } from "@dashboard/giftCards/hooks/useGiftCardPermissions"; import { useChannelsQuery, useGiftCardResendMutation } from "@dashboard/graphql"; import useForm from "@dashboard/hooks/useForm"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import { getBySlug } from "@dashboard/misc"; import { DialogProps } from "@dashboard/types"; import commonErrorMessages from "@dashboard/utils/errors/common"; diff --git a/src/giftCards/GiftCardUpdate/GiftCardUpdateBalanceDialog/GiftCardUpdateBalanceDialog.tsx b/src/giftCards/GiftCardUpdate/GiftCardUpdateBalanceDialog/GiftCardUpdateBalanceDialog.tsx index 0c42e050251..10110b3b412 100644 --- a/src/giftCards/GiftCardUpdate/GiftCardUpdateBalanceDialog/GiftCardUpdateBalanceDialog.tsx +++ b/src/giftCards/GiftCardUpdate/GiftCardUpdateBalanceDialog/GiftCardUpdateBalanceDialog.tsx @@ -5,7 +5,7 @@ import { INotification } from "@dashboard/components/notifications"; import { useGiftCardPermissions } from "@dashboard/giftCards/hooks/useGiftCardPermissions"; import { useGiftCardUpdateMutation } from "@dashboard/graphql"; import useForm from "@dashboard/hooks/useForm"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import { DialogProps } from "@dashboard/types"; import commonErrorMessages from "@dashboard/utils/errors/common"; import { Input, Text } from "@saleor/macaw-ui-next"; diff --git a/src/giftCards/GiftCardUpdate/GiftCardUpdatePageHeader/hooks/useGiftCardActivateToggle.ts b/src/giftCards/GiftCardUpdate/GiftCardUpdatePageHeader/hooks/useGiftCardActivateToggle.ts index dac18ae4d21..f5a77eb40b5 100644 --- a/src/giftCards/GiftCardUpdate/GiftCardUpdatePageHeader/hooks/useGiftCardActivateToggle.ts +++ b/src/giftCards/GiftCardUpdate/GiftCardUpdatePageHeader/hooks/useGiftCardActivateToggle.ts @@ -1,5 +1,5 @@ import { useGiftCardActivateMutation, useGiftCardDeactivateMutation } from "@dashboard/graphql"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import commonErrorMessages from "@dashboard/utils/errors/common"; import { useIntl } from "react-intl"; diff --git a/src/giftCards/GiftCardUpdate/providers/GiftCardUpdateFormProvider/GiftCardUpdateFormProvider.tsx b/src/giftCards/GiftCardUpdate/providers/GiftCardUpdateFormProvider/GiftCardUpdateFormProvider.tsx index 2abfe35b805..d02dfece0f5 100644 --- a/src/giftCards/GiftCardUpdate/providers/GiftCardUpdateFormProvider/GiftCardUpdateFormProvider.tsx +++ b/src/giftCards/GiftCardUpdate/providers/GiftCardUpdateFormProvider/GiftCardUpdateFormProvider.tsx @@ -12,7 +12,7 @@ import { import { MutationResultWithOpts } from "@dashboard/hooks/makeMutation"; import useForm, { FormChange, UseFormResult } from "@dashboard/hooks/useForm"; import useHandleFormSubmit from "@dashboard/hooks/useHandleFormSubmit"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import { getDefaultNotifierSuccessErrorData } from "@dashboard/hooks/useNotifier/utils"; import { getFormErrors } from "@dashboard/utils/errors"; import createMetadataUpdateHandler from "@dashboard/utils/handlers/metadataUpdateHandler"; diff --git a/src/giftCards/GiftCardsList/GiftCardListBulkActions/GiftCardListBulkActions.tsx b/src/giftCards/GiftCardsList/GiftCardListBulkActions/GiftCardListBulkActions.tsx index f0ccf67ac92..a44fda9c928 100644 --- a/src/giftCards/GiftCardsList/GiftCardListBulkActions/GiftCardListBulkActions.tsx +++ b/src/giftCards/GiftCardsList/GiftCardListBulkActions/GiftCardListBulkActions.tsx @@ -4,7 +4,7 @@ import { useGiftCardBulkActivateMutation, useGiftCardBulkDeactivateMutation, } from "@dashboard/graphql"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import { getByIds } from "@dashboard/orders/components/OrderReturnPage/utils"; import { useIntl } from "react-intl"; diff --git a/src/giftCards/GiftCardsList/providers/GiftCardListProvider/GiftCardListProvider.tsx b/src/giftCards/GiftCardsList/providers/GiftCardListProvider/GiftCardListProvider.tsx index eb271541d44..dd6eb8fae2d 100644 --- a/src/giftCards/GiftCardsList/providers/GiftCardListProvider/GiftCardListProvider.tsx +++ b/src/giftCards/GiftCardsList/providers/GiftCardListProvider/GiftCardListProvider.tsx @@ -13,7 +13,7 @@ import { import { UseFilterPresets, useFilterPresets } from "@dashboard/hooks/useFilterPresets"; import useListSettings, { UseListSettings } from "@dashboard/hooks/useListSettings"; import useNavigator from "@dashboard/hooks/useNavigator"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import { usePaginationReset } from "@dashboard/hooks/usePaginationReset"; import { createPaginationState, PageInfo, PaginationState } from "@dashboard/hooks/usePaginator"; import { UseRowSelection, useRowSelection } from "@dashboard/hooks/useRowSelection"; diff --git a/src/giftCards/components/GiftCardDeleteDialog/useGiftCardBulkDelete.tsx b/src/giftCards/components/GiftCardDeleteDialog/useGiftCardBulkDelete.tsx index e97ad3ee56a..1c0ad5a0c23 100644 --- a/src/giftCards/components/GiftCardDeleteDialog/useGiftCardBulkDelete.tsx +++ b/src/giftCards/components/GiftCardDeleteDialog/useGiftCardBulkDelete.tsx @@ -1,7 +1,7 @@ import { useGiftCardList } from "@dashboard/giftCards/GiftCardsList/providers/GiftCardListProvider"; import { BulkDeleteGiftCardMutation, useBulkDeleteGiftCardMutation } from "@dashboard/graphql"; import { MutationResultWithOpts } from "@dashboard/hooks/makeMutation"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import commonErrorMessages from "@dashboard/utils/errors/common"; import { useIntl } from "react-intl"; diff --git a/src/giftCards/components/GiftCardDeleteDialog/useGiftCardSingleDelete.tsx b/src/giftCards/components/GiftCardDeleteDialog/useGiftCardSingleDelete.tsx index e60197e693e..0c0106295a1 100644 --- a/src/giftCards/components/GiftCardDeleteDialog/useGiftCardSingleDelete.tsx +++ b/src/giftCards/components/GiftCardDeleteDialog/useGiftCardSingleDelete.tsx @@ -2,7 +2,7 @@ import { getGiftCardErrorMessage } from "@dashboard/giftCards/GiftCardUpdate/messages"; import { DeleteGiftCardMutation, useDeleteGiftCardMutation } from "@dashboard/graphql"; import { MutationResultWithOpts } from "@dashboard/hooks/makeMutation"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import { useIntl } from "react-intl"; import { giftCardDeleteDialogMessages as messages } from "./messages"; diff --git a/src/hooks/makeMutation.ts b/src/hooks/makeMutation.ts index 4d48e8abae8..bc96acd4438 100644 --- a/src/hooks/makeMutation.ts +++ b/src/hooks/makeMutation.ts @@ -15,7 +15,7 @@ import { GqlErrors, hasError } from "@dashboard/utils/api"; import { DocumentNode } from "graphql"; import { useIntl } from "react-intl"; -import useNotifier from "./useNotifier"; +import { useNotifier } from "./useNotifier"; export type MutationResultWithOpts = MutationResult & MutationResultAdditionalProps; diff --git a/src/hooks/makeQuery.ts b/src/hooks/makeQuery.ts index 609d0b53a82..629bf40ee3c 100644 --- a/src/hooks/makeQuery.ts +++ b/src/hooks/makeQuery.ts @@ -17,7 +17,7 @@ import { useEffect } from "react"; import { useIntl } from "react-intl"; import useAppState from "./useAppState"; -import useNotifier from "./useNotifier"; +import { useNotifier } from "./useNotifier"; export { useLazyQuery } from "@apollo/client"; diff --git a/src/hooks/useNotifier/index.ts b/src/hooks/useNotifier/index.ts index 4f87156dc75..a9f065e4a4b 100644 --- a/src/hooks/useNotifier/index.ts +++ b/src/hooks/useNotifier/index.ts @@ -1,2 +1,2 @@ -export { default } from "./useNotifier"; -export * from "./useNotifier"; +export type { UseNotifierResult } from "./useNotifier"; +export { useNotifier } from "./useNotifier"; diff --git a/src/hooks/useNotifier/useNotifier.test.tsx b/src/hooks/useNotifier/useNotifier.test.tsx index 062b23ebfd9..8c73e75c7ec 100644 --- a/src/hooks/useNotifier/useNotifier.test.tsx +++ b/src/hooks/useNotifier/useNotifier.test.tsx @@ -6,7 +6,7 @@ import { PropsWithChildren } from "react"; import { IntlProvider } from "react-intl"; import { toast } from "sonner"; -import useNotifier from "./useNotifier"; +import { useNotifier } from "./useNotifier"; jest.mock("sonner", () => ({ toast: { diff --git a/src/hooks/useNotifier/useNotifier.tsx b/src/hooks/useNotifier/useNotifier.tsx index cc8523aa8c6..19b0487ea9d 100644 --- a/src/hooks/useNotifier/useNotifier.tsx +++ b/src/hooks/useNotifier/useNotifier.tsx @@ -67,4 +67,4 @@ function useNotifier(): UseNotifierResult { return notify; } -export default useNotifier; +export { useNotifier }; diff --git a/src/hooks/useOnSetDefaultVariant.ts b/src/hooks/useOnSetDefaultVariant.ts index a6752dfa59b..22575db8d43 100644 --- a/src/hooks/useOnSetDefaultVariant.ts +++ b/src/hooks/useOnSetDefaultVariant.ts @@ -1,5 +1,5 @@ import { Node, useProductVariantSetDefaultMutation } from "@dashboard/graphql"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import { getProductErrorMessage } from "@dashboard/utils/errors"; import { useIntl } from "react-intl"; diff --git a/src/modelTypes/views/PageTypeCreate.tsx b/src/modelTypes/views/PageTypeCreate.tsx index 55a358bd4b7..660b80e8bfa 100644 --- a/src/modelTypes/views/PageTypeCreate.tsx +++ b/src/modelTypes/views/PageTypeCreate.tsx @@ -6,7 +6,7 @@ import { useUpdatePrivateMetadataMutation, } from "@dashboard/graphql"; import useNavigator from "@dashboard/hooks/useNavigator"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import { getMutationErrors } from "@dashboard/misc"; import createMetadataCreateHandler from "@dashboard/utils/handlers/metadataCreateHandler"; import { useIntl } from "react-intl"; diff --git a/src/modelTypes/views/PageTypeDetails.tsx b/src/modelTypes/views/PageTypeDetails.tsx index 01d654c39c8..a2b65052c12 100644 --- a/src/modelTypes/views/PageTypeDetails.tsx +++ b/src/modelTypes/views/PageTypeDetails.tsx @@ -20,7 +20,7 @@ import { import useBulkActions from "@dashboard/hooks/useBulkActions"; import { useListSelectedItems } from "@dashboard/hooks/useListSelectedItems"; import useNavigator from "@dashboard/hooks/useNavigator"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import { commonMessages } from "@dashboard/intl"; import { getStringOrPlaceholder } from "@dashboard/misc"; import { ReorderEvent } from "@dashboard/types"; diff --git a/src/modelTypes/views/PageTypeList/PageTypeList.tsx b/src/modelTypes/views/PageTypeList/PageTypeList.tsx index fc94c7f925c..c85b793afb7 100644 --- a/src/modelTypes/views/PageTypeList/PageTypeList.tsx +++ b/src/modelTypes/views/PageTypeList/PageTypeList.tsx @@ -8,7 +8,7 @@ import useBulkActions from "@dashboard/hooks/useBulkActions"; import { useFilterPresets } from "@dashboard/hooks/useFilterPresets"; import useListSettings from "@dashboard/hooks/useListSettings"; import useNavigator from "@dashboard/hooks/useNavigator"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import { usePaginationReset } from "@dashboard/hooks/usePaginationReset"; import usePaginator, { createPaginationState, diff --git a/src/modeling/views/PageCreate.tsx b/src/modeling/views/PageCreate.tsx index 5fc64ac05eb..32c23bb517a 100644 --- a/src/modeling/views/PageCreate.tsx +++ b/src/modeling/views/PageCreate.tsx @@ -17,7 +17,7 @@ import { } from "@dashboard/graphql"; import { getSearchFetchMoreProps } from "@dashboard/hooks/makeTopLevelSearch/utils"; import useNavigator from "@dashboard/hooks/useNavigator"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import { getMutationErrors } from "@dashboard/misc"; import useCategorySearch from "@dashboard/searches/useCategorySearch"; import useCollectionSearch from "@dashboard/searches/useCollectionSearch"; diff --git a/src/modeling/views/PageDetails.tsx b/src/modeling/views/PageDetails.tsx index 8bb58b1eb92..8da6d15a340 100644 --- a/src/modeling/views/PageDetails.tsx +++ b/src/modeling/views/PageDetails.tsx @@ -29,7 +29,7 @@ import { useUpdatePrivateMetadataMutation, } from "@dashboard/graphql"; import useNavigator from "@dashboard/hooks/useNavigator"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import { commonMessages } from "@dashboard/intl"; import useCategorySearch from "@dashboard/searches/useCategorySearch"; import useCollectionSearch from "@dashboard/searches/useCollectionSearch"; diff --git a/src/modeling/views/PageList/PageList.tsx b/src/modeling/views/PageList/PageList.tsx index f990a711b22..6e1301635d2 100644 --- a/src/modeling/views/PageList/PageList.tsx +++ b/src/modeling/views/PageList/PageList.tsx @@ -14,7 +14,7 @@ import { getSearchFetchMoreProps } from "@dashboard/hooks/makeTopLevelSearch/uti import { useFilterPresets } from "@dashboard/hooks/useFilterPresets"; import useListSettings from "@dashboard/hooks/useListSettings"; import useNavigator from "@dashboard/hooks/useNavigator"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import { usePaginationReset } from "@dashboard/hooks/usePaginationReset"; import usePaginator, { createPaginationState, diff --git a/src/orders/components/OrderLineMetadataDialog/useHandleSubmit.ts b/src/orders/components/OrderLineMetadataDialog/useHandleSubmit.ts index 66e61a0cb10..276d4509d91 100644 --- a/src/orders/components/OrderLineMetadataDialog/useHandleSubmit.ts +++ b/src/orders/components/OrderLineMetadataDialog/useHandleSubmit.ts @@ -4,7 +4,7 @@ import { useUpdateMetadataMutation, useUpdatePrivateMetadataMutation, } from "@dashboard/graphql"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import { commonMessages } from "@dashboard/intl"; import createMetadataUpdateHandler from "@dashboard/utils/handlers/metadataUpdateHandler"; import { useMemo, useRef, useState } from "react"; diff --git a/src/orders/components/OrderManualTransactionRefundPage/OrderManualTransactionRefundPage.test.tsx b/src/orders/components/OrderManualTransactionRefundPage/OrderManualTransactionRefundPage.test.tsx index 3393c509072..0839e2f2bfa 100644 --- a/src/orders/components/OrderManualTransactionRefundPage/OrderManualTransactionRefundPage.test.tsx +++ b/src/orders/components/OrderManualTransactionRefundPage/OrderManualTransactionRefundPage.test.tsx @@ -6,7 +6,7 @@ import { TransactionActionEnum, TransactionItemFragment, } from "@dashboard/graphql"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import { ThemeProvider as LegacyThemeProvider } from "@saleor/macaw-ui"; import { ThemeProvider } from "@saleor/macaw-ui-next"; import { render, screen } from "@testing-library/react"; diff --git a/src/orders/components/OrderManualTransactionRefundPage/components/OrderManualTransactionRefundForm/OrderManualTransactionRefundForm.tsx b/src/orders/components/OrderManualTransactionRefundPage/components/OrderManualTransactionRefundForm/OrderManualTransactionRefundForm.tsx index e97b5e72533..fc0c565b2c6 100644 --- a/src/orders/components/OrderManualTransactionRefundPage/components/OrderManualTransactionRefundForm/OrderManualTransactionRefundForm.tsx +++ b/src/orders/components/OrderManualTransactionRefundPage/components/OrderManualTransactionRefundForm/OrderManualTransactionRefundForm.tsx @@ -6,7 +6,7 @@ import { useOrderTransactionRequestActionMutation, } from "@dashboard/graphql"; import useNavigator from "@dashboard/hooks/useNavigator"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import { refundSavebarMessages } from "@dashboard/orders/components/OrderTransactionRefundPage/messages"; import { orderUrl } from "@dashboard/orders/urls"; import { diff --git a/src/orders/views/OrderDetails/OrderDetails.tsx b/src/orders/views/OrderDetails/OrderDetails.tsx index cdfb1b2f3e5..9a84b31d09f 100644 --- a/src/orders/views/OrderDetails/OrderDetails.tsx +++ b/src/orders/views/OrderDetails/OrderDetails.tsx @@ -13,7 +13,7 @@ import { } from "@dashboard/graphql"; import useBackgroundTask from "@dashboard/hooks/useBackgroundTask"; import useNavigator from "@dashboard/hooks/useNavigator"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import { commonMessages } from "@dashboard/intl"; import { createOrderMetadataIdSchema } from "@dashboard/orders/components/OrderDetailsPage/utils"; import getOrderErrorMessage from "@dashboard/utils/errors/order"; diff --git a/src/orders/views/OrderDetails/OrderDetailsMessages.tsx b/src/orders/views/OrderDetails/OrderDetailsMessages.tsx index 0df2de6e706..159e2da6265 100644 --- a/src/orders/views/OrderDetails/OrderDetailsMessages.tsx +++ b/src/orders/views/OrderDetails/OrderDetailsMessages.tsx @@ -28,7 +28,7 @@ import { } from "@dashboard/graphql"; import useLocale from "@dashboard/hooks/useLocale"; import useNavigator from "@dashboard/hooks/useNavigator"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import getOrderErrorMessage from "@dashboard/utils/errors/order"; import { getOrderTransactionErrorMessage, diff --git a/src/orders/views/OrderDraftList/OrderDraftList.tsx b/src/orders/views/OrderDraftList/OrderDraftList.tsx index b7d8e5e9eef..acf9e43010a 100644 --- a/src/orders/views/OrderDraftList/OrderDraftList.tsx +++ b/src/orders/views/OrderDraftList/OrderDraftList.tsx @@ -12,7 +12,7 @@ import { useOrderDraftCreateMutation, useOrderDraftListQuery } from "@dashboard/ import { useFilterPresets } from "@dashboard/hooks/useFilterPresets"; import useListSettings from "@dashboard/hooks/useListSettings"; import useNavigator from "@dashboard/hooks/useNavigator"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import { usePaginationReset } from "@dashboard/hooks/usePaginationReset"; import usePaginator, { createPaginationState, diff --git a/src/orders/views/OrderDraftList/useBulkDeletion.ts b/src/orders/views/OrderDraftList/useBulkDeletion.ts index d54dcac42bb..7dcc0321f8b 100644 --- a/src/orders/views/OrderDraftList/useBulkDeletion.ts +++ b/src/orders/views/OrderDraftList/useBulkDeletion.ts @@ -1,5 +1,5 @@ import { useOrderDraftBulkCancelMutation } from "@dashboard/graphql"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import { useIntl } from "react-intl"; export const useBulkDeletion = (onComplete: () => void) => { diff --git a/src/orders/views/OrderEditGrantRefund/OrderEditGrantRefund.tsx b/src/orders/views/OrderEditGrantRefund/OrderEditGrantRefund.tsx index db72ee6dadd..80a4a96ae79 100644 --- a/src/orders/views/OrderEditGrantRefund/OrderEditGrantRefund.tsx +++ b/src/orders/views/OrderEditGrantRefund/OrderEditGrantRefund.tsx @@ -6,7 +6,7 @@ import { useOrderGrantRefundEditMutation, } from "@dashboard/graphql"; import useNavigator from "@dashboard/hooks/useNavigator"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import { extractMutationErrors } from "@dashboard/misc"; import OrderGrantRefundPage from "@dashboard/orders/components/OrderGrantRefundPage"; import { OrderGrantRefundFormData } from "@dashboard/orders/components/OrderGrantRefundPage/form"; diff --git a/src/orders/views/OrderFulfill/OrderFulfill.tsx b/src/orders/views/OrderFulfill/OrderFulfill.tsx index 58bee106c40..6bfaf5331fd 100644 --- a/src/orders/views/OrderFulfill/OrderFulfill.tsx +++ b/src/orders/views/OrderFulfill/OrderFulfill.tsx @@ -7,7 +7,7 @@ import { useOrderFulfillSettingsQuery, } from "@dashboard/graphql"; import useNavigator from "@dashboard/hooks/useNavigator"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import { getMutationErrors } from "@dashboard/misc"; import OrderFulfillPage, { OrderFulfillSubmitData, diff --git a/src/orders/views/OrderGrantRefund/OrderGrantRefund.tsx b/src/orders/views/OrderGrantRefund/OrderGrantRefund.tsx index e4c63e6e470..91cb093398f 100644 --- a/src/orders/views/OrderGrantRefund/OrderGrantRefund.tsx +++ b/src/orders/views/OrderGrantRefund/OrderGrantRefund.tsx @@ -4,7 +4,7 @@ import { useOrderGrantRefundAddMutation, } from "@dashboard/graphql"; import useNavigator from "@dashboard/hooks/useNavigator"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import { extractMutationErrors } from "@dashboard/misc"; import OrderGrantRefundPage from "@dashboard/orders/components/OrderGrantRefundPage"; import { OrderGrantRefundFormData } from "@dashboard/orders/components/OrderGrantRefundPage/form"; diff --git a/src/orders/views/OrderList/OrderList.tsx b/src/orders/views/OrderList/OrderList.tsx index 181ba2f7172..ad26d44b0f5 100644 --- a/src/orders/views/OrderList/OrderList.tsx +++ b/src/orders/views/OrderList/OrderList.tsx @@ -12,7 +12,7 @@ import { useFilterHandlers } from "@dashboard/hooks/useFilterHandlers"; import { useFilterPresets } from "@dashboard/hooks/useFilterPresets"; import useListSettings from "@dashboard/hooks/useListSettings"; import useNavigator from "@dashboard/hooks/useNavigator"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import { usePaginationReset } from "@dashboard/hooks/usePaginationReset"; import usePaginator, { createPaginationState, diff --git a/src/orders/views/OrderRefund/OrderRefund.tsx b/src/orders/views/OrderRefund/OrderRefund.tsx index d4be4351d33..4774fec94c7 100644 --- a/src/orders/views/OrderRefund/OrderRefund.tsx +++ b/src/orders/views/OrderRefund/OrderRefund.tsx @@ -5,7 +5,7 @@ import { useOrderRefundMutation, } from "@dashboard/graphql"; import useNavigator from "@dashboard/hooks/useNavigator"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import { extractMutationErrors } from "@dashboard/misc"; import OrderRefundPage from "@dashboard/orders/components/OrderRefundPage"; import { diff --git a/src/orders/views/OrderReturn/OrderReturn.tsx b/src/orders/views/OrderReturn/OrderReturn.tsx index 58cfd2550d1..c94e22dbe3e 100644 --- a/src/orders/views/OrderReturn/OrderReturn.tsx +++ b/src/orders/views/OrderReturn/OrderReturn.tsx @@ -4,7 +4,7 @@ import { useOrderDetailsQuery, } from "@dashboard/graphql"; import useNavigator from "@dashboard/hooks/useNavigator"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import { extractMutationErrors } from "@dashboard/misc"; import OrderReturnPage from "@dashboard/orders/components/OrderReturnPage"; import { OrderReturnFormData } from "@dashboard/orders/components/OrderReturnPage/form"; diff --git a/src/orders/views/OrderSendRefund/OrderSendRefund.tsx b/src/orders/views/OrderSendRefund/OrderSendRefund.tsx index 1825914ebc5..5d40958386d 100644 --- a/src/orders/views/OrderSendRefund/OrderSendRefund.tsx +++ b/src/orders/views/OrderSendRefund/OrderSendRefund.tsx @@ -4,7 +4,7 @@ import { useCreateManualTransactionRefundMutation, useOrderDetailsQuery, } from "@dashboard/graphql"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import OrderSendRefundPage from "@dashboard/orders/components/OrderSendRefundPage"; import { getTransactionCreateErrorMessage } from "@dashboard/utils/errors/transaction"; import { defineMessages, useIntl } from "react-intl"; diff --git a/src/orders/views/OrderSettings.tsx b/src/orders/views/OrderSettings.tsx index f74f604ff7a..2b224ff83a6 100644 --- a/src/orders/views/OrderSettings.tsx +++ b/src/orders/views/OrderSettings.tsx @@ -1,6 +1,6 @@ // @ts-strict-ignore import { useOrderSettingsQuery, useOrderSettingsUpdateMutation } from "@dashboard/graphql"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import { commonMessages } from "@dashboard/intl"; import { extractMutationErrors, getMutationState } from "@dashboard/misc"; import OrderSettingsPage from "@dashboard/orders/components/OrderSettingsPage"; diff --git a/src/orders/views/OrderTransactionRefundCreate/OrderTransactionRefundCreate.tsx b/src/orders/views/OrderTransactionRefundCreate/OrderTransactionRefundCreate.tsx index c95c35c85ad..5ee8d09b9f2 100644 --- a/src/orders/views/OrderTransactionRefundCreate/OrderTransactionRefundCreate.tsx +++ b/src/orders/views/OrderTransactionRefundCreate/OrderTransactionRefundCreate.tsx @@ -4,7 +4,7 @@ import { useRefundSettingsQuery, } from "@dashboard/graphql"; import useNavigator from "@dashboard/hooks/useNavigator"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import OrderTransactionRefundPage, { OrderTransactionRefundError, OrderTransactionRefundPageFormData, diff --git a/src/orders/views/OrderTransactionRefundEdit/OrderTransactionRefundEdit.tsx b/src/orders/views/OrderTransactionRefundEdit/OrderTransactionRefundEdit.tsx index fbfeb164ee0..03b1ed630ea 100644 --- a/src/orders/views/OrderTransactionRefundEdit/OrderTransactionRefundEdit.tsx +++ b/src/orders/views/OrderTransactionRefundEdit/OrderTransactionRefundEdit.tsx @@ -8,7 +8,7 @@ import { useRefundSettingsQuery, } from "@dashboard/graphql"; import useNavigator from "@dashboard/hooks/useNavigator"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import { extractMutationErrors } from "@dashboard/misc"; import OrderTransactionRefundPage, { OrderTransactionRefundError, diff --git a/src/permissionGroups/views/PermissionGroupCreate/PermissionGroupCreate.tsx b/src/permissionGroups/views/PermissionGroupCreate/PermissionGroupCreate.tsx index dad48727a09..43a68b456d0 100644 --- a/src/permissionGroups/views/PermissionGroupCreate/PermissionGroupCreate.tsx +++ b/src/permissionGroups/views/PermissionGroupCreate/PermissionGroupCreate.tsx @@ -3,7 +3,7 @@ import useAppChannel from "@dashboard/components/AppLayout/AppChannelContext"; import { WindowTitle } from "@dashboard/components/WindowTitle"; import { usePermissionGroupCreateMutation } from "@dashboard/graphql"; import useNavigator from "@dashboard/hooks/useNavigator"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import useShop from "@dashboard/hooks/useShop"; import { extractMutationErrors } from "@dashboard/misc"; import { PermissionData } from "@dashboard/permissionGroups/components/PermissionGroupDetailsPage"; diff --git a/src/permissionGroups/views/PermissionGroupDetails/PermissionGroupDetails.tsx b/src/permissionGroups/views/PermissionGroupDetails/PermissionGroupDetails.tsx index ffe5cd886f1..3af709e39a5 100644 --- a/src/permissionGroups/views/PermissionGroupDetails/PermissionGroupDetails.tsx +++ b/src/permissionGroups/views/PermissionGroupDetails/PermissionGroupDetails.tsx @@ -11,7 +11,7 @@ import { } from "@dashboard/graphql"; import useBulkActions from "@dashboard/hooks/useBulkActions"; import useNavigator from "@dashboard/hooks/useNavigator"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import useShop from "@dashboard/hooks/useShop"; import useStateFromProps from "@dashboard/hooks/useStateFromProps"; import { commonMessages } from "@dashboard/intl"; diff --git a/src/productTypes/views/ProductTypeCreate.tsx b/src/productTypes/views/ProductTypeCreate.tsx index 048f57417d9..34bd22703ad 100644 --- a/src/productTypes/views/ProductTypeCreate.tsx +++ b/src/productTypes/views/ProductTypeCreate.tsx @@ -8,7 +8,7 @@ import { useUpdatePrivateMetadataMutation, } from "@dashboard/graphql"; import useNavigator from "@dashboard/hooks/useNavigator"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import { useTaxClassFetchMore } from "@dashboard/taxes/utils/useTaxClassFetchMore"; import createMetadataCreateHandler from "@dashboard/utils/handlers/metadataCreateHandler"; import { useIntl } from "react-intl"; diff --git a/src/productTypes/views/ProductTypeList/ProductTypeList.tsx b/src/productTypes/views/ProductTypeList/ProductTypeList.tsx index b3d7fc77f0a..ebcd33854b0 100644 --- a/src/productTypes/views/ProductTypeList/ProductTypeList.tsx +++ b/src/productTypes/views/ProductTypeList/ProductTypeList.tsx @@ -8,7 +8,7 @@ import useBulkActions from "@dashboard/hooks/useBulkActions"; import { useFilterPresets } from "@dashboard/hooks/useFilterPresets"; import useListSettings from "@dashboard/hooks/useListSettings"; import useNavigator from "@dashboard/hooks/useNavigator"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import { usePaginationReset } from "@dashboard/hooks/usePaginationReset"; import usePaginator, { createPaginationState, diff --git a/src/productTypes/views/ProductTypeUpdate/index.tsx b/src/productTypes/views/ProductTypeUpdate/index.tsx index 058efae97d1..c48eddfdc82 100644 --- a/src/productTypes/views/ProductTypeUpdate/index.tsx +++ b/src/productTypes/views/ProductTypeUpdate/index.tsx @@ -22,7 +22,7 @@ import { import useBulkActions from "@dashboard/hooks/useBulkActions"; import { useListSelectedItems } from "@dashboard/hooks/useListSelectedItems"; import useNavigator from "@dashboard/hooks/useNavigator"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import { commonMessages } from "@dashboard/intl"; import { getStringOrPlaceholder, maybe } from "@dashboard/misc"; import useProductTypeDelete from "@dashboard/productTypes/hooks/useProductTypeDelete"; diff --git a/src/products/components/OrderDiscountProviders/OrderDiscountProvider.tsx b/src/products/components/OrderDiscountProviders/OrderDiscountProvider.tsx index b2e3c8ea5e9..fc12f2dc077 100644 --- a/src/products/components/OrderDiscountProviders/OrderDiscountProvider.tsx +++ b/src/products/components/OrderDiscountProviders/OrderDiscountProvider.tsx @@ -7,7 +7,7 @@ import { useOrderDiscountDeleteMutation, useOrderDiscountUpdateMutation, } from "@dashboard/graphql"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import { getDefaultNotifierSuccessErrorData } from "@dashboard/hooks/useNotifier/utils"; import { OrderDiscountCommonInput } from "@dashboard/orders/components/OrderDiscountCommonModal/types"; import { createContext } from "react"; diff --git a/src/products/components/OrderDiscountProviders/OrderLineDiscountProvider.tsx b/src/products/components/OrderDiscountProviders/OrderLineDiscountProvider.tsx index c6c6b1ee972..c3262364ff7 100644 --- a/src/products/components/OrderDiscountProviders/OrderLineDiscountProvider.tsx +++ b/src/products/components/OrderDiscountProviders/OrderLineDiscountProvider.tsx @@ -6,7 +6,7 @@ import { useOrderLineDiscountRemoveMutation, useOrderLineDiscountUpdateMutation, } from "@dashboard/graphql"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import { getDefaultNotifierSuccessErrorData } from "@dashboard/hooks/useNotifier/utils"; import { getById } from "@dashboard/misc"; import { OrderDiscountCommonInput } from "@dashboard/orders/components/OrderDiscountCommonModal/types"; diff --git a/src/products/views/ProductCreate/ProductCreate.tsx b/src/products/views/ProductCreate/ProductCreate.tsx index 21861acf22f..4f2a944defb 100644 --- a/src/products/views/ProductCreate/ProductCreate.tsx +++ b/src/products/views/ProductCreate/ProductCreate.tsx @@ -21,7 +21,7 @@ import { import { getSearchFetchMoreProps } from "@dashboard/hooks/makeTopLevelSearch/utils"; import useChannels from "@dashboard/hooks/useChannels"; import useNavigator from "@dashboard/hooks/useNavigator"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import useShop from "@dashboard/hooks/useShop"; import { getMutationErrors } from "@dashboard/misc"; import ProductCreatePage, { diff --git a/src/products/views/ProductImage.tsx b/src/products/views/ProductImage.tsx index 53ab18418f8..4666e315839 100644 --- a/src/products/views/ProductImage.tsx +++ b/src/products/views/ProductImage.tsx @@ -7,7 +7,7 @@ import { useProductMediaUpdateMutation, } from "@dashboard/graphql"; import useNavigator from "@dashboard/hooks/useNavigator"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import { commonMessages } from "@dashboard/intl"; import { FormattedMessage, useIntl } from "react-intl"; diff --git a/src/products/views/ProductList/ProductList.tsx b/src/products/views/ProductList/ProductList.tsx index d70a908e1b7..5e3639d8f33 100644 --- a/src/products/views/ProductList/ProductList.tsx +++ b/src/products/views/ProductList/ProductList.tsx @@ -28,7 +28,7 @@ import { useFilterHandlers } from "@dashboard/hooks/useFilterHandlers"; import { useFilterPresets } from "@dashboard/hooks/useFilterPresets"; import useListSettings from "@dashboard/hooks/useListSettings"; import useNavigator from "@dashboard/hooks/useNavigator"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import { usePaginationReset } from "@dashboard/hooks/usePaginationReset"; import usePaginator, { createPaginationState, diff --git a/src/products/views/ProductUpdate/ProductUpdate.tsx b/src/products/views/ProductUpdate/ProductUpdate.tsx index 68c72234032..4e8e75af1b8 100644 --- a/src/products/views/ProductUpdate/ProductUpdate.tsx +++ b/src/products/views/ProductUpdate/ProductUpdate.tsx @@ -18,7 +18,7 @@ import { } from "@dashboard/graphql"; import { getSearchFetchMoreProps } from "@dashboard/hooks/makeTopLevelSearch/utils"; import useNavigator from "@dashboard/hooks/useNavigator"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import { commonMessages, errorMessages } from "@dashboard/intl"; import { useSearchAttributeValuesSuggestions } from "@dashboard/searches/useAttributeValueSearch"; import useCategorySearch from "@dashboard/searches/useCategorySearch"; diff --git a/src/products/views/ProductUpdate/handlers/useProductUpdateHandler.ts b/src/products/views/ProductUpdate/handlers/useProductUpdateHandler.ts index 84d11db0c4d..cb38d9bbe26 100644 --- a/src/products/views/ProductUpdate/handlers/useProductUpdateHandler.ts +++ b/src/products/views/ProductUpdate/handlers/useProductUpdateHandler.ts @@ -26,7 +26,7 @@ import { useUpdateMetadataMutation, useUpdatePrivateMetadataMutation, } from "@dashboard/graphql"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import { commonMessages } from "@dashboard/intl"; import { getMutationErrors } from "@dashboard/misc"; import { ProductUpdateSubmitData } from "@dashboard/products/components/ProductUpdatePage/types"; diff --git a/src/products/views/ProductVariant/ProductVariant.tsx b/src/products/views/ProductVariant/ProductVariant.tsx index 1bdecc88449..31123431335 100644 --- a/src/products/views/ProductVariant/ProductVariant.tsx +++ b/src/products/views/ProductVariant/ProductVariant.tsx @@ -30,7 +30,7 @@ import { useVariantUpdateMutation, } from "@dashboard/graphql"; import useNavigator from "@dashboard/hooks/useNavigator"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import useOnSetDefaultVariant from "@dashboard/hooks/useOnSetDefaultVariant"; import useShop from "@dashboard/hooks/useShop"; import { commonMessages } from "@dashboard/intl"; diff --git a/src/products/views/ProductVariantCreate.tsx b/src/products/views/ProductVariantCreate.tsx index 896be9b4a01..5d73be19a97 100644 --- a/src/products/views/ProductVariantCreate.tsx +++ b/src/products/views/ProductVariantCreate.tsx @@ -18,7 +18,7 @@ import { useVariantCreateMutation, } from "@dashboard/graphql"; import useNavigator from "@dashboard/hooks/useNavigator"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import useShop from "@dashboard/hooks/useShop"; import useCategorySearch from "@dashboard/searches/useCategorySearch"; import useCollectionSearch from "@dashboard/searches/useCollectionSearch"; diff --git a/src/refundsSettings/components/RefundsSettingsPage/RefundsSettingsPage.tsx b/src/refundsSettings/components/RefundsSettingsPage/RefundsSettingsPage.tsx index a779e2247a9..ffd9e364b9d 100644 --- a/src/refundsSettings/components/RefundsSettingsPage/RefundsSettingsPage.tsx +++ b/src/refundsSettings/components/RefundsSettingsPage/RefundsSettingsPage.tsx @@ -12,7 +12,7 @@ import { useRefundSettingsUpdateMutation, } from "@dashboard/graphql"; import useNavigator from "@dashboard/hooks/useNavigator"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import { pageCreateUrl } from "@dashboard/modeling/urls"; import { pageTypeAddUrl, pageTypeUrl } from "@dashboard/modelTypes/urls"; import { refundsSettingsPageMessages } from "@dashboard/refundsSettings/components/RefundsSettingsPage/messages"; diff --git a/src/shipping/handlers.ts b/src/shipping/handlers.ts index 3cb50dd7369..3a54deb1a02 100644 --- a/src/shipping/handlers.ts +++ b/src/shipping/handlers.ts @@ -14,7 +14,7 @@ import { useShippingMethodChannelListingUpdateMutation, } from "@dashboard/graphql"; import useNavigator from "@dashboard/hooks/useNavigator"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import { commonMessages } from "@dashboard/intl"; import { extractMutationErrors, getMutationState } from "@dashboard/misc"; import { getParsedDataForJsonStringField } from "@dashboard/utils/richText/misc"; diff --git a/src/shipping/views/RateUpdate.tsx b/src/shipping/views/RateUpdate.tsx index f48d09a6bfd..2dad2053601 100644 --- a/src/shipping/views/RateUpdate.tsx +++ b/src/shipping/views/RateUpdate.tsx @@ -26,7 +26,7 @@ import useBulkActions from "@dashboard/hooks/useBulkActions"; import useChannels from "@dashboard/hooks/useChannels"; import useLocalPaginator, { useLocalPaginationState } from "@dashboard/hooks/useLocalPaginator"; import useNavigator from "@dashboard/hooks/useNavigator"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import { PaginatorContext } from "@dashboard/hooks/usePaginator"; import { commonMessages, sectionNames } from "@dashboard/intl"; import { getById, getByUnmatchingId } from "@dashboard/misc"; diff --git a/src/shipping/views/ShippingZoneCreate.tsx b/src/shipping/views/ShippingZoneCreate.tsx index c2e9f88c945..67fbcbe8674 100644 --- a/src/shipping/views/ShippingZoneCreate.tsx +++ b/src/shipping/views/ShippingZoneCreate.tsx @@ -1,7 +1,7 @@ // @ts-strict-ignore import { useCreateShippingZoneMutation, useShopCountriesQuery } from "@dashboard/graphql"; import useNavigator from "@dashboard/hooks/useNavigator"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import useShop from "@dashboard/hooks/useShop"; import { commonMessages } from "@dashboard/intl"; import { extractMutationErrors } from "@dashboard/misc"; diff --git a/src/shipping/views/ShippingZoneDetails/index.tsx b/src/shipping/views/ShippingZoneDetails/index.tsx index 66150a449c2..96e3efcc776 100644 --- a/src/shipping/views/ShippingZoneDetails/index.tsx +++ b/src/shipping/views/ShippingZoneDetails/index.tsx @@ -18,7 +18,7 @@ import { } from "@dashboard/graphql"; import { useLocalPaginationState } from "@dashboard/hooks/useLocalPaginator"; import useNavigator from "@dashboard/hooks/useNavigator"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import useShop from "@dashboard/hooks/useShop"; import { commonMessages } from "@dashboard/intl"; import { diff --git a/src/shipping/views/ShippingZonesList.tsx b/src/shipping/views/ShippingZonesList.tsx index ce492f0a8ef..767c3060f2b 100644 --- a/src/shipping/views/ShippingZonesList.tsx +++ b/src/shipping/views/ShippingZonesList.tsx @@ -7,7 +7,7 @@ import { } from "@dashboard/graphql"; import useListSettings from "@dashboard/hooks/useListSettings"; import useNavigator from "@dashboard/hooks/useNavigator"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import { usePaginationReset } from "@dashboard/hooks/usePaginationReset"; import usePaginator, { createPaginationState, diff --git a/src/siteSettings/views/index.tsx b/src/siteSettings/views/index.tsx index 3439259556c..f8cc103c758 100644 --- a/src/siteSettings/views/index.tsx +++ b/src/siteSettings/views/index.tsx @@ -4,7 +4,7 @@ import { useShopSettingsUpdateMutation, useSiteSettingsQuery, } from "@dashboard/graphql"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import { commonMessages, sectionNames } from "@dashboard/intl"; import { useIntl } from "react-intl"; diff --git a/src/staff/components/StaffPasswordResetDialog/StaffPasswordResetDialog.tsx b/src/staff/components/StaffPasswordResetDialog/StaffPasswordResetDialog.tsx index 6f55585bd68..8299a1360e9 100644 --- a/src/staff/components/StaffPasswordResetDialog/StaffPasswordResetDialog.tsx +++ b/src/staff/components/StaffPasswordResetDialog/StaffPasswordResetDialog.tsx @@ -5,7 +5,7 @@ import BackButton from "@dashboard/components/BackButton"; import { ConfirmButton } from "@dashboard/components/ConfirmButton"; import { DashboardModal } from "@dashboard/components/Modal"; import { useRequestPasswordResetMutation } from "@dashboard/graphql"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import { DialogProps } from "@dashboard/types"; import { Box, Input, Paragraph } from "@saleor/macaw-ui-next"; import React from "react"; diff --git a/src/staff/hooks/useProfileOperations.ts b/src/staff/hooks/useProfileOperations.ts index c679f32688c..9df6d82e809 100644 --- a/src/staff/hooks/useProfileOperations.ts +++ b/src/staff/hooks/useProfileOperations.ts @@ -5,7 +5,7 @@ import { useUserAvatarUpdateMutation, } from "@dashboard/graphql"; import useNavigator from "@dashboard/hooks/useNavigator"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import { commonMessages, errorMessages } from "@dashboard/intl"; import { useIntl } from "react-intl"; diff --git a/src/staff/hooks/useStaffUserOperations.ts b/src/staff/hooks/useStaffUserOperations.ts index eb97db3dbe9..285ac9317f1 100644 --- a/src/staff/hooks/useStaffUserOperations.ts +++ b/src/staff/hooks/useStaffUserOperations.ts @@ -1,6 +1,6 @@ import { useStaffMemberDeleteMutation, useStaffMemberUpdateMutation } from "@dashboard/graphql"; import useNavigator from "@dashboard/hooks/useNavigator"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import { commonMessages } from "@dashboard/intl"; import { useIntl } from "react-intl"; diff --git a/src/staff/views/StaffList/StaffList.tsx b/src/staff/views/StaffList/StaffList.tsx index 7092883a070..ee0d63e1a3c 100644 --- a/src/staff/views/StaffList/StaffList.tsx +++ b/src/staff/views/StaffList/StaffList.tsx @@ -9,7 +9,7 @@ import { useStaffListQuery, useStaffMemberAddMutation } from "@dashboard/graphql import { useFilterPresets } from "@dashboard/hooks/useFilterPresets"; import useListSettings from "@dashboard/hooks/useListSettings"; import useNavigator from "@dashboard/hooks/useNavigator"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import { usePaginationReset } from "@dashboard/hooks/usePaginationReset"; import usePaginator, { createPaginationState, diff --git a/src/structures/views/MenuDetails/index.tsx b/src/structures/views/MenuDetails/index.tsx index d03f1131df1..9e45db192bf 100644 --- a/src/structures/views/MenuDetails/index.tsx +++ b/src/structures/views/MenuDetails/index.tsx @@ -11,7 +11,7 @@ import { useMenuUpdateMutation, } from "@dashboard/graphql"; import useNavigator from "@dashboard/hooks/useNavigator"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import { pageUrl } from "@dashboard/modeling/urls"; import { languageEntityUrl, TranslatableEntities } from "@dashboard/translations/urls"; import { useCachedLocales } from "@dashboard/translations/useCachedLocales"; diff --git a/src/structures/views/MenuList/MenuList.tsx b/src/structures/views/MenuList/MenuList.tsx index 03bf534afe5..9a7cd6be118 100644 --- a/src/structures/views/MenuList/MenuList.tsx +++ b/src/structures/views/MenuList/MenuList.tsx @@ -10,7 +10,7 @@ import { import useBulkActions from "@dashboard/hooks/useBulkActions"; import useListSettings from "@dashboard/hooks/useListSettings"; import useNavigator from "@dashboard/hooks/useNavigator"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import { usePaginationReset } from "@dashboard/hooks/usePaginationReset"; import usePaginator, { createPaginationState, diff --git a/src/taxes/views/TaxChannelsList.tsx b/src/taxes/views/TaxChannelsList.tsx index 08fac27743e..e073fa87117 100644 --- a/src/taxes/views/TaxChannelsList.tsx +++ b/src/taxes/views/TaxChannelsList.tsx @@ -4,7 +4,7 @@ import { useTaxConfigurationUpdateMutation, } from "@dashboard/graphql"; import useNavigator from "@dashboard/hooks/useNavigator"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import useShop from "@dashboard/hooks/useShop"; import { commonMessages } from "@dashboard/intl"; import createDialogActionHandlers from "@dashboard/utils/handlers/dialogActionHandlers"; diff --git a/src/taxes/views/TaxClassesList.tsx b/src/taxes/views/TaxClassesList.tsx index b3052b85b77..47d6c5a65dd 100644 --- a/src/taxes/views/TaxClassesList.tsx +++ b/src/taxes/views/TaxClassesList.tsx @@ -11,7 +11,7 @@ import { useUpdatePrivateMetadataMutation, } from "@dashboard/graphql"; import useNavigator from "@dashboard/hooks/useNavigator"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import { commonMessages } from "@dashboard/intl"; import createMetadataCreateHandler, { CreateMetadataHandlerFunctionResult, diff --git a/src/taxes/views/TaxCountriesList.tsx b/src/taxes/views/TaxCountriesList.tsx index 573f8bbc3a6..a06f1fa5ee8 100644 --- a/src/taxes/views/TaxCountriesList.tsx +++ b/src/taxes/views/TaxCountriesList.tsx @@ -8,7 +8,7 @@ import { useTaxCountryConfigurationUpdateMutation, } from "@dashboard/graphql"; import useNavigator from "@dashboard/hooks/useNavigator"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import useShop from "@dashboard/hooks/useShop"; import { commonMessages } from "@dashboard/intl"; import createDialogActionHandlers from "@dashboard/utils/handlers/dialogActionHandlers"; diff --git a/src/translations/views/TranslationsAttributes.tsx b/src/translations/views/TranslationsAttributes.tsx index 7d6886ed93a..0ff4cef8520 100644 --- a/src/translations/views/TranslationsAttributes.tsx +++ b/src/translations/views/TranslationsAttributes.tsx @@ -8,7 +8,7 @@ import { import useListSettings from "@dashboard/hooks/useListSettings"; import useLocalPaginator, { useLocalPaginationState } from "@dashboard/hooks/useLocalPaginator"; import useNavigator from "@dashboard/hooks/useNavigator"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import { PaginatorContext } from "@dashboard/hooks/usePaginator"; import useShop from "@dashboard/hooks/useShop"; import { commonMessages } from "@dashboard/intl"; diff --git a/src/translations/views/TranslationsCategories.tsx b/src/translations/views/TranslationsCategories.tsx index 8f3e87f5945..5eb962c58a9 100644 --- a/src/translations/views/TranslationsCategories.tsx +++ b/src/translations/views/TranslationsCategories.tsx @@ -5,7 +5,7 @@ import { useUpdateCategoryTranslationsMutation, } from "@dashboard/graphql"; import useNavigator from "@dashboard/hooks/useNavigator"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import useShop from "@dashboard/hooks/useShop"; import { commonMessages } from "@dashboard/intl"; import { extractMutationErrors } from "@dashboard/misc"; diff --git a/src/translations/views/TranslationsCollections.tsx b/src/translations/views/TranslationsCollections.tsx index a09b37f0153..acfce5f8052 100644 --- a/src/translations/views/TranslationsCollections.tsx +++ b/src/translations/views/TranslationsCollections.tsx @@ -5,7 +5,7 @@ import { useUpdateCollectionTranslationsMutation, } from "@dashboard/graphql"; import useNavigator from "@dashboard/hooks/useNavigator"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import useShop from "@dashboard/hooks/useShop"; import { commonMessages } from "@dashboard/intl"; import { stringifyQs } from "@dashboard/utils/urls"; diff --git a/src/translations/views/TranslationsMenuItem.tsx b/src/translations/views/TranslationsMenuItem.tsx index 54d563ca815..b694fd4479d 100644 --- a/src/translations/views/TranslationsMenuItem.tsx +++ b/src/translations/views/TranslationsMenuItem.tsx @@ -5,7 +5,7 @@ import { useUpdateMenuItemTranslationsMutation, } from "@dashboard/graphql"; import useNavigator from "@dashboard/hooks/useNavigator"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import useShop from "@dashboard/hooks/useShop"; import { commonMessages } from "@dashboard/intl"; import { extractMutationErrors } from "@dashboard/misc"; diff --git a/src/translations/views/TranslationsPages.tsx b/src/translations/views/TranslationsPages.tsx index c985db236c9..8ca0d9e1abb 100644 --- a/src/translations/views/TranslationsPages.tsx +++ b/src/translations/views/TranslationsPages.tsx @@ -6,7 +6,7 @@ import { useUpdatePageTranslationsMutation, } from "@dashboard/graphql"; import useNavigator from "@dashboard/hooks/useNavigator"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import useShop from "@dashboard/hooks/useShop"; import { commonMessages } from "@dashboard/intl"; import { extractMutationErrors } from "@dashboard/misc"; diff --git a/src/translations/views/TranslationsProductVariants.tsx b/src/translations/views/TranslationsProductVariants.tsx index ca26db933ad..db739134c25 100644 --- a/src/translations/views/TranslationsProductVariants.tsx +++ b/src/translations/views/TranslationsProductVariants.tsx @@ -6,7 +6,7 @@ import { useUpdateProductVariantTranslationsMutation, } from "@dashboard/graphql"; import useNavigator from "@dashboard/hooks/useNavigator"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import useShop from "@dashboard/hooks/useShop"; import { commonMessages } from "@dashboard/intl"; import { OutputData } from "@editorjs/editorjs"; diff --git a/src/translations/views/TranslationsProducts.tsx b/src/translations/views/TranslationsProducts.tsx index f0243fb77cc..27a70e08679 100644 --- a/src/translations/views/TranslationsProducts.tsx +++ b/src/translations/views/TranslationsProducts.tsx @@ -6,7 +6,7 @@ import { useUpdateProductTranslationsMutation, } from "@dashboard/graphql"; import useNavigator from "@dashboard/hooks/useNavigator"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import useShop from "@dashboard/hooks/useShop"; import { commonMessages } from "@dashboard/intl"; import { getMultipleUrlValues, stringifyQs } from "@dashboard/utils/urls"; diff --git a/src/translations/views/TranslationsSales.tsx b/src/translations/views/TranslationsSales.tsx index 6729b146ff0..cb405323007 100644 --- a/src/translations/views/TranslationsSales.tsx +++ b/src/translations/views/TranslationsSales.tsx @@ -5,7 +5,7 @@ import { useUpdateSaleTranslationsMutation, } from "@dashboard/graphql"; import useNavigator from "@dashboard/hooks/useNavigator"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import useShop from "@dashboard/hooks/useShop"; import { commonMessages } from "@dashboard/intl"; import { extractMutationErrors } from "@dashboard/misc"; diff --git a/src/translations/views/TranslationsShippingMethod.tsx b/src/translations/views/TranslationsShippingMethod.tsx index 4561c7fc0e1..ed2f51e8879 100644 --- a/src/translations/views/TranslationsShippingMethod.tsx +++ b/src/translations/views/TranslationsShippingMethod.tsx @@ -5,7 +5,7 @@ import { useUpdateShippingMethodTranslationsMutation, } from "@dashboard/graphql"; import useNavigator from "@dashboard/hooks/useNavigator"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import useShop from "@dashboard/hooks/useShop"; import { commonMessages } from "@dashboard/intl"; import { extractMutationErrors } from "@dashboard/misc"; diff --git a/src/translations/views/TranslationsVouchers.tsx b/src/translations/views/TranslationsVouchers.tsx index 0800f80c1d3..bf3ef17e613 100644 --- a/src/translations/views/TranslationsVouchers.tsx +++ b/src/translations/views/TranslationsVouchers.tsx @@ -5,7 +5,7 @@ import { useVoucherTranslationDetailsQuery, } from "@dashboard/graphql"; import useNavigator from "@dashboard/hooks/useNavigator"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import useShop from "@dashboard/hooks/useShop"; import { commonMessages } from "@dashboard/intl"; import { stringifyQs } from "@dashboard/utils/urls"; diff --git a/src/warehouses/views/WarehouseCreate/WarehouseCreate.tsx b/src/warehouses/views/WarehouseCreate/WarehouseCreate.tsx index 3f5d84d3904..20e838dc961 100644 --- a/src/warehouses/views/WarehouseCreate/WarehouseCreate.tsx +++ b/src/warehouses/views/WarehouseCreate/WarehouseCreate.tsx @@ -1,7 +1,7 @@ import { WindowTitle } from "@dashboard/components/WindowTitle"; import { CountryCode, useWarehouseCreateMutation } from "@dashboard/graphql"; import useNavigator from "@dashboard/hooks/useNavigator"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import useShop from "@dashboard/hooks/useShop"; import { commonMessages } from "@dashboard/intl"; import { extractMutationErrors, findValueInEnum, getMutationStatus } from "@dashboard/misc"; diff --git a/src/warehouses/views/WarehouseDetails/WarehouseDetails.tsx b/src/warehouses/views/WarehouseDetails/WarehouseDetails.tsx index b464d020ac5..b08f1b32c61 100644 --- a/src/warehouses/views/WarehouseDetails/WarehouseDetails.tsx +++ b/src/warehouses/views/WarehouseDetails/WarehouseDetails.tsx @@ -7,7 +7,7 @@ import { useWarehouseUpdateMutation, } from "@dashboard/graphql"; import useNavigator from "@dashboard/hooks/useNavigator"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import useShop from "@dashboard/hooks/useShop"; import { commonMessages } from "@dashboard/intl"; import { diff --git a/src/warehouses/views/WarehouseList/WarehouseList.tsx b/src/warehouses/views/WarehouseList/WarehouseList.tsx index 4c193fc5fc0..5caf83272f7 100644 --- a/src/warehouses/views/WarehouseList/WarehouseList.tsx +++ b/src/warehouses/views/WarehouseList/WarehouseList.tsx @@ -6,7 +6,7 @@ import { useWarehouseDeleteMutation, useWarehouseListQuery } from "@dashboard/gr import { useFilterPresets } from "@dashboard/hooks/useFilterPresets"; import useListSettings from "@dashboard/hooks/useListSettings"; import useNavigator from "@dashboard/hooks/useNavigator"; -import useNotifier from "@dashboard/hooks/useNotifier"; +import { useNotifier } from "@dashboard/hooks/useNotifier"; import { usePaginationReset } from "@dashboard/hooks/usePaginationReset"; import usePaginator, { createPaginationState, From 09fb3051feae4d6c17c2f83e6b4b5d4f599026ad Mon Sep 17 00:00:00 2001 From: Mirek Mencel Date: Tue, 20 Jan 2026 14:52:26 +0100 Subject: [PATCH 82/84] Remove unused `show` method --- src/components/notifications/NotificationProvider.tsx | 1 - src/components/notifications/index.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/src/components/notifications/NotificationProvider.tsx b/src/components/notifications/NotificationProvider.tsx index feac7766ade..424c105246d 100644 --- a/src/components/notifications/NotificationProvider.tsx +++ b/src/components/notifications/NotificationProvider.tsx @@ -6,7 +6,6 @@ import { INotificationContext, NotificationContext } from "."; const NotificationProvider = ({ children }: PropsWithChildren) => { const context = useMemo( () => ({ - show: () => {}, remove: (id: number) => { toast.dismiss(id); }, diff --git a/src/components/notifications/index.ts b/src/components/notifications/index.ts index 1d23b2c70b4..d0454c821a7 100644 --- a/src/components/notifications/index.ts +++ b/src/components/notifications/index.ts @@ -15,7 +15,6 @@ export interface INotification { } export interface INotificationContext { - show: (notification: INotification, timeout?: number | null) => void; remove: (notificationId: number) => void; clearErrorNotifications: () => void; } From b5a1d67d10be3b864e8984f2941e8a96bd0dd378 Mon Sep 17 00:00:00 2001 From: Mirek Mencel Date: Tue, 20 Jan 2026 17:11:44 +0100 Subject: [PATCH 83/84] Fix tests --- .../AttributeSwatchField/useFileProcessing.test.tsx | 4 +++- src/auth/AuthProvider.test.tsx | 3 +-- .../components/CollectionProducts/useProductReorder.test.ts | 4 +++- src/components/Form/useExitFormDialog.test.tsx | 4 +++- .../MetadataDialog/useHandleMetadataSubmit.test.ts | 4 +++- .../components/DiscountRules/DiscountRules.test.tsx | 3 +-- .../hooks/useHandleCreateAppSubmit.test.ts | 4 +++- .../views/InstallCustomExtension/hooks/useInstallApp.test.ts | 4 +++- .../components/AppFrame/appActionsHandler.test.ts | 5 +++-- .../components/AppFrame/useAppActions.test.ts | 4 +++- .../OrderManualTransactionRefundPage.test.tsx | 3 +-- .../StaffPasswordResetDialog.test.tsx | 3 +-- .../WelcomePageSidebar/WelcmePageSidebar.test.tsx | 3 +-- 13 files changed, 29 insertions(+), 19 deletions(-) diff --git a/src/attributes/components/AttributeSwatchField/useFileProcessing.test.tsx b/src/attributes/components/AttributeSwatchField/useFileProcessing.test.tsx index 3b00cf49f15..f12814aca80 100644 --- a/src/attributes/components/AttributeSwatchField/useFileProcessing.test.tsx +++ b/src/attributes/components/AttributeSwatchField/useFileProcessing.test.tsx @@ -14,7 +14,9 @@ jest.mock("@dashboard/intl", () => ({ }, })); -jest.mock("@dashboard/hooks/useNotifier", () => () => jest.fn()); +jest.mock("@dashboard/hooks/useNotifier", () => ({ + useNotifier: () => jest.fn(), +})); describe("useFileProcessing", () => { const mockUploadFile = jest.fn(); diff --git a/src/auth/AuthProvider.test.tsx b/src/auth/AuthProvider.test.tsx index 3c4c5b8c077..e629cba14e9 100644 --- a/src/auth/AuthProvider.test.tsx +++ b/src/auth/AuthProvider.test.tsx @@ -71,8 +71,7 @@ jest.mock("@dashboard/graphql", () => ({ })), })); jest.mock("@dashboard/hooks/useNotifier", () => ({ - __esModule: true, - default: jest.fn(() => () => undefined), + useNotifier: jest.fn(() => () => undefined), })); jest.mock("@dashboard/hooks/useNavigator", () => ({ __esModule: true, diff --git a/src/collections/components/CollectionProducts/useProductReorder.test.ts b/src/collections/components/CollectionProducts/useProductReorder.test.ts index a2a56138cc1..5d77a9730b8 100644 --- a/src/collections/components/CollectionProducts/useProductReorder.test.ts +++ b/src/collections/components/CollectionProducts/useProductReorder.test.ts @@ -23,7 +23,9 @@ jest.mock("./useCollectionId", () => ({ useCollectionId: jest.fn(() => "collection-id-1"), })); -jest.mock("@dashboard/hooks/useNotifier", () => jest.fn()); +jest.mock("@dashboard/hooks/useNotifier", () => ({ + useNotifier: jest.fn(), +})); describe("CollectionProducts/useProductReorder", () => { const mockReorder = jest.fn(); diff --git a/src/components/Form/useExitFormDialog.test.tsx b/src/components/Form/useExitFormDialog.test.tsx index 95680950f0b..72f7709255a 100644 --- a/src/components/Form/useExitFormDialog.test.tsx +++ b/src/components/Form/useExitFormDialog.test.tsx @@ -8,7 +8,9 @@ import { ExitFormDialogContext } from "./ExitFormDialogProvider"; import { useExitFormDialog } from "./useExitFormDialog"; import { useExitFormDialogProvider } from "./useExitFormDialogProvider"; -jest.mock("../../hooks/useNotifier", () => undefined); +jest.mock("../../hooks/useNotifier", () => ({ + useNotifier: () => jest.fn(), +})); const MockExitFormDialogProvider = ({ children }) => { const { providerData } = useExitFormDialogProvider(); diff --git a/src/components/MetadataDialog/useHandleMetadataSubmit.test.ts b/src/components/MetadataDialog/useHandleMetadataSubmit.test.ts index 14649681497..4be1e84b431 100644 --- a/src/components/MetadataDialog/useHandleMetadataSubmit.test.ts +++ b/src/components/MetadataDialog/useHandleMetadataSubmit.test.ts @@ -13,7 +13,9 @@ import { useHandleMetadataSubmit } from "./useHandleMetadataSubmit"; // Mocks const mockNotify = jest.fn(); -jest.mock("@dashboard/hooks/useNotifier", () => () => mockNotify); +jest.mock("@dashboard/hooks/useNotifier", () => ({ + useNotifier: () => mockNotify, +})); jest.mock("@apollo/client", () => ({ ...(jest.requireActual("@apollo/client") as {}), diff --git a/src/discounts/components/DiscountRules/DiscountRules.test.tsx b/src/discounts/components/DiscountRules/DiscountRules.test.tsx index 860bd39b70c..926a5a9e500 100644 --- a/src/discounts/components/DiscountRules/DiscountRules.test.tsx +++ b/src/discounts/components/DiscountRules/DiscountRules.test.tsx @@ -18,8 +18,7 @@ import { DiscountRules } from "./DiscountRules"; import { catalogComplexRules, catalogRules, channels, orderRules } from "./mocksData"; jest.mock("@dashboard/hooks/useNotifier", () => ({ - __esModule: true, - default: jest.fn(() => () => undefined), + useNotifier: jest.fn(() => () => undefined), })); jest.mock("@dashboard/discounts/views/DiscountDetails/context/context", () => ({ __esModule: true, diff --git a/src/extensions/views/AddCustomExtension/hooks/useHandleCreateAppSubmit.test.ts b/src/extensions/views/AddCustomExtension/hooks/useHandleCreateAppSubmit.test.ts index a45449babe9..b87f3b8b7f1 100644 --- a/src/extensions/views/AddCustomExtension/hooks/useHandleCreateAppSubmit.test.ts +++ b/src/extensions/views/AddCustomExtension/hooks/useHandleCreateAppSubmit.test.ts @@ -19,7 +19,9 @@ jest.mock("@dashboard/hooks/useNavigator", () => () => jest.fn()); const mockNotify = jest.fn(); -jest.mock("@dashboard/hooks/useNotifier", () => () => mockNotify); +jest.mock("@dashboard/hooks/useNotifier", () => ({ + useNotifier: () => mockNotify, +})); const mockUseAppCreateMutation = useAppCreateMutation as jest.MockedFunction< typeof useAppCreateMutation diff --git a/src/extensions/views/InstallCustomExtension/hooks/useInstallApp.test.ts b/src/extensions/views/InstallCustomExtension/hooks/useInstallApp.test.ts index f37c698a1ed..f98ffb94b5e 100644 --- a/src/extensions/views/InstallCustomExtension/hooks/useInstallApp.test.ts +++ b/src/extensions/views/InstallCustomExtension/hooks/useInstallApp.test.ts @@ -19,7 +19,9 @@ jest.mock("@dashboard/graphql", () => { jest.mock("@dashboard/hooks/useLocalStorage", () => jest.fn()); jest.mock("@dashboard/hooks/useNavigator", () => jest.fn()); -jest.mock("@dashboard/hooks/useNotifier", () => jest.fn()); +jest.mock("@dashboard/hooks/useNotifier", () => ({ + useNotifier: jest.fn(), +})); jest.mock("@dashboard/misc", () => ({ extractMutationErrors: jest.fn(), })); diff --git a/src/extensions/views/ViewManifestExtension/components/AppFrame/appActionsHandler.test.ts b/src/extensions/views/ViewManifestExtension/components/AppFrame/appActionsHandler.test.ts index 21278b39a11..735fc4f0965 100644 --- a/src/extensions/views/ViewManifestExtension/components/AppFrame/appActionsHandler.test.ts +++ b/src/extensions/views/ViewManifestExtension/components/AppFrame/appActionsHandler.test.ts @@ -1,5 +1,4 @@ import * as dashboardConfig from "@dashboard/config"; -import { UseNotifierResult } from "@dashboard/hooks/useNotifier"; import { renderHook } from "@testing-library/react-hooks"; import * as ReactIntl from "react-intl"; import { IntlShape } from "react-intl"; @@ -20,7 +19,9 @@ jest.mock("../ExternalAppContext/ExternalAppContext"); const mockNotify = jest.fn(); const mockCloseExternalApp = jest.fn(); -jest.mock("@dashboard/hooks/useNotifier", (): UseNotifierResult => () => mockNotify); +jest.mock("@dashboard/hooks/useNotifier", () => ({ + useNotifier: () => mockNotify, +})); jest.spyOn(ExternalAppContext, "useExternalApp").mockImplementation(() => ({ close: mockCloseExternalApp, openApp: jest.fn(), diff --git a/src/extensions/views/ViewManifestExtension/components/AppFrame/useAppActions.test.ts b/src/extensions/views/ViewManifestExtension/components/AppFrame/useAppActions.test.ts index ebb9f3a0b3e..542339819e5 100644 --- a/src/extensions/views/ViewManifestExtension/components/AppFrame/useAppActions.test.ts +++ b/src/extensions/views/ViewManifestExtension/components/AppFrame/useAppActions.test.ts @@ -14,7 +14,9 @@ jest.mock("@sentry/react", () => ({ const mockNotifier = jest.fn(); -jest.mock("@dashboard/hooks/useNotifier", () => (): jest.Mock => mockNotifier); +jest.mock("@dashboard/hooks/useNotifier", () => ({ + useNotifier: (): jest.Mock => mockNotifier, +})); jest.mock("./usePostToExtension"); diff --git a/src/orders/components/OrderManualTransactionRefundPage/OrderManualTransactionRefundPage.test.tsx b/src/orders/components/OrderManualTransactionRefundPage/OrderManualTransactionRefundPage.test.tsx index 0839e2f2bfa..61f0d0f7b7d 100644 --- a/src/orders/components/OrderManualTransactionRefundPage/OrderManualTransactionRefundPage.test.tsx +++ b/src/orders/components/OrderManualTransactionRefundPage/OrderManualTransactionRefundPage.test.tsx @@ -20,8 +20,7 @@ jest.mock("@dashboard/hooks/useNavigator", () => () => jest.fn); jest.mock("@dashboard/components/Savebar"); jest.mock("@dashboard/hooks/useNotifier", () => ({ - __esModule: true, - default: jest.fn(() => () => undefined), + useNotifier: jest.fn(() => () => undefined), })); mockResizeObserver(); diff --git a/src/staff/components/StaffPasswordResetDialog/StaffPasswordResetDialog.test.tsx b/src/staff/components/StaffPasswordResetDialog/StaffPasswordResetDialog.test.tsx index 76fda419959..6638f7d843a 100644 --- a/src/staff/components/StaffPasswordResetDialog/StaffPasswordResetDialog.test.tsx +++ b/src/staff/components/StaffPasswordResetDialog/StaffPasswordResetDialog.test.tsx @@ -29,8 +29,7 @@ jest.mock("@dashboard/graphql", () => ({ })); jest.mock("@dashboard/hooks/useNotifier", () => ({ - __esModule: true, - default: jest.fn(() => jest.fn()), + useNotifier: jest.fn(() => jest.fn()), })); jest.mock("@dashboard/auth/utils", () => ({ diff --git a/src/welcomePage/WelcomePageSidebar/WelcmePageSidebar.test.tsx b/src/welcomePage/WelcomePageSidebar/WelcmePageSidebar.test.tsx index 991c97bab31..f1cc4eb6379 100644 --- a/src/welcomePage/WelcomePageSidebar/WelcmePageSidebar.test.tsx +++ b/src/welcomePage/WelcomePageSidebar/WelcmePageSidebar.test.tsx @@ -32,8 +32,7 @@ jest.mock("./components/WelcomePageStocksAnalytics/useWelcomePageStocksAnalytics })); jest.mock("@dashboard/hooks/useNotifier", () => ({ - __esModule: true, - default: jest.fn(() => () => undefined), + useNotifier: jest.fn(() => () => undefined), })); afterEach(() => { From a9f0e7bbdd3e96e132d494c757428dbfc577bcab Mon Sep 17 00:00:00 2001 From: Mirek Mencel Date: Wed, 21 Jan 2026 15:34:28 +0100 Subject: [PATCH 84/84] Drop changeset leftover after merge --- .changeset/mighty-toes-march.md | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 .changeset/mighty-toes-march.md diff --git a/.changeset/mighty-toes-march.md b/.changeset/mighty-toes-march.md deleted file mode 100644 index 4ddcd95b7c3..00000000000 --- a/.changeset/mighty-toes-march.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"saleor-dashboard": patch ---- - -Introduce new, unified order capture dialog