Order details datagrid (#3325)

* Show available channels

* Add price  and updatedAt columns

* Fix sorting, only sort on selected columns

* Sort by channel

* Allow delete name and product type

* Fix show not product found

* Extract mssages

* Product datagrid custom column picker

* Column picker in data grid in dirty hack for bug

* fix storybook props

* Restore Datagrid defalt column picker with custom render

* Add sort by attributes

* Use datagrid loading cells

* Fix product searching

* Show attributes before last updated

* Readonly all fields in datagrid

* Fix creating new datagrid row

* Remove add new procut button from datagrid

* Show only active sorted column

* Temp fix for column filter

* Fix column mismatch

* Add comments and spred props to ColumnPicker

* Cleanup

* Update avatar size and styles

* On row click with hover on row styles

* Use new theme

* Change placeholder image

* Draw rounded image with border

* Readonly product datagrid

* Use new theme colors in datagrid

* Add vertical borders control to datagrid

* Add empty column to add padding

* Add coursor to datagrid

* Restore vertical borders, fix cursor pointer

* Add custom freezed column

* Initial tooltip for column

* Move tooltip to datagrid

* Adjust datagrid colors style, add possibility to select column

* Change datagrid selected cells colors

* Fix typo and extract messages

* Base order  datagrid

* Cleanup Datagrid component

* Cleanup and code refactor

* Remove cursor pointer props from readonlyCell

* Use money cell for total column

* Add custom cell renderers and fix types

* Simple tags implemenrtion for status and payment col

* Add colors from theme

* Make tagCell more dynamic

* Refactor Datagrid file structure

* Add loading indicators

* Selecting column without cells in readonly

* Add sort icons to orders list

* Refactor after CR. fix typos

* Change color of selected colum cell on hover

* Improve selected header text contrast

* Move useColumnPickerColumns to hooks dir with tests

* Add less padding to column picker button

* Remove double border top

* Fix cursor pointer for tagCell and moneyCell

* useGetCellContent hook

* On loading show only one row

* Add missing darkmod color for warning tag

* Refactor columns in datagrid

* Add new macaw theme provider to storybook

* Fix  passing props in datagrid

* Trigger deployment

* Fix column picker in products

* useDatagridColumns

* Fix one more time

* Add column picker with default columns

* Change color for selected header change to textBrandDefault

* Remove unused code, move attributes colums as last

* Cleanup useDatagridColumns

* Improve DatagridProps

* Static datagrid for products (#3144)

* Migrate top nav of product list page to new MacawUI (#3290)

* feat: migrate top nav of product page

* feat: add proper deprecation links

---------

Co-authored-by: Michał Droń <dron.official@yahoo.com>

* Datagrin on order details intial

* Adjust ExtraInfoLines

* Remove padding on datagrid card content

* Remove datagrid card paddding (#3310)

* Disable column icon when no rows in orders

* Datagrid row hover show only when readonly and row clickable

* Implement card view for product list (#3292)

* Add temporary view switcher

* Add basic product tile view

* Bump macaw-ui

* Add ellipsis

* Bump macaw-ui

* Add status dot & fix non-rectangular thumbnails

* Bump macaw-ui

* Add variable size placeholder icon

* Improve loader

* Fix top nav menu key error

* Add pagination

* Add unit tests

* Extract messages

* Extract status color to function

* CR Refactor

* Hold product view state in local storage (#3315)

* Remember view state for product list

* Use util status function for status dots

* Datagrin in orderDraftDetails

* Remove not used components

* Fix for empty column and hover in datagrid for product (#3324)

* Remove datagrid card paddding (#3310)

* Fix for empty column and hover in datagrid for product (#3324)

* Use themeValues from macaw (#3326)

* Upgrade macaw

* Use themeValues

* Use themeValues from macaw (#3326)

* Upgrade macaw

* Use themeValues

* Add empty column from datagrid, improve theme types

* Use theme type from typeof

* Use empty column and themeValues

* Filter empty column from default

* New product header (#3346)

* Extraxt messages

* Remve title left padding

* Fix switching view

* Add margin right to nav button

* Improve view switch

* Update switch view icons

* Add spacing to switch

* Add more space

* Add new filterbar to order list

* Code refactor and tests

* Refactor OrderDraftDetailsDatagrid

* Extract messages

* Refactor OrderDraftDetailsDatagrid

* Update alert messages

* Extract messages

* Write unit tests

* Improve switch component

* Overwrite Pill styles

* Common method to get status color for pills

* Local Pill component POC

* Add ThemeProvider to test wrapper

* Extract messages

* Refactor Pill

* Fix Pill path

* Fix tests mocks

* Remove scrollbar and border bottom

* Add custom border to to datagrid

* Fix borders

* Fix border bottom

* Refactor and cleanup

* Remove not needed selectionActions code

* Move logic code t misc

* Fix scrollbar and zindex datagrid borders

* Fix product tiles condition

* Use utils functions, remove not used code

* Refactor to hooks

* Loading prps instead of disabled

* use getStatusColor

* Move getMenuItems to separate function

* Fix loading props

* Use empty colum hook in OrderDetailsDatagrid

* Fix empty column when save column change

* Fix bottom line in layout overlap

* Show moneCell with discounted price

* Make quantity ediable in order draft datagrid

* Readonly datagrid cells

* Update onyl when column is quantity

* Fix message

* Keep first column in datagrid not removable

* Fix for not existing column

* Add loader over datagrid, fix problem with border top when empty text in variants

* Fix error color and change color in datagrid

* Use formatMoneyAmount

* Fix remove order draft product with discount

* Extract messages

* Add product sku to order draft details datagrid

* Fix loading state and change cell color

* Add MoneyDiscuntedCell

* Use MoneyDiscuntedCell in order draft details datagrid

* Add trash bin icon

* Restor discount modal for order draft summary

* Fix problem with deleting quantity

* Improve await for promises and handler zero quantity error

* Fix column order issue

* Add discount modal box shadow

* Allow decimal as percentage value for discount

* Fix max fixed value

* Remove double border

* Fix z-index issue on discount modal

* Remove padding on order details datagrid

* Add proper error message to common discount modal

* Fix is submit disabled

* Move status as last column in datagrid

* Add padding to money discount cell editor

* Make quantity column smaller

* Fix recalculating disount value

* Fix calculate change discount type

* Store calculated value without triming decimal, trim decimal in input

* Refactor money cells

* markCellsDirty rename to areCellsDirty

* Remove discount from MoneyCell

* Use const to store row height in discount editor

* Fix copy in discount modal

* Remove past on money discount cell

* Remove locale in product varaints

* Fix nullable sku

* Extract messages

* Fix keeping always first column

* Remove padding on tracking info

* Fix story

* Fix render 0 money amount

* Fix displaying not empty string money

* adding new tests: add new product, change quantity, add inline discount, delete product for grid - on orders details view (#3652)

* adding new e2e for grid on orders details view

* merging conflicts fix - and adding new TC numbers to new tests

* trigger tests

* failing tests fixes

---------

Co-authored-by: Michał Droń <dron.official@yahoo.com>
Co-authored-by: Krzysztof Żuraw <9116238+krzysztofzuraw@users.noreply.github.com>
Co-authored-by: Michał Droń <droniu@droniu.dev>
Co-authored-by: wojteknowacki <124166231+wojteknowacki@users.noreply.github.com>
Co-authored-by: wojteknowacki <wojciech.nowacki@saleor.io>
This commit is contained in:
Paweł Chyła 2023-05-18 09:52:13 +02:00 committed by GitHub
parent 757833ca57
commit 55337b5998
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
64 changed files with 1461 additions and 832 deletions

View file

@ -3,10 +3,13 @@
import faker from "faker";
import { ORDER_REFUND } from "../../elements/orders/order-refund";
import { ORDERS_SELECTORS } from "../../elements/orders/orders-selectors";
import { BUTTON_SELECTORS } from "../../elements/shared/button-selectors";
import { SHARED_ELEMENTS } from "../../elements/shared/sharedElements";
import {
BUTTON_SELECTORS,
ORDER_GRANT_REFUND,
ORDERS_SELECTORS,
SHARED_ELEMENTS,
} from "../../elements/";
import { MESSAGES } from "../../fixtures";
import { urlList } from "../../fixtures/urlList";
import { ONE_PERMISSION_USERS } from "../../fixtures/users";
import {
@ -22,6 +25,7 @@ import {
createFulfilledOrder,
createOrder,
createReadyToFulfillOrder,
createUnconfirmedOrder,
} from "../../support/api/utils/ordersUtils";
import * as productsUtils from "../../support/api/utils/products/productsUtils";
import {
@ -34,6 +38,12 @@ import {
} from "../../support/api/utils/taxesUtils";
import { selectChannelInPicker } from "../../support/pages/channelsPage";
import { finalizeDraftOrder } from "../../support/pages/draftOrderPage";
import {
addNewProductToOrder,
applyFixedLineDiscountForProduct,
changeQuantityOfProducts,
deleteProductFromGridTableOnIndex,
} from "../../support/pages/ordersOperations";
describe("Orders", () => {
const startsWith = "CyOrders-";
@ -47,6 +57,9 @@ describe("Orders", () => {
let address;
let taxClass;
const shippingPrice = 2;
const variantPrice = 1;
before(() => {
cy.clearSessionData().loginUserViaRequest();
deleteCustomersStartsWith(startsWith);
@ -73,6 +86,7 @@ describe("Orders", () => {
createShipping({
channelId: defaultChannel.id,
name: randomName,
price: shippingPrice,
address,
taxClassId: taxClass.id,
});
@ -95,6 +109,7 @@ describe("Orders", () => {
productsUtils.createProductInChannel({
name: randomName,
channelId: defaultChannel.id,
price: variantPrice,
warehouseId: warehouse.id,
productTypeId: productTypeResp.id,
attributeId: attributeResp.id,
@ -206,7 +221,7 @@ describe("Orders", () => {
cy.visit(urlList.orders + `${order.id}`);
cy.get(ORDERS_SELECTORS.refundButton)
.click()
.get(ORDER_REFUND.productsQuantityInput)
.get(ORDER_GRANT_REFUND.productsQuantityInput)
.type("1")
.addAliasToGraphRequest("OrderFulfillmentRefundProducts");
cy.get(BUTTON_SELECTORS.submit)
@ -221,4 +236,103 @@ describe("Orders", () => {
});
},
);
it(
"should add line item discount (for single product in order) . TC: SALEOR_2125",
{ tags: ["@orders", "@allEnv", "@stable"] },
() => {
const totalPrice = variantPrice + shippingPrice;
const inlineDiscount = 0.5;
const discountReason = "product damaged";
createUnconfirmedOrder({
customerId: customer.id,
channelId: defaultChannel.id,
shippingMethod,
variantsList,
address,
}).then(unconfirmedOrderResponse => {
cy.visit(urlList.orders + `${unconfirmedOrderResponse.order.id}`);
applyFixedLineDiscountForProduct(inlineDiscount, discountReason);
cy.get(ORDERS_SELECTORS.priceCellFirstRowOrderDetails).should(
"have.text",
inlineDiscount,
);
cy.get(ORDERS_SELECTORS.orderSummarySubtotalPriceRow).should(
"contain.text",
variantPrice - inlineDiscount,
);
cy.get(ORDERS_SELECTORS.orderSummaryTotalPriceRow).should(
"contain.text",
totalPrice - inlineDiscount,
);
});
},
);
it(
"should remove product from unconfirmed order . TC: SALEOR_2126",
{ tags: ["@orders", "@allEnv", "@stable"] },
() => {
createUnconfirmedOrder({
customerId: customer.id,
channelId: defaultChannel.id,
shippingMethod,
variantsList,
address,
}).then(unconfirmedOrderResponse => {
cy.visit(urlList.orders + `${unconfirmedOrderResponse.order.id}`);
deleteProductFromGridTableOnIndex(0);
cy.contains(MESSAGES.noProductFound).should("be.visible");
cy.get(ORDERS_SELECTORS.productDeleteFromRowButton).should("not.exist");
});
},
);
it(
"should change quantity of products on order detail view . TC: SALEOR_2127",
{ tags: ["@orders", "@allEnv", "@stable"] },
() => {
createUnconfirmedOrder({
customerId: customer.id,
channelId: defaultChannel.id,
shippingMethod,
variantsList,
address,
}).then(unconfirmedOrderResponse => {
cy.visit(urlList.orders + `${unconfirmedOrderResponse.order.id}`);
changeQuantityOfProducts();
cy.get(ORDERS_SELECTORS.orderSummarySubtotalPriceRow).should(
"contain.text",
variantPrice * 2,
);
cy.get(ORDERS_SELECTORS.orderSummaryTotalPriceRow).should(
"contain.text",
shippingPrice + variantPrice * 2,
);
});
},
);
it(
"should add new product on order detail view . TC: SALEOR_2128",
{ tags: ["@orders", "@allEnv", "@stable"] },
() => {
createUnconfirmedOrder({
customerId: customer.id,
channelId: defaultChannel.id,
shippingMethod,
variantsList,
address,
}).then(unconfirmedOrderResponse => {
cy.visit(urlList.orders + `${unconfirmedOrderResponse.order.id}`);
cy.get(ORDERS_SELECTORS.dataGridTable).should("be.visible");
addNewProductToOrder().then(productName => {
cy.get(ORDERS_SELECTORS.productNameSecondRowOrderDetails).should(
"contain.text",
productName,
);
});
});
},
);
});

View file

@ -3,7 +3,7 @@
import faker from "faker";
import { BUTTON_SELECTORS, PRODUCTS_LIST } from "../../../elements/";
import { DIALOGS_MESSAGES } from "../../../fixtures/";
import { MESSAGES } from "../../../fixtures/";
import { urlList } from "../../../fixtures/urlList";
import { getDefaultChannel, productsUtils } from "../../../support/api/utils/";
import { ensureCanvasStatic } from "../../../support/customCommands/sharedElementsOperations/canvas";
@ -55,9 +55,7 @@ describe("Test for deleting products", () => {
cy.clickGridCell(0, 0);
cy.clickGridCell(0, 1);
cy.clickOnElement(BUTTON_SELECTORS.deleteProductsButton);
cy.contains(DIALOGS_MESSAGES.confirmProductsDeletion).should(
"be.visible",
);
cy.contains(MESSAGES.confirmProductsDeletion).should("be.visible");
cy.addAliasToGraphRequest("productBulkDelete")
.clickSubmitButton()
.waitForRequestAndCheckIfNoErrors("@productBulkDelete");

View file

@ -24,7 +24,7 @@ export const ATTRIBUTES_DETAILS = {
entityTypeOptions: {
PRODUCT: '[data-test-id="select-field-option-PRODUCT"]',
PRODUCT_VARIANT: '[data-test-id="select-field-option-PRODUCT_VARIANT"]',
PAGE: '[data-test-id*="PAGE"]',
PAGE: '[data-test-id="select-field-option-PAGE"]',
},
selectUnitCheckbox: '[name="selectUnit"]',
unitSystemSelect: '[data-test-id="unit-system"]',

View file

@ -22,6 +22,8 @@ export { SALES_SELECTORS, VOUCHERS_SELECTORS } from "./discounts";
export { HOMEPAGE_SELECTORS } from "./homePage/homePage-selectors";
export { PAGINATION } from "./navigation";
export {
ADD_PRODUCT_TO_ORDER_DIALOG,
DRAFT_ORDER_SELECTORS,
DRAFT_ORDERS_LIST_SELECTORS,
ORDER_GRANT_REFUND,
ORDER_TRANSACTION_CREATE,

View file

@ -1,4 +1,9 @@
export { DRAFT_ORDERS_LIST_SELECTORS } from "./draft-orders-list-selectors";
export {
ADD_PRODUCT_TO_ORDER_DIALOG,
ORDERS_SELECTORS,
} from "./orders-selectors";
export { DRAFT_ORDER_SELECTORS } from "./draft-order-selectors";
export { ORDER_GRANT_REFUND } from "./order-grant-refund";
export { ORDERS_SELECTORS } from "./orders-selectors";
export { ORDER_TRANSACTION_CREATE } from "./transaction-selectors";

View file

@ -1,3 +0,0 @@
export const ORDER_REFUND = {
productsQuantityInput: '[data-test-id*="quantity-input"]'
};

View file

@ -9,10 +9,26 @@ export const ORDERS_SELECTORS = {
orderFulfillmentFrame: "[data-test-id='order-fulfillment']",
refundButton: '[data-test-id="refund-button"]',
fulfillMenuButton: '[data-test-id="fulfill-menu"]',
priceCellFirstRowOrderDetails: "[id='glide-cell-4-0']",
productNameSecondRowOrderDetails: "[id='glide-cell-1-1']",
quantityCellFirstRowOrderDetails: "[id='glide-cell-3-0']",
discountFixedPriceButton: '[data-test-id="FIXED"]',
discountAmountField: '[data-test-id="price-field"]',
discountReasonField: '[data-test-id="discount-reason"]',
orderSummarySubtotalPriceRow: '[data-test-id="order-subtotal-price"]',
orderSummaryTotalPriceRow: '[data-test-id="order-total-price"]',
dataGridTable: "[data-testid='data-grid-canvas']",
productDeleteFromRowButton: "[data-test-id='row-action-button']",
markAsPaidButton: '[data-test-id="markAsPaidButton"]',
grantRefundButton: '[data-test-id="grantRefundButton"]',
transactionReferenceInput: '[data-test-id="transaction-reference-input"]',
orderTransactionsList: '[data-test-id="orderTransactionsList"]',
grantRefundButton: '[data-test-id="grantRefundButton"]',
captureManualTransactionButton:
'[data-test-id="captureManualTransactionButton"]',
};
export const ADD_PRODUCT_TO_ORDER_DIALOG = {
productRow: "[data-test-id='product']",
productName: "[data-test-id='product-name']",
productVariant: "[data-test-id='variant']",
checkbox: "[data-test-id='checkbox']",
};

View file

@ -16,6 +16,7 @@ export const SHARED_ELEMENTS = {
selectOption: '[data-test-id*="select-field-option"]',
svgImage: "svg",
fileInput: 'input[type="file"]',
pageHeader: '[data-test-id="page-header"]',
urlInput: 'input[type="url"]',
richTextEditor: {
loader: '[class*="codex-editor__loader"]',

View file

@ -2,4 +2,4 @@ export { bodyMockHomePage } from "./bodyMocks";
export { orderDraftCreateDemoResponse } from "./errors/demo/orderDratCreate";
export { urlList } from "./urlList";
export { ONE_PERMISSION_USERS } from "./users";
export { DIALOGS_MESSAGES } from "./messages";
export { MESSAGES } from "./messages";

View file

@ -1,3 +1,4 @@
export const DIALOGS_MESSAGES = {
export const MESSAGES = {
noProductFound: "No products found",
confirmProductsDeletion: "Are you sure you want to delete 2 products?",
};

View file

@ -208,6 +208,28 @@ function assignVariantsToOrder(order, variantsList) {
orderRequest.addProductToOrder(order.id, variantElement.id);
});
}
export function createUnconfirmedOrder({
customerId,
shippingMethod,
channelId,
variantsList,
address,
}) {
let order;
return orderRequest
.createDraftOrder({ customerId, channelId, address })
.then(orderResp => {
order = orderResp;
assignVariantsToOrder(order, variantsList);
})
.then(orderResp => {
shippingMethod = getShippingMethodIdFromCheckout(
orderResp.order,
shippingMethod.name,
);
orderRequest.addShippingMethod(order.id, shippingMethod);
});
}
export function addPayment(checkoutId) {
return checkoutRequest.addPayment({

View file

@ -1,4 +1,10 @@
import { CHANNEL_FORM_SELECTORS, ORDERS_SELECTORS } from "../../elements";
import {
ADD_PRODUCT_TO_ORDER_DIALOG,
CHANNEL_FORM_SELECTORS,
DRAFT_ORDER_SELECTORS,
ORDERS_SELECTORS,
SHARED_ELEMENTS,
} from "../../elements";
export function pickAndSelectChannelOnCreateOrderFormByIndex(index) {
cy.get(ORDERS_SELECTORS.createOrderButton)
@ -9,3 +15,57 @@ export function pickAndSelectChannelOnCreateOrderFormByIndex(index) {
.eq(index)
.click();
}
export function applyFixedLineDiscountForProduct(
discountAmount,
discountReason,
) {
cy.get(ORDERS_SELECTORS.dataGridTable).should("be.visible");
cy.get(ORDERS_SELECTORS.priceCellFirstRowOrderDetails)
.dblclick({ force: true })
.type("2", { force: true });
cy.get(ORDERS_SELECTORS.discountFixedPriceButton).click();
cy.get(ORDERS_SELECTORS.discountAmountField).type(discountAmount);
cy.get(ORDERS_SELECTORS.discountReasonField).type(discountReason);
cy.addAliasToGraphRequest("OrderLineDiscountUpdate")
.clickSubmitButton()
.click()
.waitForRequestAndCheckIfNoErrors("@OrderLineDiscountUpdate");
}
export function changeQuantityOfProducts() {
cy.get(ORDERS_SELECTORS.dataGridTable).should("be.visible");
cy.get(ORDERS_SELECTORS.quantityCellFirstRowOrderDetails)
.dblclick({ force: true })
.type("2", { force: true });
cy.addAliasToGraphRequest("OrderLineUpdate")
// grid expects focus to be dismissed from cell - because of that extra action needed which blur focus from cell (other more elegant build in actions was not working)
.get(SHARED_ELEMENTS.pageHeader)
.click()
.waitForRequestAndCheckIfNoErrors("@OrderLineUpdate");
}
export function deleteProductFromGridTableOnIndex(trIndex = 0) {
cy.get(ORDERS_SELECTORS.dataGridTable).should("be.visible");
cy.addAliasToGraphRequest("OrderLineDelete")
.get(ORDERS_SELECTORS.productDeleteFromRowButton)
.eq(trIndex)
.click()
.wait("@OrderLineDelete");
}
export function addNewProductToOrder(productIndex = 0, variantIndex = 0) {
cy.get(DRAFT_ORDER_SELECTORS.addProducts).click();
return cy
.get(ADD_PRODUCT_TO_ORDER_DIALOG.productRow)
.eq(productIndex)
.find(ADD_PRODUCT_TO_ORDER_DIALOG.productName)
.invoke("text")
.then(productName => {
cy.get(ADD_PRODUCT_TO_ORDER_DIALOG.productVariant)
.eq(variantIndex)
.find(ADD_PRODUCT_TO_ORDER_DIALOG.checkbox)
.click();
cy.addAliasToGraphRequest("OrderLinesAdd")
.get('[type="submit"]')
.click()
.waitForRequestAndCheckIfNoErrors("@OrderLinesAdd");
cy.wrap(productName);
});
}

View file

@ -2134,6 +2134,9 @@
"context": "checkbox label",
"string": "Restrict order value"
},
"DhFqJF": {
"string": "Edit discount"
},
"Dhherd": {
"context": "dialog header, title",
"string": "Invoice Generation"
@ -3237,6 +3240,9 @@
"context": "cancelled fulfillment, section header",
"string": "Refunded and Returned"
},
"LKD6fB": {
"string": "Remove product"
},
"LKoIB1": {
"string": "Add search engine title and description to make this product easier to find"
},
@ -3647,10 +3653,6 @@
"context": "input label",
"string": "Stock reservation for authenticated user (in minutes)"
},
"Oad+ES": {
"context": "alert message",
"string": "This product is not published in this channel."
},
"ObRk1O": {
"string": "If this option is disabled, discount will be counted for every eligible product"
},
@ -3924,6 +3926,10 @@
"context": "staff added type order discount",
"string": "Staff added"
},
"QL4a0Z": {
"context": "alert message",
"string": "Not published in this channel"
},
"QLVddq": {
"context": "button",
"string": "Create customer"
@ -4428,9 +4434,6 @@
"context": "searchbar placeholder",
"string": "Country"
},
"UD7/q8": {
"string": "No Products added to Order"
},
"UJnqdm": {
"context": "dialog header",
"string": "Unassign Attribute From Product Type"
@ -4618,6 +4621,10 @@
"context": "error message",
"string": "Default shipping zone already exists"
},
"VIdXPy": {
"context": "value input helper text",
"string": "Cannot be higher than the price"
},
"VKWPBf": {
"context": "dialog header",
"string": "Delete Staff User Avatar"
@ -5153,6 +5160,9 @@
"Yy/yDL": {
"string": "Reset password"
},
"YzLUXA": {
"string": "Ensure this value is greater than 0."
},
"Z/7hyu": {
"context": "card balance label",
"string": "Card Balance"
@ -5195,6 +5205,10 @@
"ZIc5lM": {
"string": "Product Name"
},
"ZJOX8n": {
"context": "alert message",
"string": "Product no longer exists"
},
"ZJPYFl": {
"context": "accepted header names",
"string": "Headers with in following format are accepted: <code>authorization*</code>, <code>x-*</code>"
@ -5737,6 +5751,10 @@
"context": "error message",
"string": "Cannot change the quantity because of insufficient stock"
},
"dDCLFW": {
"context": "alert message",
"string": "Not available for sale this channel"
},
"dEUZg2": {
"context": "header",
"string": "Menu Items"
@ -7221,10 +7239,6 @@
"context": "gift card history message",
"string": "Gift card was activated"
},
"pEMxyy": {
"context": "alert message",
"string": "This product does no longer exist."
},
"pFVX6g": {
"string": "Variant with these attributes already exists"
},
@ -8557,6 +8571,10 @@
"context": "modal header",
"string": "Media from the URL you supply will be shown in the media gallery. You will be able to define the order of the gallery."
},
"zHx85l": {
"context": "value input helper text",
"string": "Cannot be higher than 100%"
},
"zKOGkU": {
"context": "time during discount is active, header",
"string": "Active Dates"
@ -8565,10 +8583,6 @@
"context": "gift card removed success alert message",
"string": "{selectedItemsCount,plural,one{Successfully deleted gift card} other{Successfully deleted gift cards}}"
},
"zO+l0L": {
"context": "alert message",
"string": "This product is not available for sale in this channel."
},
"zQX6xO": {
"context": "dialog header",
"string": "Delete App"

View file

@ -35,6 +35,7 @@ const Checkbox: React.FC<CheckboxProps> = ({ helperText, error, ...props }) => {
return (
<>
<MuiCheckbox
data-test-id="checkbox"
{...rest}
onClick={
disableClickPropagation

View file

@ -102,6 +102,7 @@ export interface DatagridProps {
freezeColumns?: DataEditorProps["freezeColumns"];
verticalBorder?: DataEditorProps["verticalBorder"];
columnSelect?: DataEditorProps["columnSelect"];
showEmptyDatagrid?: boolean;
rowAnchor?: (item: Item) => string;
}
@ -128,6 +129,7 @@ export const Datagrid: React.FC<DatagridProps> = ({
columnSelect = "none",
onColumnMoved,
onColumnResize,
showEmptyDatagrid = false,
loading,
rowAnchor,
hasRowHover = false,
@ -156,6 +158,7 @@ export const Datagrid: React.FC<DatagridProps> = ({
const [selection, setSelection] = useState<GridSelection>();
const [hoverRow, setHoverRow] = useState<number | undefined>(undefined);
const [areCellsDirty, setCellsDirty] = useState(true);
// Allow to listen to which row is selected and notfiy parent component
useEffect(() => {
@ -178,7 +181,12 @@ export const Datagrid: React.FC<DatagridProps> = ({
removed,
getChangeIndex,
onRowAdded,
} = useDatagridChange(availableColumns, rows, onChange);
} = useDatagridChange(
availableColumns,
rows,
onChange,
(areCellsDirty: boolean) => setCellsDirty(areCellsDirty),
);
const rowsTotal = rows - removed.length + added.length;
const hasMenuItem = !!menuItems(0).length;
@ -197,7 +205,7 @@ export const Datagrid: React.FC<DatagridProps> = ({
return {
...getCellContent(item, opts),
...(changed
...(changed && areCellsDirty
? {
themeOverride: {
bgCell: themeValues.colors.background.surfaceBrandSubdued,
@ -214,13 +222,15 @@ export const Datagrid: React.FC<DatagridProps> = ({
};
},
[
availableColumns,
changes,
added,
removed,
getChangeIndex,
availableColumns,
getCellContent,
themeValues,
areCellsDirty,
themeValues.colors.background.surfaceBrandSubdued,
themeValues.colors.background.surfaceCriticalDepressed,
getCellError,
],
);
@ -452,7 +462,7 @@ export const Datagrid: React.FC<DatagridProps> = ({
</Header>
)}
<CardContent classes={{ root: classes.cardContentRoot }}>
{rowsTotal > 0 ? (
{rowsTotal > 0 || showEmptyDatagrid ? (
<>
{selection?.rows && selection?.rows.length > 0 && (
<div className={classes.actionBtnBar}>
@ -549,16 +559,6 @@ export const Datagrid: React.FC<DatagridProps> = ({
}
rowMarkerWidth={48}
/>
<Box
position="relative"
backgroundColor="plain"
borderTopWidth={1}
borderTopStyle="solid"
borderColor="neutralPlain"
__maxWidth={contentMaxWidth}
margin="auto"
zIndex="2"
/>
{/* FIXME: https://github.com/glideapps/glide-data-grid/issues/505 */}
{hasColumnGroups && (
<div className={classes.columnGroupFixer} />

View file

@ -1,13 +1,13 @@
import {
CustomCell,
CustomRenderer,
getMiddleCenterBias,
GridCellKind,
ProvideEditorCallback,
} from "@glideapps/glide-data-grid";
import React from "react";
import { usePriceField } from "../../PriceField/usePriceField";
import { usePriceField } from "../../../PriceField/usePriceField";
import { drawCurrency, drawPrice } from "./utils";
interface MoneyCellProps {
readonly kind: "money-cell";
@ -55,28 +55,13 @@ export const moneyCellRenderer = (): CustomRenderer<MoneyCell> => ({
const hasValue = value === 0 ? true : !!value;
const formatted = value?.toString() ?? "-";
ctx.fillStyle = theme.textDark;
ctx.textAlign = "right";
ctx.fillText(
formatted,
rect.x + rect.width - 8,
rect.y + rect.height / 2 + getMiddleCenterBias(ctx, theme),
);
drawPrice(ctx, theme, rect, formatted);
ctx.save();
ctx.fillStyle = theme.textMedium;
ctx.textAlign = "left";
ctx.font = [
theme.baseFontStyle.replace(/bold/g, "normal"),
theme.fontFamily,
].join(" ");
ctx.fillText(
hasValue ? currency : "-",
rect.x + 8,
rect.y + rect.height / 2 + getMiddleCenterBias(ctx, theme),
);
ctx.restore();
drawCurrency(ctx, theme, rect, hasValue ? currency : "-");
ctx.restore();
return true;
},
provideEditor: () => ({

View file

@ -0,0 +1,114 @@
import { Locale } from "@dashboard/components/Locale";
import OrderDiscountCommonModal from "@dashboard/orders/components/OrderDiscountCommonModal/OrderDiscountCommonModal";
import {
ORDER_LINE_DISCOUNT,
OrderDiscountCommonInput,
} from "@dashboard/orders/components/OrderDiscountCommonModal/types";
import { useOrderLineDiscountContext } from "@dashboard/products/components/OrderDiscountProviders/OrderLineDiscountProvider";
import {
CustomCell,
CustomRenderer,
GridCellKind,
} from "@glideapps/glide-data-grid";
import React, { useCallback } from "react";
import { cellHeight } from "../../styles";
import {
drawCurrency,
drawLineCrossedPrice,
drawPrice,
getFormattedMoney,
} from "./utils";
interface MoneyDiscountedCellProps {
readonly kind: "money-discounted-cell";
readonly currency: string;
readonly undiscounted?: string | number;
readonly value: number | string | null;
readonly lineItemId?: string;
readonly locale: Locale;
}
const DATAGRID_BORDER_WIDTH = 1;
const ROW_HEIGHT = cellHeight + DATAGRID_BORDER_WIDTH;
export type MoneyDiscuntedCell = CustomCell<MoneyDiscountedCellProps>;
const MoneyDiscountedCellEditor = ({ onFinishedEditing, value }) => {
const getDiscountProviderValues = useOrderLineDiscountContext();
const editedLineId = value.data.lineItemId;
const discountProviderValues = editedLineId
? getDiscountProviderValues(editedLineId)
: null;
const handleDiscountConfirm = useCallback(
async (discount: OrderDiscountCommonInput) => {
await discountProviderValues.addOrderLineDiscount(discount);
onFinishedEditing(undefined);
},
[discountProviderValues, onFinishedEditing],
);
const handleDiscountRemove = useCallback(async () => {
await discountProviderValues.removeOrderLineDiscount();
onFinishedEditing(undefined);
}, [discountProviderValues, onFinishedEditing]);
return (
<OrderDiscountCommonModal
onClose={() => onFinishedEditing(undefined)}
modalType={ORDER_LINE_DISCOUNT}
maxPrice={discountProviderValues.unitUndiscountedPrice}
onConfirm={handleDiscountConfirm}
onRemove={handleDiscountRemove}
existingDiscount={discountProviderValues.orderLineDiscount}
confirmStatus={discountProviderValues.orderLineDiscountUpdateStatus}
removeStatus={discountProviderValues.orderLineDiscountRemoveStatus}
/>
);
};
export const moneyDiscountedCellRenderer =
(): CustomRenderer<MoneyDiscuntedCell> => ({
kind: GridCellKind.Custom,
isMatch: (c): c is MoneyDiscuntedCell =>
(c.data as any).kind === "money-discounted-cell",
draw: (args, cell) => {
const { ctx, theme, rect } = args;
const { currency, value, undiscounted, locale } = cell.data;
const hasValue = value === 0 ? true : !!value;
const formattedValue = getFormattedMoney(value, currency, locale, "-");
const formattedUndiscounted = getFormattedMoney(
undiscounted !== value ? undiscounted : "",
currency,
locale,
);
const formattedWithDiscount =
formattedUndiscounted + " " + formattedValue;
drawPrice(ctx, theme, rect, formattedWithDiscount);
// Draw crossed line above price without discount
if (undiscounted !== undefined && undiscounted !== value) {
drawLineCrossedPrice(
ctx,
rect,
formattedWithDiscount,
formattedUndiscounted,
);
}
ctx.save();
drawCurrency(ctx, theme, rect, hasValue ? currency : "-");
ctx.restore();
return true;
},
provideEditor: () => ({
editor: MoneyDiscountedCellEditor,
styleOverride: {
padding: `${ROW_HEIGHT}px 0 0 0`,
},
disableStyling: true,
}),
});

View file

@ -0,0 +1,2 @@
export * from "./MoneyDiscountedCell";
export * from "./MoneyCell";

View file

@ -0,0 +1,73 @@
import { Locale } from "@dashboard/components/Locale";
import { formatMoneyAmount } from "@dashboard/components/Money";
import {
getMiddleCenterBias,
Rectangle,
Theme,
} from "@glideapps/glide-data-grid";
const OFFSET = 8;
export function drawLineCrossedPrice(
ctx: CanvasRenderingContext2D,
rect: Rectangle,
discountedPrice: string,
undiscountedPrice: string,
) {
const { width: totalTextWidth } = ctx.measureText(discountedPrice);
const { width: undiscountedTextWidth } = ctx.measureText(undiscountedPrice);
ctx.fillRect(
rect.x + rect.width - OFFSET - totalTextWidth,
rect.y + rect.height / 2,
undiscountedTextWidth,
1,
);
}
export function drawPrice(
ctx: CanvasRenderingContext2D,
theme: Theme,
rect: Rectangle,
text: string,
) {
ctx.fillStyle = theme.textDark;
ctx.textAlign = "right";
ctx.fillText(
text,
rect.x + rect.width - OFFSET,
rect.y + rect.height / 2 + getMiddleCenterBias(ctx, theme),
);
}
export function drawCurrency(
ctx: CanvasRenderingContext2D,
theme: Theme,
rect: Rectangle,
currency: string,
) {
ctx.fillStyle = theme.textMedium;
ctx.textAlign = "left";
ctx.font = [
theme.baseFontStyle.replace(/bold/g, "normal"),
theme.fontFamily,
].join(" ");
ctx.fillText(
currency,
rect.x + 8,
rect.y + rect.height / 2 + getMiddleCenterBias(ctx, theme),
);
}
export function getFormattedMoney(
value: string | number,
currency: string,
locale: Locale,
placeholder = "",
) {
if (value !== undefined && value !== null && value !== "") {
return formatMoneyAmount({ amount: Number(value), currency }, locale);
}
return placeholder;
}

View file

@ -2,6 +2,7 @@ import {
NumberCell,
numberCellEmptyValue,
} from "@dashboard/components/Datagrid/customCells/NumberCell";
import { Locale } from "@dashboard/components/Locale";
import { GridCell, GridCellKind } from "@glideapps/glide-data-grid";
import {
@ -9,7 +10,7 @@ import {
DropdownCellContentProps,
DropdownChoice,
} from "./DropdownCell";
import { MoneyCell } from "./MoneyCell";
import { MoneyCell, MoneyDiscuntedCell } from "./Money";
import { ThumbnailCell } from "./ThumbnailCell";
const common = {
@ -110,6 +111,41 @@ export function moneyCell(
};
}
interface MoneyDiscountedCellData {
value: number | string | null;
discount?: string | number;
undiscounted?: string | number;
currency: string;
locale: Locale;
lineItemId?: string;
}
export function moneyDiscountedCell(
{
value,
undiscounted,
currency,
locale,
lineItemId,
}: MoneyDiscountedCellData,
opts?: Partial<GridCell>,
): MoneyDiscuntedCell {
return {
...common,
...opts,
kind: GridCellKind.Custom,
data: {
kind: "money-discounted-cell",
value,
currency,
undiscounted,
lineItemId,
locale,
},
copyData: value?.toString() ?? "",
};
}
export function dropdownCell(
value: DropdownChoice,
dataOpts: DropdownCellContentProps &

View file

@ -3,7 +3,8 @@ import { useExtraCells } from "@glideapps/glide-data-grid-cells";
import { useMemo } from "react";
import { dropdownCellRenderer } from "./DropdownCell";
import { moneyCellRenderer } from "./MoneyCell";
import { moneyCellRenderer } from "./Money/MoneyCell";
import { moneyDiscountedCellRenderer } from "./Money/MoneyDiscountedCell";
import { numberCellRenderer } from "./NumberCell";
import { thumbnailCellRenderer } from "./ThumbnailCell";
@ -14,6 +15,7 @@ export function useCustomCellRenderers() {
const renderers = useMemo(
() => [
moneyCellRenderer(),
moneyDiscountedCellRenderer(),
numberCellRenderer(locale),
dropdownCellRenderer,
thumbnailCellRenderer,

View file

@ -78,11 +78,11 @@ export function useColumnsDefault(
const columnChoices = useMemo(
() =>
applyFilters(columns).map(({ id, title }) => ({
applyFilters(columns, availableColumns).map(({ id, title }) => ({
label: title,
value: id,
})),
[columns],
[columns, availableColumns],
);
const availableColumnsChoices = useMemo(
() =>
@ -113,22 +113,27 @@ export function useColumnsDefault(
};
}
function applyFilters(columns: readonly AvailableColumn[]) {
return columns.filter(byNoEmptyColumn).filter(byNotFirstColumn);
function applyFilters(
columns: readonly AvailableColumn[],
availableColumns?: readonly AvailableColumn[],
) {
return columns
.filter(byNoEmptyColumn)
.filter(byNotFirstColumn(availableColumns));
}
function byNoEmptyColumn(column: AvailableColumn) {
return column.id !== "empty";
}
function byNotFirstColumn(
_: AvailableColumn,
index: number,
array: AvailableColumn[],
) {
if (array.some(col => col.id === "empty")) {
return index > 1;
function byNotFirstColumn(availableColumns?: readonly AvailableColumn[]) {
return (column: AvailableColumn, index: number, array: AvailableColumn[]) => {
// Check not first column base on available columns to prevent base on columns that order can change
if (availableColumns) {
const colIndex = availableColumns.findIndex(col => col.id === column.id);
return availableColumns[0]?.id === "empty" ? colIndex > 1 : colIndex > 0;
}
return index > 0;
return array[0]?.id === "empty" ? index > 1 : index > 0;
};
}

View file

@ -24,7 +24,10 @@ export interface DatagridChangeOpts {
removed: number[];
updates: DatagridChange[];
}
export type OnDatagridChange = (opts: DatagridChangeOpts) => void;
export type OnDatagridChange = (
opts: DatagridChangeOpts,
setMarkCellsDirty: (areCellsDirty: boolean) => void,
) => void;
export interface UseDatagridChangeState {
added: number[];
@ -64,6 +67,7 @@ function useDatagridChange(
availableColumns: readonly AvailableColumn[],
rows: number,
onChange?: OnDatagridChange,
setMarkCellsDirty?: (areCellsDirty: boolean) => void,
) {
const { added, setAdded, removed, setRemoved, changes } =
useDatagridChangeStateContext();
@ -78,14 +82,17 @@ function useDatagridChange(
const notify = useCallback(
(updates: DatagridChange[], added: number[], removed: number[]) => {
if (onChange) {
onChange({
onChange(
{
updates,
removed,
added,
});
},
setMarkCellsDirty,
);
}
},
[onChange],
[onChange, setMarkCellsDirty],
);
const onCellEdited = useCallback(

View file

@ -66,7 +66,7 @@ const useStyles = makeStyles(
boxShadow: "none !important",
padding: "0 !important",
},
"& input, & textarea": {
"& input:not([class*='MuiInputBase']), & textarea": {
appearance: "none",
background: "none",
border: "none",
@ -77,7 +77,7 @@ const useStyles = makeStyles(
padding: vars.space[3],
outline: 0,
},
'& input[type="number"]': {
"& input[type='number']:not([class*='MuiInputBase'])": {
textAlign: "right",
width: "100%",
},

View file

@ -77,6 +77,7 @@ export const PriceField: React.FC<PriceFieldProps> = props => {
fullWidth
value={value}
InputLabelProps={InputLabelProps}
data-test-id="price-field"
InputProps={{
...InputProps,
endAdornment: currencySymbol ? (

View file

@ -13,7 +13,7 @@ import { FormattedMessage } from "react-intl";
import { useStyles } from "./styles";
export interface RadioGroupFieldChoice<
T extends string | number = string | number
T extends string | number = string | number,
> {
disabled?: boolean;
value: T;
@ -85,6 +85,7 @@ export const RadioGroupField: React.FC<RadioGroupFieldProps> = props => {
}}
control={
<Radio
data-test-id={choice.value}
className={clsx({
[classes.alignTop]: alignTop,
})}

View file

@ -0,0 +1,84 @@
import ColumnPicker from "@dashboard/components/ColumnPicker";
import Datagrid from "@dashboard/components/Datagrid/Datagrid";
import { useColumnsDefault } from "@dashboard/components/Datagrid/hooks/useColumnsDefault";
import {
DatagridChangeStateContext,
useDatagridChangeState,
} from "@dashboard/components/Datagrid/hooks/useDatagridChange";
import { OrderLineFragment } from "@dashboard/graphql";
import React from "react";
import { useIntl } from "react-intl";
import { messages } from "../OrderListDatagrid/messages";
import { useColumns, useGetCellContent } from "./datagrid";
interface OrderDetailsDatagridProps {
lines: OrderLineFragment[];
loading: boolean;
}
export const OrderDetailsDatagrid = ({
lines,
loading,
}: OrderDetailsDatagridProps) => {
const intl = useIntl();
const datagrid = useDatagridChangeState();
const availableColumns = useColumns();
const {
availableColumnsChoices,
columnChoices,
columns,
defaultColumns,
onColumnMoved,
onColumnResize,
onColumnsChange,
picker,
} = useColumnsDefault(availableColumns);
const getCellContent = useGetCellContent({
columns,
data: lines,
loading,
});
return (
<DatagridChangeStateContext.Provider value={datagrid}>
<Datagrid
readonly
showEmptyDatagrid
rowMarkers="none"
columnSelect="single"
freezeColumns={1}
availableColumns={columns}
emptyText={intl.formatMessage(messages.emptyText)}
getCellContent={getCellContent}
getCellError={() => false}
menuItems={() => []}
rows={loading ? 1 : lines.length}
selectionActions={() => null}
onColumnResize={onColumnResize}
onColumnMoved={onColumnMoved}
renderColumnPicker={defaultProps => (
<ColumnPicker
{...defaultProps}
IconButtonProps={{
...defaultProps.IconButtonProps,
disabled: lines.length === 0,
}}
availableColumns={availableColumnsChoices}
initialColumns={columnChoices}
defaultColumns={defaultColumns}
onSave={onColumnsChange}
hasMore={false}
loading={false}
onFetchMore={() => undefined}
onQueryChange={picker.setQuery}
query={picker.query}
/>
)}
/>
</DatagridChangeStateContext.Provider>
);
};

View file

@ -0,0 +1,110 @@
import {
loadingCell,
moneyCell,
readonlyTextCell,
thumbnailCell,
} from "@dashboard/components/Datagrid/customCells/cells";
import { GetCellContentOpts } from "@dashboard/components/Datagrid/Datagrid";
import { AvailableColumn } from "@dashboard/components/Datagrid/types";
import { OrderLineFragment } from "@dashboard/graphql";
import { getDatagridRowDataIndex } from "@dashboard/misc";
import { GridCell, Item } from "@glideapps/glide-data-grid";
import { useCallback, useMemo } from "react";
import { useIntl } from "react-intl";
import { columnsMessages } from "./messages";
export const useColumns = (): AvailableColumn[] => {
const intl = useIntl();
const columns = useMemo(
() => [
{
id: "product",
title: intl.formatMessage(columnsMessages.product),
width: 300,
},
{
id: "sku",
title: intl.formatMessage(columnsMessages.sku),
width: 150,
},
{
id: "quantity",
title: intl.formatMessage(columnsMessages.quantity),
width: 80,
},
{
id: "price",
title: intl.formatMessage(columnsMessages.price),
width: 150,
},
{
id: "total",
title: intl.formatMessage(columnsMessages.total),
width: 150,
},
],
[intl],
);
return columns;
};
interface GetCellContentProps {
columns: AvailableColumn[];
data: OrderLineFragment[];
loading: boolean;
}
export const useGetCellContent = ({
columns,
data,
loading,
}: GetCellContentProps) => {
const getCellContent = useCallback(
([column, row]: Item, { added, removed }: GetCellContentOpts): GridCell => {
if (loading) {
return loadingCell();
}
const columnId = columns[column].id;
const rowData = added.includes(row)
? undefined
: data[getDatagridRowDataIndex(row, removed)];
if (!rowData) {
return readonlyTextCell("", false);
}
switch (columnId) {
case "product":
return thumbnailCell(
rowData?.productName ?? "",
rowData.thumbnail?.url ?? "",
);
case "sku":
return readonlyTextCell(rowData.productSku ?? "", false);
case "quantity":
return readonlyTextCell(rowData.quantity.toString(), false);
case "price":
return moneyCell(
rowData.unitPrice.gross.amount,
rowData.unitPrice.gross.currency,
);
case "total":
return moneyCell(
rowData.totalPrice.gross.amount,
rowData.totalPrice.gross.currency,
);
default:
return readonlyTextCell("", false);
}
},
[columns, data, loading],
);
return getCellContent;
};

View file

@ -0,0 +1 @@
export * from "./OrderDetailsDatagrid";

View file

@ -0,0 +1,29 @@
import { defineMessages } from "react-intl";
export const columnsMessages = defineMessages({
product: {
id: "WE8IFE",
defaultMessage: "Product",
description: "product name",
},
sku: {
id: "8J81ri",
defaultMessage: "SKU",
description: "ordered product sku",
},
quantity: {
id: "tvpAXl",
defaultMessage: "Quantity",
description: "ordered product quantity",
},
price: {
id: "b810WJ",
defaultMessage: "Price",
description: "product price",
},
total: {
id: "qT6YYk",
defaultMessage: "Total",
description: "order line total price",
},
});

View file

@ -14,7 +14,7 @@ import React from "react";
import OrderDetailsPage, { OrderDetailsPageProps } from "./OrderDetailsPage";
const props: Omit<OrderDetailsPageProps, "classes"> = {
disabled: false,
loading: false,
onBillingAddressEdit: undefined,
onTransactionAction: () => undefined,
onFulfillmentApprove: () => undefined,

View file

@ -50,7 +50,7 @@ export interface OrderDetailsPageProps {
id: string;
name: string;
}>;
disabled: boolean;
loading: boolean;
saveButtonBarState: ConfirmButtonTransitionState;
errors: OrderErrorFragment[];
onOrderLineAdd?: () => void;
@ -85,7 +85,7 @@ export interface OrderDetailsPageProps {
const OrderDetailsPage: React.FC<OrderDetailsPageProps> = props => {
const {
disabled,
loading,
order,
shop,
saveButtonBarState,
@ -162,11 +162,11 @@ const OrderDetailsPage: React.FC<OrderDetailsPageProps> = props => {
const allowSave = () => {
if (!isOrderUnconfirmed) {
return disabled;
return loading;
} else if (!order?.lines?.length) {
return true;
}
return disabled;
return loading;
};
const selectCardMenuItems = filteredConditionalItems([
@ -231,12 +231,14 @@ const OrderDetailsPage: React.FC<OrderDetailsPageProps> = props => {
notAllowedToFulfillUnpaid={notAllowedToFulfillUnpaid}
lines={unfulfilled}
onFulfill={onOrderFulfill}
loading={loading}
/>
) : (
<>
<OrderDraftDetails
order={order}
errors={errors}
loading={loading}
onOrderLineAdd={onOrderLineAdd}
onOrderLineChange={onOrderLineChange}
onOrderLineRemove={onOrderLineRemove}

View file

@ -16,7 +16,7 @@ import React from "react";
import OrderDetailsPage, { OrderDetailsPageProps } from "./OrderDetailsPage";
const props: Omit<OrderDetailsPageProps, "classes"> = {
disabled: false,
loading: false,
onBillingAddressEdit: undefined,
onTransactionAction: () => undefined,
onFulfillmentApprove: () => undefined,

View file

@ -13,6 +13,9 @@ const useStyles = makeStyles(
alignItems: "center",
padding: theme.spacing(3, 3, 0, 3),
},
closeIcon: {
cursor: "pointer",
},
}),
{ name: "ModalTitle" },
);
@ -34,7 +37,7 @@ const ModalTitle: React.FC<ModalTitleProps> = ({
<>
<div className={classes.container}>
<Typography variant="h5">{title}</Typography>
<CloseIcon onClick={onClose} />
<CloseIcon className={classes.closeIcon} onClick={onClose} />
</div>
{withBorder && (
<>

View file

@ -6,22 +6,10 @@ import RadioGroupField from "@dashboard/components/RadioGroupField";
import { DiscountValueTypeEnum, MoneyFragment } from "@dashboard/graphql";
import { useUpdateEffect } from "@dashboard/hooks/useUpdateEffect";
import { buttonMessages } from "@dashboard/intl";
import {
Card,
CardContent,
Popper,
TextField,
Typography,
} from "@material-ui/core";
import { PopperPlacementType } from "@material-ui/core/Popper";
import { toFixed } from "@dashboard/utils/toFixed";
import { Card, CardContent, TextField, Typography } from "@material-ui/core";
import { ConfirmButtonTransitionState, makeStyles } from "@saleor/macaw-ui";
import React, {
ChangeEvent,
MutableRefObject,
useEffect,
useRef,
useState,
} from "react";
import React, { ChangeEvent, useEffect, useRef, useState } from "react";
import { defineMessages, useIntl } from "react-intl";
import ModalTitle from "./ModalTitle";
@ -31,16 +19,10 @@ import {
OrderDiscountType,
} from "./types";
const fullNumbersRegex = /^[0-9]*$/;
const numbersRegex = /([0-9]+\.?[0-9]*)$/;
const PERMIL = 0.01;
const useStyles = makeStyles(
theme => ({
container: {
zIndex: 1000,
marginTop: theme.spacing(1),
},
removeButton: {
"&:hover": {
backgroundColor: theme.palette.error.main,
@ -98,6 +80,16 @@ const messages = defineMessages({
defaultMessage: "Invalid value",
description: "value input helper text",
},
valueBiggerThatPrice: {
defaultMessage: "Cannot be higher than the price",
id: "VIdXPy",
description: "value input helper text",
},
valueBiggerThat100: {
defaultMessage: "Cannot be higher than 100%",
id: "zHx85l",
description: "value input helper text",
},
discountValueLabel: {
id: "GAmGog",
defaultMessage: "Discount value",
@ -116,10 +108,7 @@ export interface OrderDiscountCommonModalProps {
onClose: () => void;
onRemove: () => void;
modalType: OrderDiscountType;
anchorRef: MutableRefObject<any>;
existingDiscount: OrderDiscountCommonInput;
dialogPlacement: PopperPlacementType;
isOpen: boolean;
confirmStatus: ConfirmButtonTransitionState;
removeStatus: ConfirmButtonTransitionState;
}
@ -128,12 +117,9 @@ const OrderDiscountCommonModal: React.FC<OrderDiscountCommonModalProps> = ({
maxPrice = { amount: null, currency: "" },
onConfirm,
modalType,
anchorRef,
onClose,
onRemove,
existingDiscount,
dialogPlacement,
isOpen,
confirmStatus,
removeStatus,
}) => {
@ -147,7 +133,7 @@ const OrderDiscountCommonModal: React.FC<OrderDiscountCommonModalProps> = ({
const stringifiedValue = existingDiscount.value.toString();
if (calculationMode === DiscountValueTypeEnum.FIXED) {
return parseFloat(stringifiedValue).toFixed(2);
return parseFloat(stringifiedValue).toString();
}
return stringifiedValue;
@ -166,7 +152,7 @@ const OrderDiscountCommonModal: React.FC<OrderDiscountCommonModalProps> = ({
const initialData = getInitialData();
const [isValueError, setValueError] = useState<boolean>(false);
const [valueErrorMsg, setValueErrorMsg] = useState<string | null>(null);
const [reason, setReason] = useState<string>(initialData.reason);
const [value, setValue] = useState<string>(initialData.value);
const [calculationMode, setCalculationMode] = useState<DiscountValueTypeEnum>(
@ -196,24 +182,36 @@ const OrderDiscountCommonModal: React.FC<OrderDiscountCommonModalProps> = ({
) => {
const value = event.target.value;
handleSetError(value);
setValueErrorMsg(getErrorMessage(value));
setValue(value);
};
const getParsedDiscountValue = () => parseFloat(value) || 0;
const isAmountTooLarge = () => {
const isAmountTooLarge = (value?: string) => {
const topAmount = isDiscountTypePercentage ? 100 : maxAmount;
if (value) {
return (parseFloat(value) || 0) > topAmount;
}
return getParsedDiscountValue() > topAmount;
};
const handleSetError = (value: string) => {
const regexToCheck = isDiscountTypePercentage
? fullNumbersRegex
: numbersRegex;
const getErrorMessage = (value: string): string | null => {
if (isAmountTooLarge(value)) {
if (calculationMode === DiscountValueTypeEnum.PERCENTAGE) {
return intl.formatMessage(messages.valueBiggerThat100);
}
setValueError(!regexToCheck.test(value));
return intl.formatMessage(messages.valueBiggerThatPrice);
}
if (!numbersRegex.test(value)) {
return intl.formatMessage(messages.invalidValue);
}
return null;
};
const handleConfirm = () => {
@ -228,7 +226,7 @@ const OrderDiscountCommonModal: React.FC<OrderDiscountCommonModalProps> = ({
setReason(initialData.reason);
setValue(initialData.value);
setCalculationMode(initialData.calculationMode);
setValueError(false);
setValueErrorMsg(null);
};
useEffect(setDefaultValues, [
@ -246,21 +244,22 @@ const OrderDiscountCommonModal: React.FC<OrderDiscountCommonModalProps> = ({
calculationMode === DiscountValueTypeEnum.FIXED;
const recalculatedValueFromPercentageToFixed = (
getParsedDiscountValue() *
PERMIL *
maxPrice.amount
).toFixed(2);
(getParsedDiscountValue() * maxPrice.amount) /
100
).toString();
const recalculatedValueFromFixedToPercentage = Math.round(
(getParsedDiscountValue() * (1 / PERMIL)) / maxPrice.amount,
const recalculatedValueFromFixedToPercentage = (
(getParsedDiscountValue() / maxPrice.amount) *
100
).toString();
const recalculatedValue = changedFromPercentageToFixed
? recalculatedValueFromPercentageToFixed
: recalculatedValueFromFixedToPercentage;
handleSetError(recalculatedValue);
setValueErrorMsg(getErrorMessage(recalculatedValue));
setValue(recalculatedValue);
previousCalculationMode.current = calculationMode;
};
useUpdateEffect(handleValueConversion, [calculationMode]);
@ -274,15 +273,9 @@ const OrderDiscountCommonModal: React.FC<OrderDiscountCommonModalProps> = ({
calculationMode === DiscountValueTypeEnum.FIXED ? currency : "%";
const isSubmitDisabled =
!getParsedDiscountValue() || isValueError || isAmountTooLarge();
!getParsedDiscountValue() || !!valueErrorMsg || isAmountTooLarge();
return (
<Popper
open={isOpen}
anchorEl={anchorRef.current}
className={classes.container}
placement={dialogPlacement}
>
<Card>
<ModalTitle title={intl.formatMessage(dialogTitle)} onClose={onClose} />
<CardContent>
@ -297,9 +290,9 @@ const OrderDiscountCommonModal: React.FC<OrderDiscountCommonModalProps> = ({
<CardSpacer />
<PriceField
label={intl.formatMessage(messages.discountValueLabel)}
error={isValueError}
hint={isValueError && intl.formatMessage(messages.invalidValue)}
value={value}
error={!!valueErrorMsg}
hint={valueErrorMsg}
value={toFixed(value, 2)}
onChange={handleSetDiscountValue}
currencySymbol={valueFieldSymbol}
/>
@ -311,6 +304,7 @@ const OrderDiscountCommonModal: React.FC<OrderDiscountCommonModalProps> = ({
className={classes.reasonInput}
label={intl.formatMessage(messages.discountReasonLabel)}
value={reason}
data-test-id="discount-reason"
onChange={(event: ChangeEvent<HTMLInputElement>) =>
setReason(event.target.value)
}
@ -337,7 +331,6 @@ const OrderDiscountCommonModal: React.FC<OrderDiscountCommonModalProps> = ({
)}
</DialogButtons>
</Card>
</Popper>
);
};

View file

@ -22,6 +22,7 @@ interface OrderDraftDetailsProps {
order: OrderDetailsFragment;
channelUsabilityData?: ChannelUsabilityDataQuery;
errors: OrderErrorFragment[];
loading: boolean;
onOrderLineAdd: () => void;
onOrderLineChange: (id: string, data: OrderLineInput) => void;
onOrderLineRemove: (id: string) => void;
@ -32,6 +33,7 @@ const OrderDraftDetails: React.FC<OrderDraftDetailsProps> = ({
order,
channelUsabilityData,
errors,
loading,
onOrderLineAdd,
onOrderLineChange,
onOrderLineRemove,
@ -70,6 +72,7 @@ const OrderDraftDetails: React.FC<OrderDraftDetailsProps> = ({
<OrderDraftDetailsProducts
order={order}
errors={errors}
loading={loading}
onOrderLineChange={onOrderLineChange}
onOrderLineRemove={onOrderLineRemove}
/>

View file

@ -0,0 +1,126 @@
import ColumnPicker from "@dashboard/components/ColumnPicker";
import Datagrid from "@dashboard/components/Datagrid/Datagrid";
import { useColumnsDefault } from "@dashboard/components/Datagrid/hooks/useColumnsDefault";
import {
DatagridChangeOpts,
DatagridChangeStateContext,
useDatagridChangeState,
} from "@dashboard/components/Datagrid/hooks/useDatagridChange";
import { OrderDetailsFragment, OrderErrorFragment } from "@dashboard/graphql";
import { TrashBinIcon } from "@saleor/macaw-ui/next";
import React, { useCallback } from "react";
import { useIntl } from "react-intl";
import { FormData } from "../OrderDraftDetailsProducts/OrderDraftDetailsProducts";
import { useColumns, useGetCellContent } from "./datagrid";
import { messages } from "./messages";
interface OrderDraftDetailsDatagridProps {
loading: boolean;
lines: OrderDetailsFragment["lines"];
errors: OrderErrorFragment[];
onOrderLineChange: (id: string, data: FormData) => void;
onOrderLineRemove: (id: string) => void;
}
export const OrderDraftDetailsDatagrid = ({
lines,
errors,
onOrderLineChange,
onOrderLineRemove,
}: OrderDraftDetailsDatagridProps) => {
const intl = useIntl();
const datagrid = useDatagridChangeState();
const { availableColumns } = useColumns();
const {
availableColumnsChoices,
columnChoices,
columns,
defaultColumns,
onColumnMoved,
onColumnResize,
onColumnsChange,
picker,
} = useColumnsDefault(availableColumns);
const getCellContent = useGetCellContent({
columns,
lines,
errors,
});
const getMenuItems = useCallback(
index => [
{
label: intl.formatMessage(messages.deleteOrder),
Icon: <TrashBinIcon />,
onSelect: () => {
onOrderLineRemove(lines[index].id);
},
},
],
[intl, lines, onOrderLineRemove],
);
const handleDatagridChange = useCallback(
async (
{ updates }: DatagridChangeOpts,
setMarkCellsDirty: (areCellsDirty: boolean) => void,
) => {
await Promise.all(
updates.map(({ data, column, row }) => {
const orderId = lines[row].id;
if (column === "quantity" && data !== "") {
return onOrderLineChange(orderId, { quantity: data });
}
}),
);
datagrid.changes.current = [];
setMarkCellsDirty(false);
},
[datagrid.changes, lines, onOrderLineChange],
);
return (
<DatagridChangeStateContext.Provider value={datagrid}>
<Datagrid
rowMarkers="none"
columnSelect="none"
freezeColumns={2}
verticalBorder={col => (col > 1 ? true : false)}
availableColumns={columns}
emptyText={intl.formatMessage(messages.emptyText)}
getCellContent={getCellContent}
getCellError={() => false}
menuItems={getMenuItems}
rows={lines.length}
selectionActions={() => null}
onColumnResize={onColumnResize}
onColumnMoved={onColumnMoved}
renderColumnPicker={defaultProps => (
<ColumnPicker
{...defaultProps}
IconButtonProps={{
...defaultProps.IconButtonProps,
disabled: lines.length === 0,
}}
availableColumns={availableColumnsChoices}
initialColumns={columnChoices}
defaultColumns={defaultColumns}
onSave={onColumnsChange}
hasMore={false}
loading={false}
onFetchMore={() => undefined}
onQueryChange={picker.setQuery}
query={picker.query}
/>
)}
onChange={handleDatagridChange}
/>
</DatagridChangeStateContext.Provider>
);
};

View file

@ -0,0 +1,217 @@
import {
moneyCell,
moneyDiscountedCell,
readonlyTextCell,
tagsCell,
textCell,
thumbnailCell,
} from "@dashboard/components/Datagrid/customCells/cells";
import { GetCellContentOpts } from "@dashboard/components/Datagrid/Datagrid";
import { useEmptyColumn } from "@dashboard/components/Datagrid/hooks/useEmptyColumn";
import { AvailableColumn } from "@dashboard/components/Datagrid/types";
import { OrderDetailsFragment, OrderErrorFragment } from "@dashboard/graphql";
import useLocale from "@dashboard/hooks/useLocale";
import {
getDatagridRowDataIndex,
getStatusColor,
isFirstColumn,
} from "@dashboard/misc";
import { useOrderLineDiscountContext } from "@dashboard/products/components/OrderDiscountProviders/OrderLineDiscountProvider";
import getOrderErrorMessage from "@dashboard/utils/errors/order";
import { GridCell, Item } from "@glideapps/glide-data-grid";
import { DefaultTheme, useTheme } from "@saleor/macaw-ui/next";
import { useMemo } from "react";
import { IntlShape, useIntl } from "react-intl";
import { lineAlertMessages } from "../OrderDraftDetailsProducts/messages";
import { columnsMessages } from "./messages";
export const useColumns = () => {
const emptyColumn = useEmptyColumn();
const intl = useIntl();
const availableColumns = useMemo(
() => [
emptyColumn,
{
id: "product",
title: intl.formatMessage(columnsMessages.product),
width: 300,
},
{
id: "sku",
title: "SKU",
width: 150,
},
{
id: "quantity",
title: intl.formatMessage(columnsMessages.quantity),
width: 80,
},
{
id: "price",
title: intl.formatMessage(columnsMessages.price),
width: 150,
},
{
id: "total",
title: intl.formatMessage(columnsMessages.total),
width: 150,
},
{
id: "status",
title: "Status",
width: 250,
},
],
[emptyColumn, intl],
);
return {
availableColumns,
};
};
interface GetCellContentProps {
columns: AvailableColumn[];
lines: OrderDetailsFragment["lines"];
errors: OrderErrorFragment[];
}
export const useGetCellContent = ({
columns,
lines,
errors,
}: GetCellContentProps) => {
const intl = useIntl();
const { theme } = useTheme();
const { locale } = useLocale();
const getValues = useOrderLineDiscountContext();
return (
[column, row]: Item,
{ added, removed, changes, getChangeIndex }: GetCellContentOpts,
): GridCell => {
if (isFirstColumn(column)) {
return readonlyTextCell("", false);
}
const columnId = columns[column].id;
const change = changes.current[getChangeIndex(columnId, row)]?.data;
const rowData = added.includes(row)
? undefined
: lines[getDatagridRowDataIndex(row, removed)];
if (!rowData) {
return readonlyTextCell("", false);
}
const { unitUndiscountedPrice, unitDiscountedPrice } = getValues(
rowData.id,
);
switch (columnId) {
case "product":
return thumbnailCell(
rowData?.productName ?? "",
rowData.thumbnail?.url ?? "",
{
readonly: true,
allowOverlay: false,
},
);
case "quantity":
return textCell(change || rowData.quantity.toString());
case "price":
return moneyDiscountedCell(
{
value: unitDiscountedPrice.amount,
currency: unitDiscountedPrice.currency,
undiscounted: unitUndiscountedPrice.amount,
lineItemId: rowData.id,
locale,
},
{
allowOverlay: true,
},
);
case "status":
const orderErrors = getOrderErrors(errors, rowData.id);
const status = getOrderLineStatus(intl, rowData, orderErrors);
return tagsCell(
status.map(toTagValue(theme)),
status.map(status => status.status),
{
readonly: true,
allowOverlay: false,
},
);
case "sku":
return readonlyTextCell(rowData?.productSku ?? "", false);
case "total":
return moneyCell(
rowData.totalPrice.gross.amount,
rowData.totalPrice.gross.currency,
{
readonly: true,
allowOverlay: false,
},
);
default:
return readonlyTextCell("", false);
}
};
};
function toTagValue(currentTheme: DefaultTheme) {
return ({ status, type }: OrderStatus) => ({
color: getStatusColor(type, currentTheme),
tag: status,
});
}
interface OrderStatus {
type: "warning" | "error";
status: string;
}
const getOrderLineStatus = (
intl: IntlShape,
line: OrderDetailsFragment["lines"][number],
error?: OrderErrorFragment,
): OrderStatus[] => {
const statuses = [];
if (error) {
statuses.push({
type: "error",
status: getOrderErrorMessage(error, intl),
});
}
const product = line.variant?.product;
if (!product) {
statuses.push({
type: "warning",
status: intl.formatMessage(lineAlertMessages.notExists),
});
}
const isAvailableForPurchase = product?.isAvailableForPurchase;
if (product && !isAvailableForPurchase) {
statuses.push({
type: "warning",
status: intl.formatMessage(lineAlertMessages.notAvailable),
});
}
return statuses;
};
function getOrderErrors(errors: OrderErrorFragment[], id: string) {
return errors.find(error => error.orderLines?.some(lineId => lineId === id));
}

View file

@ -0,0 +1 @@
export * from "./OrderDraftDetailsDatagrid";

View file

@ -0,0 +1,38 @@
import { defineMessages } from "react-intl";
export const columnsMessages = defineMessages({
product: {
id: "x/ZVlU",
defaultMessage: "Product",
},
quantity: {
id: "nEWp+k",
defaultMessage: "Quantity",
description: "quantity of ordered products",
},
price: {
id: "32dfzI",
defaultMessage: "Price",
description: "price or ordered products",
},
total: {
id: "lVwmf5",
defaultMessage: "Total",
description: "total price of ordered products",
},
});
export const messages = defineMessages({
emptyText: {
id: "Q1Uzbb",
defaultMessage: "No products found",
},
deleteOrder: {
id: "LKD6fB",
defaultMessage: "Remove product",
},
editDiscount: {
id: "DhFqJF",
defaultMessage: "Edit discount",
},
});

View file

@ -1,56 +1,18 @@
import ResponsiveTable from "@dashboard/components/ResponsiveTable";
import Skeleton from "@dashboard/components/Skeleton";
import TableRowLink from "@dashboard/components/TableRowLink";
import { OrderDetailsFragment, OrderErrorFragment } from "@dashboard/graphql";
import {
OrderLineDiscountConsumer,
OrderLineDiscountContextConsumerProps,
} from "@dashboard/products/components/OrderDiscountProviders/OrderLineDiscountProvider";
import getOrderErrorMessage from "@dashboard/utils/errors/order";
import { TableBody, TableCell, TableHead, Typography } from "@material-ui/core";
import { makeStyles } from "@saleor/macaw-ui";
import React from "react";
import { FormattedMessage, useIntl } from "react-intl";
import { renderCollection } from "../../../misc";
import TableLine from "./TableLine";
import { OrderDraftDetailsDatagrid } from "../OrderDraftDetailsDatagrid/OrderDraftDetailsDatagrid";
export interface FormData {
quantity: number;
}
const useStyles = makeStyles(
theme => ({
colAction: {
width: theme.spacing(10),
},
colName: {
width: "auto",
},
colNameLabel: {},
colPrice: {},
colQuantity: {},
colTotal: {},
skeleton: {
margin: theme.spacing(0, 4),
},
errorInfo: {
color: theme.palette.error.main,
marginLeft: theme.spacing(1.5),
display: "inline",
},
quantityField: {
"& input": {
padding: "12px 12px 10px",
},
width: 60,
},
table: {
[theme.breakpoints.up("md")]: {
tableLayout: "auto",
},
tableLayout: "auto",
},
}),
{ name: "OrderDraftDetailsProducts" },
);
@ -58,19 +20,21 @@ const useStyles = makeStyles(
interface OrderDraftDetailsProductsProps {
order?: OrderDetailsFragment;
errors: OrderErrorFragment[];
loading: boolean;
onOrderLineChange: (id: string, data: FormData) => void;
onOrderLineRemove: (id: string) => void;
}
const OrderDraftDetailsProducts: React.FC<
OrderDraftDetailsProductsProps
> = props => {
const { order, errors, onOrderLineChange, onOrderLineRemove } = props;
const OrderDraftDetailsProducts: React.FC<OrderDraftDetailsProductsProps> = ({
order,
errors,
loading,
onOrderLineChange,
onOrderLineRemove,
}) => {
const classes = useStyles();
const lines = order?.lines ?? [];
const intl = useIntl();
const classes = useStyles(props);
const formErrors = errors.filter(error => error.field === "lines");
if (order === undefined) {
@ -78,79 +42,15 @@ const OrderDraftDetailsProducts: React.FC<
}
return (
<ResponsiveTable className={classes.table}>
{!!lines.length && (
<TableHead>
<TableRowLink>
<TableCell className={classes.colName} colSpan={2}>
<span className={classes.colNameLabel}>
<FormattedMessage id="x/ZVlU" defaultMessage="Product" />
</span>
</TableCell>
<TableCell className={classes.colQuantity}>
<FormattedMessage
id="nEWp+k"
defaultMessage="Quantity"
description="quantity of ordered products"
/>
</TableCell>
<TableCell className={classes.colPrice}>
<FormattedMessage
id="32dfzI"
defaultMessage="Price"
description="price or ordered products"
/>
</TableCell>
<TableCell className={classes.colTotal}>
<FormattedMessage
id="lVwmf5"
defaultMessage="Total"
description="total price of ordered products"
/>
</TableCell>
<TableCell className={classes.colAction} />
</TableRowLink>
</TableHead>
)}
<TableBody>
{!!lines.length ? (
renderCollection(lines, line => (
<OrderLineDiscountConsumer key={line.id} orderLineId={line.id}>
{(
orderLineDiscountProps: OrderLineDiscountContextConsumerProps,
) => (
<TableLine
{...orderLineDiscountProps}
line={line}
error={formErrors.find(error =>
error.orderLines?.some(id => id === line.id),
)}
onOrderLineChange={onOrderLineChange}
<OrderDraftDetailsDatagrid
lines={lines}
loading={loading}
onOrderLineRemove={onOrderLineRemove}
onOrderLineChange={onOrderLineChange}
errors={formErrors}
/>
)}
</OrderLineDiscountConsumer>
))
) : (
<>
<TableRowLink>
<TableCell colSpan={5}>
<FormattedMessage
id="UD7/q8"
defaultMessage="No Products added to Order"
/>
{!!formErrors.length && (
<Typography variant="body2" className={classes.errorInfo}>
{getOrderErrorMessage(formErrors[0], intl)}
</Typography>
)}
</TableCell>
</TableRowLink>
</>
)}
</TableBody>
</ResponsiveTable>
);
};
OrderDraftDetailsProducts.displayName = "OrderDraftDetailsProducts";
export default OrderDraftDetailsProducts;

View file

@ -1,54 +0,0 @@
import { order } from "@dashboard/orders/fixtures";
import { render, screen } from "@testing-library/react";
import React from "react";
import TableLine from "./TableLine";
jest.mock("react-intl", () => ({
useIntl: jest.fn(() => ({
formatMessage: jest.fn(x => x.defaultMessage),
})),
defineMessages: jest.fn(x => x),
}));
jest.mock("@saleor/macaw-ui", () => ({
useStyles: jest.fn(() => () => ({})),
makeStyles: jest.fn(() => () => ({})),
Avatar: jest.fn(() => () => <></>),
IconButton: jest.fn(() => () => <></>),
DeleteIcon: jest.fn(() => () => <></>),
ImageIcon: jest.fn(() => () => <></>),
}));
const mockedOrder = order("");
describe("TableLine rendering", () => {
it("renders with values from API", async () => {
// Arrange
const mockedLine = mockedOrder.lines[0];
const props = {
line: mockedLine,
onOrderLineChange: jest.fn(),
onOrderLineRemove: jest.fn(),
addOrderLineDiscount: jest.fn(),
removeOrderLineDiscount: jest.fn(),
orderLineDiscountUpdateStatus: "default" as const,
orderLineDiscountRemoveStatus: "default" as const,
openDialog: jest.fn(),
closeDialog: jest.fn(),
isDialogOpen: false,
totalDiscountedPrice: mockedLine.totalPrice.gross,
unitUndiscountedPrice: mockedLine.undiscountedUnitPrice.gross,
unitDiscountedPrice: mockedLine.unitPrice.gross,
};
// Act
render(<TableLine {...props} />);
// Assert
const tableLine = screen.getByTestId(`table-line-total-${mockedLine.id}`);
expect(tableLine).toHaveTextContent(
mockedLine.totalPrice.gross.currency.toString(),
);
});
});

View file

@ -1,165 +0,0 @@
import Link from "@dashboard/components/Link";
import Money from "@dashboard/components/Money";
import TableCellAvatar from "@dashboard/components/TableCellAvatar";
import { AVATAR_MARGIN } from "@dashboard/components/TableCellAvatar/Avatar";
import TableRowLink from "@dashboard/components/TableRowLink";
import {
OrderErrorFragment,
OrderLineFragment,
OrderLineInput,
} from "@dashboard/graphql";
import { OrderLineDiscountContextConsumerProps } from "@dashboard/products/components/OrderDiscountProviders/OrderLineDiscountProvider";
import { TableCell, Typography } from "@material-ui/core";
import { DeleteIcon, IconButton, makeStyles } from "@saleor/macaw-ui";
import clsx from "clsx";
import React, { useRef } from "react";
import { maybe } from "../../../misc";
import OrderDiscountCommonModal from "../OrderDiscountCommonModal";
import { ORDER_LINE_DISCOUNT } from "../OrderDiscountCommonModal/types";
import TableLineAlert from "./TableLineAlert";
import TableLineForm from "./TableLineForm";
import useLineAlerts from "./useLineAlerts";
const useStyles = makeStyles(
theme => ({
colStatusEmpty: {
"&:first-child:not(.MuiTableCell-paddingCheckbox)": {
paddingRight: 0,
},
},
colAction: {
width: `calc(76px + ${theme.spacing(0.5)})`,
},
colName: {
width: "auto",
},
colNameLabel: {
marginLeft: AVATAR_MARGIN,
},
colPrice: {
textAlign: "right",
},
colQuantity: {
textAlign: "right",
},
colTotal: {
textAlign: "right",
},
strike: {
textDecoration: "line-through",
color: theme.palette.grey[400],
},
}),
{ name: "OrderDraftDetailsProducts" },
);
interface TableLineProps extends OrderLineDiscountContextConsumerProps {
line: OrderLineFragment;
error?: OrderErrorFragment;
onOrderLineChange: (id: string, data: OrderLineInput) => void;
onOrderLineRemove: (id: string) => void;
}
const TableLine: React.FC<TableLineProps> = ({
line,
error,
onOrderLineChange,
onOrderLineRemove,
orderLineDiscount,
addOrderLineDiscount,
removeOrderLineDiscount,
openDialog,
closeDialog,
orderLineDiscountRemoveStatus,
isDialogOpen,
totalDiscountedPrice,
unitUndiscountedPrice,
unitDiscountedPrice,
orderLineDiscountUpdateStatus,
}) => {
const classes = useStyles();
const popperAnchorRef = useRef<HTMLTableRowElement | null>(null);
const { id, thumbnail, productName, productSku } = line;
const alerts = useLineAlerts({
line,
error,
});
const getUnitPriceLabel = () => {
const money = <Money money={unitUndiscountedPrice} />;
if (!!orderLineDiscount) {
return (
<>
<Typography className={classes.strike}>{money}</Typography>
<Link onClick={openDialog}>
<Money money={unitDiscountedPrice} />
</Link>
</>
);
}
return <Link onClick={openDialog}>{money}</Link>;
};
return (
<TableRowLink key={id}>
<TableCell
className={clsx({
[classes.colStatusEmpty]: !alerts.length,
})}
>
{!!alerts.length && (
<TableLineAlert
alerts={alerts}
variant={!!error ? "error" : "warning"}
/>
)}
</TableCell>
<TableCellAvatar
className={classes.colName}
thumbnail={maybe(() => thumbnail.url)}
>
<Typography variant="body2">{productName}</Typography>
<Typography variant="caption">{productSku}</Typography>
</TableCellAvatar>
<TableCell className={classes.colQuantity}>
<TableLineForm line={line} onOrderLineChange={onOrderLineChange} />
</TableCell>
<TableCell className={classes.colPrice} ref={popperAnchorRef}>
{getUnitPriceLabel()}
<OrderDiscountCommonModal
isOpen={isDialogOpen}
anchorRef={popperAnchorRef}
onClose={closeDialog}
modalType={ORDER_LINE_DISCOUNT}
maxPrice={unitUndiscountedPrice}
onConfirm={addOrderLineDiscount}
onRemove={removeOrderLineDiscount}
existingDiscount={orderLineDiscount}
confirmStatus={orderLineDiscountUpdateStatus}
removeStatus={orderLineDiscountRemoveStatus}
dialogPlacement="bottom-end"
/>
</TableCell>
<TableCell className={classes.colTotal}>
<Money
money={{
amount: totalDiscountedPrice.amount,
currency: totalDiscountedPrice.currency,
}}
data-test-id={`table-line-total-${line.id}`}
/>
</TableCell>
<TableCell className={classes.colAction}>
<IconButton variant="secondary" onClick={() => onOrderLineRemove(id)}>
<DeleteIcon color="primary" />
</IconButton>
</TableCell>
</TableRowLink>
);
};
export default TableLine;

View file

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

View file

@ -1,75 +0,0 @@
import DebounceForm from "@dashboard/components/DebounceForm";
import Form from "@dashboard/components/Form";
import { OrderLineFragment, OrderLineInput } from "@dashboard/graphql";
import createNonNegativeValueChangeHandler from "@dashboard/utils/handlers/nonNegativeValueChangeHandler";
import { TextField } from "@material-ui/core";
import { makeStyles } from "@saleor/macaw-ui";
import React from "react";
const useStyles = makeStyles(
() => ({
quantityField: {
"& input": {
padding: "12px 12px 10px",
textAlign: "right",
},
width: 100,
},
}),
{ name: "TableLineForm" },
);
interface TableLineFormProps {
line: OrderLineFragment;
onOrderLineChange: (id: string, data: OrderLineInput) => void;
}
const TableLineForm: React.FC<TableLineFormProps> = ({
line,
onOrderLineChange,
}) => {
const classes = useStyles({});
const { id, quantity } = line;
const handleSubmit = (id: string, data: OrderLineInput) => {
const quantity = data?.quantity >= 1 ? Math.floor(data.quantity) : 1;
onOrderLineChange(id, { quantity });
};
return (
<Form initial={{ quantity }} onSubmit={data => handleSubmit(id, data)}>
{({ change, data, submit, set }) => {
const handleQuantityChange = createNonNegativeValueChangeHandler(
change,
);
return (
<DebounceForm
change={handleQuantityChange}
submit={submit}
time={200}
>
{debounce => (
<TextField
className={classes.quantityField}
fullWidth
name="quantity"
type="number"
value={data.quantity}
onChange={debounce}
onBlur={() => {
if (data.quantity < 1) {
set({ quantity: 1 });
}
submit();
}}
inputProps={{ min: 1 }}
/>
)}
</DebounceForm>
);
}}
</Form>
);
};
export default TableLineForm;

View file

@ -2,18 +2,18 @@ import { defineMessages } from "react-intl";
export const lineAlertMessages = defineMessages({
notPublished: {
id: "Oad+ES",
defaultMessage: "This product is not published in this channel.",
id: "QL4a0Z",
defaultMessage: "Not published in this channel",
description: "alert message",
},
notAvailable: {
id: "zO+l0L",
defaultMessage: "This product is not available for sale in this channel.",
id: "dDCLFW",
defaultMessage: "Not available for sale this channel",
description: "alert message",
},
notExists: {
id: "pEMxyy",
defaultMessage: "This product does no longer exist.",
id: "ZJOX8n",
defaultMessage: "Product no longer exists",
description: "alert message",
},
});

View file

@ -1,40 +0,0 @@
import { OrderErrorFragment, OrderLineFragment } from "@dashboard/graphql";
import getOrderErrorMessage from "@dashboard/utils/errors/order";
import { useMemo } from "react";
import { useIntl } from "react-intl";
import { lineAlertMessages } from "./messages";
interface UseLineAlertsOpts {
line: OrderLineFragment;
error?: OrderErrorFragment;
}
const useLineAlerts = ({ line, error }: UseLineAlertsOpts) => {
const intl = useIntl();
const alerts = useMemo(() => {
const alerts: string[] = [];
if (error) {
alerts.push(getOrderErrorMessage(error, intl));
}
const product = line.variant?.product;
if (!product) {
alerts.push(intl.formatMessage(lineAlertMessages.notExists));
}
const isAvailableForPurchase = product?.isAvailableForPurchase;
if (product && !isAvailableForPurchase) {
alerts.push(intl.formatMessage(lineAlertMessages.notAvailable));
}
return alerts;
}, [line, error, intl]);
return alerts;
};
export default useLineAlerts;

View file

@ -12,7 +12,8 @@ import { getFormErrors } from "@dashboard/utils/errors";
import getOrderErrorMessage from "@dashboard/utils/errors/order";
import { Typography } from "@material-ui/core";
import { makeStyles } from "@saleor/macaw-ui";
import React, { useRef } from "react";
import { Box, Button, Popover, sprinkles } from "@saleor/macaw-ui/next";
import React from "react";
import { useIntl } from "react-intl";
import OrderDiscountCommonModal from "../OrderDiscountCommonModal";
@ -87,8 +88,6 @@ const OrderDraftDetailsSummary: React.FC<
const intl = useIntl();
const classes = useStyles(props);
const popperAnchorRef = useRef<HTMLTableRowElement | null>(null);
if (!order) {
return null;
}
@ -179,36 +178,47 @@ const OrderDraftDetailsSummary: React.FC<
return (
<table className={classes.root}>
<tbody>
<tr className={classes.relativeRow} ref={popperAnchorRef}>
<tr className={classes.relativeRow}>
<td>
<Link onClick={openDialog}>
{intl.formatMessage(discountTitle)}
</Link>
<Popover open={isDialogOpen}>
<Popover.Trigger>
<Button
variant="tertiary"
onClick={isDialogOpen ? closeDialog : openDialog}
>
<Link>{intl.formatMessage(discountTitle)}</Link>
</Button>
</Popover.Trigger>
<Popover.Content
align="start"
className={sprinkles({ zIndex: "3" })}
>
<Box boxShadow="overlay">
<OrderDiscountCommonModal
dialogPlacement="bottom-start"
modalType={ORDER_DISCOUNT}
anchorRef={popperAnchorRef}
existingDiscount={orderDiscount}
maxPrice={undiscountedPrice}
isOpen={isDialogOpen}
onConfirm={addOrderDiscount}
onClose={closeDialog}
onRemove={removeOrderDiscount}
confirmStatus={orderDiscountAddStatus}
removeStatus={orderDiscountRemoveStatus}
/>
</Box>
</Popover.Content>
</Popover>
</td>
<td className={classes.textRight}>
{getOrderDiscountLabel(orderDiscount)}
</td>
</tr>
<tr>
<tr data-test-id="order-subtotal-price">
<td>{intl.formatMessage(messages.subtotal)}</td>
<td className={classes.textRight}>
<Money money={subtotal.gross} />
</td>
</tr>
<tr>
<tr data-test-id="order-add-shipping-line">
<td>
{hasShippingMethods && getShippingMethodComponent()}
@ -230,13 +240,13 @@ const OrderDraftDetailsSummary: React.FC<
)}
</td>
</tr>
<tr>
<tr data-test-id="order-taxes-price">
<td>{intl.formatMessage(messages.taxes)}</td>
<td className={classes.textRight}>
<Money money={order.total.tax} />
</td>
</tr>
<tr>
<tr data-test-id="order-total-price">
<td>{intl.formatMessage(messages.total)}</td>
<td className={classes.textRight}>
<Money money={total.gross} />

View file

@ -43,6 +43,7 @@ const order = draftOrder(placeholderImage);
const props: Omit<OrderDraftPageProps, "classes"> = {
...fetchMoreProps,
loading: false,
disabled: false,
fetchUsers: () => undefined,
onBillingAddressEdit: undefined,

View file

@ -53,7 +53,7 @@ export interface OrderDraftPageProps extends FetchMoreProps {
const OrderDraftPage: React.FC<OrderDraftPageProps> = props => {
const {
disabled,
loading,
fetchUsers,
hasMore,
saveButtonBarState,
@ -120,6 +120,7 @@ const OrderDraftPage: React.FC<OrderDraftPageProps> = props => {
order={order as OrderDetailsFragment}
channelUsabilityData={channelUsabilityData}
errors={errors}
loading={loading}
onOrderLineAdd={onOrderLineAdd}
onOrderLineChange={onOrderLineChange}
onOrderLineRemove={onOrderLineRemove}
@ -152,7 +153,7 @@ const OrderDraftPage: React.FC<OrderDraftPageProps> = props => {
</DetailPageLayout.RightSidebar>
<Savebar
state={saveButtonBarState}
disabled={disabled}
disabled={loading}
onCancel={() => navigate(orderDraftListUrl())}
onSubmit={onDraftFinalize}
labels={{

View file

@ -1,7 +1,7 @@
import TableRowLink from "@dashboard/components/TableRowLink";
import { FulfillmentStatus, OrderDetailsFragment } from "@dashboard/graphql";
import { getStringOrPlaceholder } from "@dashboard/misc";
import { TableCell, Typography } from "@material-ui/core";
import { Typography } from "@material-ui/core";
import { Box } from "@saleor/macaw-ui/next";
import clsx from "clsx";
import React from "react";
import { FormattedMessage, useIntl } from "react-intl";
@ -9,8 +9,6 @@ import { FormattedMessage, useIntl } from "react-intl";
import { extraInfoMessages } from "./messages";
import useStyles from "./styles";
const NUMBER_OF_COLUMNS = 5;
interface ExtraInfoLinesProps {
fulfillment?: OrderDetailsFragment["fulfillments"][0];
}
@ -26,8 +24,12 @@ const ExtraInfoLines: React.FC<ExtraInfoLinesProps> = ({ fulfillment }) => {
const { warehouse, trackingNumber, status } = fulfillment;
return (
<TableRowLink>
<TableCell className={classes.infoRow} colSpan={NUMBER_OF_COLUMNS}>
<Box
paddingY={7}
borderColor="neutralHighlight"
borderBottomStyle={"solid"}
borderBottomWidth={1}
>
<Typography color="textSecondary" variant="body2">
{warehouse && (
<>
@ -66,8 +68,7 @@ const ExtraInfoLines: React.FC<ExtraInfoLinesProps> = ({ fulfillment }) => {
/>
)}
</Typography>
</TableCell>
</TableRowLink>
</Box>
);
};

View file

@ -1,17 +1,14 @@
import CardSpacer from "@dashboard/components/CardSpacer";
import ResponsiveTable from "@dashboard/components/ResponsiveTable";
import { FulfillmentStatus, OrderDetailsFragment } from "@dashboard/graphql";
import TrashIcon from "@dashboard/icons/Trash";
import { orderHasTransactions } from "@dashboard/orders/types";
import { mergeRepeatedOrderLines } from "@dashboard/orders/utils/data";
import { Card, CardContent, TableBody } from "@material-ui/core";
import { Card, CardContent } from "@material-ui/core";
import { IconButton } from "@saleor/macaw-ui";
import React from "react";
import { renderCollection } from "../../../misc";
import OrderCardTitle from "../OrderCardTitle";
import TableHeader from "../OrderProductsCardElements/OrderProductsCardHeader";
import TableLine from "../OrderProductsCardElements/OrderProductsTableRow";
import { OrderDetailsDatagrid } from "../OrderDetailsDatagrid";
import ActionButtons from "./ActionButtons";
import ExtraInfoLines from "./ExtraInfoLines";
import useStyles from "./styles";
@ -55,10 +52,12 @@ const OrderFulfilledProductsCard: React.FC<
const getLines = () => {
if (statusesToMergeLines.includes(fulfillment?.status)) {
return mergeRepeatedOrderLines(fulfillment.lines);
return mergeRepeatedOrderLines(fulfillment.lines).map(
order => order.orderLine,
);
}
return fulfillment?.lines || [];
return fulfillment?.lines.map(order => order.orderLine) || [];
};
return (
@ -85,15 +84,8 @@ const OrderFulfilledProductsCard: React.FC<
}
/>
<CardContent>
<ResponsiveTable className={classes.table}>
<TableHeader />
<TableBody>
{renderCollection(getLines(), line => (
<TableLine key={line.id} line={line} />
))}
</TableBody>
<OrderDetailsDatagrid lines={getLines()} loading={false} />
<ExtraInfoLines fulfillment={fulfillment} />
</ResponsiveTable>
<ActionButtons
orderId={order?.id}
status={fulfillment?.status}

View file

@ -27,9 +27,6 @@ const useStyles = makeStyles(
infoLabelWithMargin: {
marginBottom: theme.spacing(),
},
infoRow: {
padding: theme.spacing(2, 3),
},
}),
{ name: "OrderFulfilledProductsCard" },
);

View file

@ -8,7 +8,6 @@ import { GetCellContentOpts } from "@dashboard/components/Datagrid/Datagrid";
import { useEmptyColumn } from "@dashboard/components/Datagrid/hooks/useEmptyColumn";
import { AvailableColumn } from "@dashboard/components/Datagrid/types";
import { Locale } from "@dashboard/components/Locale";
import { formatMoneyAmount } from "@dashboard/components/Money";
import { OrderListQuery } from "@dashboard/graphql";
import useLocale from "@dashboard/hooks/useLocale";
import {
@ -128,7 +127,7 @@ export const useGetCellContent = ({ columns, orders }: GetCellContentProps) => {
case "status":
return getStatusCellContent(intl, themeValues, currentTheme, rowData);
case "total":
return getTotalCellContent(locale, rowData);
return getTotalCellContent(rowData);
default:
return textCell("");
}
@ -213,15 +212,12 @@ export function getStatusCellContent(
}
export function getTotalCellContent(
locale: Locale,
rowData: RelayToFlat<OrderListQuery["orders"]>[number],
) {
if (rowData?.total?.gross) {
return moneyCell(
formatMoneyAmount(rowData.total.gross, locale),
rowData.total.gross.currency,
{ cursor: "pointer" },
);
return moneyCell(rowData.total.gross.amount, rowData.total.gross.currency, {
cursor: "pointer",
});
}
return readonlyTextCell("-");

View file

@ -169,12 +169,12 @@ const OrderProductAddDialog: React.FC<OrderProductAddDialogProps> = props => {
scrollableTarget={scrollableTargetId}
>
<ResponsiveTable key="table">
<TableBody>
<TableBody data-test-id="add-products-table">
{renderCollection(
productChoicesWithValidVariants,
(product, productIndex) => (
<React.Fragment key={product ? product.id : "skeleton"}>
<TableRowLink>
<TableRowLink data-test-id="product">
<TableCell
padding="checkbox"
className={classes.productCheckboxCell}
@ -199,14 +199,18 @@ const OrderProductAddDialog: React.FC<OrderProductAddDialogProps> = props => {
className={classes.avatar}
thumbnail={maybe(() => product.thumbnail.url)}
/>
<TableCell className={classes.colName} colSpan={2}>
<TableCell
className={classes.colName}
colSpan={2}
data-test-id="product-name"
>
{maybe(() => product.name)}
</TableCell>
</TableRowLink>
{maybe(() => product.variants, [])
.filter(isValidVariant)
.map((variant, variantIndex) => (
<TableRowLink key={variant.id}>
<TableRowLink key={variant.id} data-test-id="variant">
<TableCell />
<TableCell className={classes.colVariantCheckbox}>
<Checkbox
@ -242,7 +246,10 @@ const OrderProductAddDialog: React.FC<OrderProductAddDialogProps> = props => {
</div>
)}
</TableCell>
<TableCell className={classes.textRight}>
<TableCell
className={classes.textRight}
data-test-id="variant-price"
>
<OrderPriceLabel pricing={variant.pricing} />
</TableCell>
</TableRowLink>

View file

@ -1,22 +1,13 @@
import { Button } from "@dashboard/components/Button";
import CardSpacer from "@dashboard/components/CardSpacer";
import ResponsiveTable from "@dashboard/components/ResponsiveTable";
import { OrderLineFragment } from "@dashboard/graphql";
import { commonMessages } from "@dashboard/intl";
import { renderCollection } from "@dashboard/misc";
import {
Card,
CardActions,
CardContent,
TableBody,
Typography,
} from "@material-ui/core";
import { Card, CardActions, CardContent, Typography } from "@material-ui/core";
import React from "react";
import { FormattedMessage } from "react-intl";
import OrderCardTitle from "../OrderCardTitle";
import TableHeader from "../OrderProductsCardElements/OrderProductsCardHeader";
import TableLine from "../OrderProductsCardElements/OrderProductsTableRow";
import { OrderDetailsDatagrid } from "../OrderDetailsDatagrid";
import { useStyles } from "./styles";
interface OrderUnfulfilledProductsCardProps {
@ -24,13 +15,18 @@ interface OrderUnfulfilledProductsCardProps {
notAllowedToFulfillUnpaid: boolean;
lines: OrderLineFragment[];
onFulfill: () => void;
loading: boolean;
}
const OrderUnfulfilledProductsCard: React.FC<
OrderUnfulfilledProductsCardProps
> = props => {
const { showFulfillmentAction, notAllowedToFulfillUnpaid, lines, onFulfill } =
props;
> = ({
showFulfillmentAction,
notAllowedToFulfillUnpaid,
lines,
onFulfill,
loading,
}) => {
const classes = useStyles();
if (!lines.length) {
@ -47,14 +43,7 @@ const OrderUnfulfilledProductsCard: React.FC<
className={classes.cardTitle}
/>
<CardContent>
<ResponsiveTable className={classes.table}>
<TableHeader />
<TableBody>
{renderCollection(lines, line => (
<TableLine key={line.id} isOrderLine line={line} />
))}
</TableBody>
</ResponsiveTable>
<OrderDetailsDatagrid lines={lines} loading={loading} />
{showFulfillmentAction && (
<CardActions className={classes.actions}>
<Button

View file

@ -210,6 +210,7 @@ export const OrderDraftDetails: React.FC<OrderDraftDetailsProps> = ({
<OrderDiscountProvider order={order}>
<OrderLineDiscountProvider order={order}>
<OrderDraftPage
loading={loading}
disabled={loading}
errors={errors}
onNoteAdd={variables =>
@ -224,7 +225,6 @@ export const OrderDraftDetails: React.FC<OrderDraftDetailsProps> = ({
hasMore={users?.data?.search?.pageInfo?.hasNextPage || false}
onFetchMore={loadMoreCustomers}
fetchUsers={searchUsers}
loading={users.loading}
usersLoading={users.loading}
onCustomerEdit={handleCustomerChange}
onDraftFinalize={() => orderDraftFinalize.mutate({ id })}

View file

@ -186,7 +186,7 @@ export const OrderNormalDetails: React.FC<OrderNormalDetailsProps> = ({
/>
<OrderDetailsPage
onOrderReturn={() => navigate(orderReturnUrl(id))}
disabled={
loading={
updateMetadataOpts.loading || updatePrivateMetadataOpts.loading
}
errors={errors}

View file

@ -183,7 +183,7 @@ export const OrderUnconfirmedDetails: React.FC<
<OrderLineDiscountProvider order={order}>
<OrderDetailsPage
onOrderReturn={() => navigate(orderReturnUrl(id))}
disabled={
loading={
updateMetadataOpts.loading || updatePrivateMetadataOpts.loading
}
errors={errors}

View file

@ -9,7 +9,7 @@ import { getDefaultNotifierSuccessErrorData } from "@dashboard/hooks/useNotifier
import { getById } from "@dashboard/misc";
import { OrderDiscountCommonInput } from "@dashboard/orders/components/OrderDiscountCommonModal/types";
import { ConfirmButtonTransitionState } from "@saleor/macaw-ui";
import React, { createContext, useState } from "react";
import React, { createContext, useContext, useState } from "react";
import { useIntl } from "react-intl";
import {
@ -44,6 +44,16 @@ interface DiscountProviderProps {
export const OrderLineDiscountContext =
createContext<GetOrderLineDiscountContextConsumerProps>(null);
export const useOrderLineDiscountContext = () => {
const context = useContext(OrderLineDiscountContext);
if (context === null) {
throw new Error("You are outside order line discount context");
}
return context;
};
export const OrderLineDiscountProvider: React.FC<DiscountProviderProps> = ({
children,
order,

View file

@ -78,7 +78,7 @@ export const ProductVariants: React.FC<ProductVariantsProps> = ({
getColumnData(c, channels, warehouses, variantAttributes, intl),
)
: [],
[variantAttributes, warehouses, channels],
[variantAttributes, warehouses, channels, intl],
);
const {
@ -103,7 +103,7 @@ export const ProductVariants: React.FC<ProductVariantsProps> = ({
searchAttributeValues: onAttributeValuesSearch,
...opts,
}),
[columns, variants],
[channels, columns, onAttributeValuesSearch, variants],
);
const getCellError = React.useCallback(
@ -117,7 +117,7 @@ export const ProductVariants: React.FC<ProductVariantsProps> = ({
searchAttributeValues: onAttributeValuesSearch,
...opts,
}),
[columns, variants, errors],
[errors, columns, channels, variants, onAttributeValuesSearch],
);
return (

View file

@ -70,6 +70,10 @@ const messages = defineMessages({
defaultMessage: "Shipping method is required for this order",
description: "error message",
},
noZeroValue: {
defaultMessage: "Ensure this value is greater than 0.",
id: "YzLUXA",
},
});
function getOrderErrorMessage(
@ -104,6 +108,8 @@ function getOrderErrorMessage(
return intl.formatMessage(messages.shippingRequired);
case OrderErrorCode.VOID_INACTIVE_PAYMENT:
return intl.formatMessage(messages.cannotVoid);
case OrderErrorCode.ZERO_QUANTITY:
return intl.formatMessage(messages.noZeroValue);
}
}

14
src/utils/toFixed.test.ts Normal file
View file

@ -0,0 +1,14 @@
import { toFixed } from "./toFixed";
describe("toFixed", () => {
test("should return empty string if num is empty", () => {
expect(toFixed("", 2)).toBe("");
});
test("should return number with fixed decimal places", () => {
expect(toFixed("1.234567", 2)).toBe("1.23");
expect(toFixed("1.234567", 3)).toBe("1.234");
expect(toFixed("1.234567", 0)).toBe("1");
expect(toFixed("1.234567", 1)).toBe("1.2");
});
});

8
src/utils/toFixed.ts Normal file
View file

@ -0,0 +1,8 @@
export function toFixed(num: string | number, fixed: number) {
if (num === "" || num === null) {
return "";
}
const re = new RegExp("^-?\\d+(?:.\\d{0," + (fixed || -1) + "})?");
return num.toString().match(re)[0];
}