From 55337b599880ffaa2106753a054c5ec494102fa2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Chy=C5=82a?= Date: Thu, 18 May 2023 09:52:13 +0200 Subject: [PATCH] Order details datagrid (#3325) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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ń * 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ń Co-authored-by: Krzysztof Żuraw <9116238+krzysztofzuraw@users.noreply.github.com> Co-authored-by: Michał Droń Co-authored-by: wojteknowacki <124166231+wojteknowacki@users.noreply.github.com> Co-authored-by: wojteknowacki --- cypress/e2e/orders/orders.js | 124 +++++++++- .../products/productsList/deleteProducts.js | 6 +- .../elements/attribute/attributes_details.js | 2 +- cypress/elements/index.js | 2 + cypress/elements/orders/index.js | 7 +- cypress/elements/orders/order-refund.js | 3 - cypress/elements/orders/orders-selectors.js | 18 +- cypress/elements/shared/sharedElements.js | 1 + cypress/fixtures/index.js | 2 +- cypress/fixtures/messages.js | 3 +- cypress/support/api/utils/ordersUtils.js | 22 ++ cypress/support/pages/ordersOperations.js | 62 ++++- locale/defaultMessages.json | 44 ++-- src/components/Checkbox/Checkbox.tsx | 1 + src/components/Datagrid/Datagrid.tsx | 30 +-- .../customCells/{ => Money}/MoneyCell.tsx | 27 +-- .../customCells/Money/MoneyDiscountedCell.tsx | 114 +++++++++ .../Datagrid/customCells/Money/index.ts | 2 + .../Datagrid/customCells/Money/utils.ts | 73 ++++++ src/components/Datagrid/customCells/cells.ts | 38 ++- .../customCells/useCustomCellRenderers.ts | 4 +- .../Datagrid/hooks/useColumnsDefault.ts | 31 +-- .../Datagrid/hooks/useDatagridChange.ts | 21 +- src/components/Datagrid/styles.ts | 4 +- src/components/PriceField/PriceField.tsx | 1 + .../RadioGroupField/RadioGroupField.tsx | 3 +- .../OrderDetailsDatagrid.tsx | 84 +++++++ .../OrderDetailsDatagrid/datagrid.ts | 110 +++++++++ .../components/OrderDetailsDatagrid/index.ts | 1 + .../OrderDetailsDatagrid/messages.ts | 29 +++ .../OrderDetailsPage.stories.tsx | 2 +- .../OrderDetailsPage/OrderDetailsPage.tsx | 10 +- .../OrderDetailsPageTransactions.stories.tsx | 2 +- .../OrderDiscountCommonModal/ModalTitle.tsx | 5 +- .../OrderDiscountCommonModal.tsx | 205 ++++++++--------- .../OrderDraftDetails/OrderDraftDetails.tsx | 3 + .../OrderDraftDetailsDatagrid.tsx | 126 ++++++++++ .../OrderDraftDetailsDatagrid/datagrid.ts | 217 ++++++++++++++++++ .../OrderDraftDetailsDatagrid/index.ts | 1 + .../OrderDraftDetailsDatagrid/messages.ts | 38 +++ .../OrderDraftDetailsProducts.tsx | 138 ++--------- .../TableLine.test.tsx | 54 ----- .../OrderDraftDetailsProducts/TableLine.tsx | 165 ------------- .../TableLineAlert.tsx | 33 --- .../TableLineForm.tsx | 75 ------ .../OrderDraftDetailsProducts/messages.ts | 12 +- .../useLineAlerts.ts | 40 ---- .../OrderDraftDetailsSummary.tsx | 58 +++-- .../OrderDraftPage/OrderDraftPage.stories.tsx | 1 + .../OrderDraftPage/OrderDraftPage.tsx | 5 +- .../ExtraInfoLines.tsx | 93 ++++---- .../OrderFulfilledProductsCard.tsx | 24 +- .../OrderFulfilledProductsCard/styles.ts | 3 - .../components/OrderListDatagrid/datagrid.ts | 12 +- .../OrderProductAddDialog.tsx | 17 +- .../OrderUnfulfilledProductsCard.tsx | 33 +-- .../OrderDetails/OrderDraftDetails/index.tsx | 2 +- .../OrderDetails/OrderNormalDetails/index.tsx | 2 +- .../OrderUnconfirmedDetails/index.tsx | 2 +- .../OrderLineDiscountProvider.tsx | 12 +- .../ProductVariants/ProductVariants.tsx | 6 +- src/utils/errors/order.ts | 6 + src/utils/toFixed.test.ts | 14 ++ src/utils/toFixed.ts | 8 + 64 files changed, 1461 insertions(+), 832 deletions(-) delete mode 100644 cypress/elements/orders/order-refund.js rename src/components/Datagrid/customCells/{ => Money}/MoneyCell.tsx (73%) create mode 100644 src/components/Datagrid/customCells/Money/MoneyDiscountedCell.tsx create mode 100644 src/components/Datagrid/customCells/Money/index.ts create mode 100644 src/components/Datagrid/customCells/Money/utils.ts create mode 100644 src/orders/components/OrderDetailsDatagrid/OrderDetailsDatagrid.tsx create mode 100644 src/orders/components/OrderDetailsDatagrid/datagrid.ts create mode 100644 src/orders/components/OrderDetailsDatagrid/index.ts create mode 100644 src/orders/components/OrderDetailsDatagrid/messages.ts create mode 100644 src/orders/components/OrderDraftDetailsDatagrid/OrderDraftDetailsDatagrid.tsx create mode 100644 src/orders/components/OrderDraftDetailsDatagrid/datagrid.ts create mode 100644 src/orders/components/OrderDraftDetailsDatagrid/index.ts create mode 100644 src/orders/components/OrderDraftDetailsDatagrid/messages.ts delete mode 100644 src/orders/components/OrderDraftDetailsProducts/TableLine.test.tsx delete mode 100644 src/orders/components/OrderDraftDetailsProducts/TableLine.tsx delete mode 100644 src/orders/components/OrderDraftDetailsProducts/TableLineAlert.tsx delete mode 100644 src/orders/components/OrderDraftDetailsProducts/TableLineForm.tsx delete mode 100644 src/orders/components/OrderDraftDetailsProducts/useLineAlerts.ts create mode 100644 src/utils/toFixed.test.ts create mode 100644 src/utils/toFixed.ts diff --git a/cypress/e2e/orders/orders.js b/cypress/e2e/orders/orders.js index 0166f5fb8..393019802 100644 --- a/cypress/e2e/orders/orders.js +++ b/cypress/e2e/orders/orders.js @@ -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, + ); + }); + }); + }, + ); }); diff --git a/cypress/e2e/products/productsList/deleteProducts.js b/cypress/e2e/products/productsList/deleteProducts.js index 74547f3bf..7e5b909f0 100644 --- a/cypress/e2e/products/productsList/deleteProducts.js +++ b/cypress/e2e/products/productsList/deleteProducts.js @@ -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"); diff --git a/cypress/elements/attribute/attributes_details.js b/cypress/elements/attribute/attributes_details.js index 10e28d41a..3327a1286 100644 --- a/cypress/elements/attribute/attributes_details.js +++ b/cypress/elements/attribute/attributes_details.js @@ -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"]', diff --git a/cypress/elements/index.js b/cypress/elements/index.js index 011dcf98a..bbaf68080 100644 --- a/cypress/elements/index.js +++ b/cypress/elements/index.js @@ -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, diff --git a/cypress/elements/orders/index.js b/cypress/elements/orders/index.js index 42437d813..b9057547e 100644 --- a/cypress/elements/orders/index.js +++ b/cypress/elements/orders/index.js @@ -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"; diff --git a/cypress/elements/orders/order-refund.js b/cypress/elements/orders/order-refund.js deleted file mode 100644 index 928a5cdff..000000000 --- a/cypress/elements/orders/order-refund.js +++ /dev/null @@ -1,3 +0,0 @@ -export const ORDER_REFUND = { - productsQuantityInput: '[data-test-id*="quantity-input"]' -}; diff --git a/cypress/elements/orders/orders-selectors.js b/cypress/elements/orders/orders-selectors.js index 8c1c63b6a..2e423cff2 100644 --- a/cypress/elements/orders/orders-selectors.js +++ b/cypress/elements/orders/orders-selectors.js @@ -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']", +}; diff --git a/cypress/elements/shared/sharedElements.js b/cypress/elements/shared/sharedElements.js index d92de3351..d9068c2ef 100644 --- a/cypress/elements/shared/sharedElements.js +++ b/cypress/elements/shared/sharedElements.js @@ -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"]', diff --git a/cypress/fixtures/index.js b/cypress/fixtures/index.js index 8bcbd7c7e..140bc893d 100644 --- a/cypress/fixtures/index.js +++ b/cypress/fixtures/index.js @@ -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"; diff --git a/cypress/fixtures/messages.js b/cypress/fixtures/messages.js index 17fb592e5..1019c85f9 100644 --- a/cypress/fixtures/messages.js +++ b/cypress/fixtures/messages.js @@ -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?", }; diff --git a/cypress/support/api/utils/ordersUtils.js b/cypress/support/api/utils/ordersUtils.js index 72f70ede9..fd9786885 100644 --- a/cypress/support/api/utils/ordersUtils.js +++ b/cypress/support/api/utils/ordersUtils.js @@ -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({ diff --git a/cypress/support/pages/ordersOperations.js b/cypress/support/pages/ordersOperations.js index 1d2b2d6dd..3f76c6db0 100644 --- a/cypress/support/pages/ordersOperations.js +++ b/cypress/support/pages/ordersOperations.js @@ -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); + }); +} diff --git a/locale/defaultMessages.json b/locale/defaultMessages.json index df6ed48de..831f64e4c 100644 --- a/locale/defaultMessages.json +++ b/locale/defaultMessages.json @@ -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: authorization*, x-*" @@ -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" diff --git a/src/components/Checkbox/Checkbox.tsx b/src/components/Checkbox/Checkbox.tsx index eede62b49..74c8664ef 100644 --- a/src/components/Checkbox/Checkbox.tsx +++ b/src/components/Checkbox/Checkbox.tsx @@ -35,6 +35,7 @@ const Checkbox: React.FC = ({ helperText, error, ...props }) => { return ( <> string; } @@ -128,6 +129,7 @@ export const Datagrid: React.FC = ({ columnSelect = "none", onColumnMoved, onColumnResize, + showEmptyDatagrid = false, loading, rowAnchor, hasRowHover = false, @@ -156,6 +158,7 @@ export const Datagrid: React.FC = ({ const [selection, setSelection] = useState(); const [hoverRow, setHoverRow] = useState(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 = ({ 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 = ({ return { ...getCellContent(item, opts), - ...(changed + ...(changed && areCellsDirty ? { themeOverride: { bgCell: themeValues.colors.background.surfaceBrandSubdued, @@ -214,13 +222,15 @@ export const Datagrid: React.FC = ({ }; }, [ - 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 = ({ )} - {rowsTotal > 0 ? ( + {rowsTotal > 0 || showEmptyDatagrid ? ( <> {selection?.rows && selection?.rows.length > 0 && (
@@ -549,16 +559,6 @@ export const Datagrid: React.FC = ({ } rowMarkerWidth={48} /> - {/* FIXME: https://github.com/glideapps/glide-data-grid/issues/505 */} {hasColumnGroups && (
diff --git a/src/components/Datagrid/customCells/MoneyCell.tsx b/src/components/Datagrid/customCells/Money/MoneyCell.tsx similarity index 73% rename from src/components/Datagrid/customCells/MoneyCell.tsx rename to src/components/Datagrid/customCells/Money/MoneyCell.tsx index 4332d7301..c7b79891b 100644 --- a/src/components/Datagrid/customCells/MoneyCell.tsx +++ b/src/components/Datagrid/customCells/Money/MoneyCell.tsx @@ -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 => ({ 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: () => ({ diff --git a/src/components/Datagrid/customCells/Money/MoneyDiscountedCell.tsx b/src/components/Datagrid/customCells/Money/MoneyDiscountedCell.tsx new file mode 100644 index 000000000..bbc850d68 --- /dev/null +++ b/src/components/Datagrid/customCells/Money/MoneyDiscountedCell.tsx @@ -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; + +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 ( + 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 => ({ + 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, + }), + }); diff --git a/src/components/Datagrid/customCells/Money/index.ts b/src/components/Datagrid/customCells/Money/index.ts new file mode 100644 index 000000000..c0f322989 --- /dev/null +++ b/src/components/Datagrid/customCells/Money/index.ts @@ -0,0 +1,2 @@ +export * from "./MoneyDiscountedCell"; +export * from "./MoneyCell"; diff --git a/src/components/Datagrid/customCells/Money/utils.ts b/src/components/Datagrid/customCells/Money/utils.ts new file mode 100644 index 000000000..cbefab2bd --- /dev/null +++ b/src/components/Datagrid/customCells/Money/utils.ts @@ -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; +} diff --git a/src/components/Datagrid/customCells/cells.ts b/src/components/Datagrid/customCells/cells.ts index 9fbc786d1..4f2183390 100644 --- a/src/components/Datagrid/customCells/cells.ts +++ b/src/components/Datagrid/customCells/cells.ts @@ -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, +): 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 & diff --git a/src/components/Datagrid/customCells/useCustomCellRenderers.ts b/src/components/Datagrid/customCells/useCustomCellRenderers.ts index 116e77d8c..d07dc1d56 100644 --- a/src/components/Datagrid/customCells/useCustomCellRenderers.ts +++ b/src/components/Datagrid/customCells/useCustomCellRenderers.ts @@ -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, diff --git a/src/components/Datagrid/hooks/useColumnsDefault.ts b/src/components/Datagrid/hooks/useColumnsDefault.ts index 985451b5a..2b5516dc0 100644 --- a/src/components/Datagrid/hooks/useColumnsDefault.ts +++ b/src/components/Datagrid/hooks/useColumnsDefault.ts @@ -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; + }; } diff --git a/src/components/Datagrid/hooks/useDatagridChange.ts b/src/components/Datagrid/hooks/useDatagridChange.ts index ee4954518..c9b629cbe 100644 --- a/src/components/Datagrid/hooks/useDatagridChange.ts +++ b/src/components/Datagrid/hooks/useDatagridChange.ts @@ -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({ - updates, - removed, - added, - }); + onChange( + { + updates, + removed, + added, + }, + setMarkCellsDirty, + ); } }, - [onChange], + [onChange, setMarkCellsDirty], ); const onCellEdited = useCallback( diff --git a/src/components/Datagrid/styles.ts b/src/components/Datagrid/styles.ts index 26ad9f5dc..2964f6cb8 100644 --- a/src/components/Datagrid/styles.ts +++ b/src/components/Datagrid/styles.ts @@ -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%", }, diff --git a/src/components/PriceField/PriceField.tsx b/src/components/PriceField/PriceField.tsx index 84a4b86dc..866a01350 100644 --- a/src/components/PriceField/PriceField.tsx +++ b/src/components/PriceField/PriceField.tsx @@ -77,6 +77,7 @@ export const PriceField: React.FC = props => { fullWidth value={value} InputLabelProps={InputLabelProps} + data-test-id="price-field" InputProps={{ ...InputProps, endAdornment: currencySymbol ? ( diff --git a/src/components/RadioGroupField/RadioGroupField.tsx b/src/components/RadioGroupField/RadioGroupField.tsx index 6500a16d5..6974f1943 100644 --- a/src/components/RadioGroupField/RadioGroupField.tsx +++ b/src/components/RadioGroupField/RadioGroupField.tsx @@ -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 = props => { }} control={ { + 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 ( + + false} + menuItems={() => []} + rows={loading ? 1 : lines.length} + selectionActions={() => null} + onColumnResize={onColumnResize} + onColumnMoved={onColumnMoved} + renderColumnPicker={defaultProps => ( + undefined} + onQueryChange={picker.setQuery} + query={picker.query} + /> + )} + /> + + ); +}; diff --git a/src/orders/components/OrderDetailsDatagrid/datagrid.ts b/src/orders/components/OrderDetailsDatagrid/datagrid.ts new file mode 100644 index 000000000..0db2f853a --- /dev/null +++ b/src/orders/components/OrderDetailsDatagrid/datagrid.ts @@ -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; +}; diff --git a/src/orders/components/OrderDetailsDatagrid/index.ts b/src/orders/components/OrderDetailsDatagrid/index.ts new file mode 100644 index 000000000..11fb65691 --- /dev/null +++ b/src/orders/components/OrderDetailsDatagrid/index.ts @@ -0,0 +1 @@ +export * from "./OrderDetailsDatagrid"; diff --git a/src/orders/components/OrderDetailsDatagrid/messages.ts b/src/orders/components/OrderDetailsDatagrid/messages.ts new file mode 100644 index 000000000..a804718c4 --- /dev/null +++ b/src/orders/components/OrderDetailsDatagrid/messages.ts @@ -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", + }, +}); diff --git a/src/orders/components/OrderDetailsPage/OrderDetailsPage.stories.tsx b/src/orders/components/OrderDetailsPage/OrderDetailsPage.stories.tsx index 6bceb9ed1..b3e19c69b 100644 --- a/src/orders/components/OrderDetailsPage/OrderDetailsPage.stories.tsx +++ b/src/orders/components/OrderDetailsPage/OrderDetailsPage.stories.tsx @@ -14,7 +14,7 @@ import React from "react"; import OrderDetailsPage, { OrderDetailsPageProps } from "./OrderDetailsPage"; const props: Omit = { - disabled: false, + loading: false, onBillingAddressEdit: undefined, onTransactionAction: () => undefined, onFulfillmentApprove: () => undefined, diff --git a/src/orders/components/OrderDetailsPage/OrderDetailsPage.tsx b/src/orders/components/OrderDetailsPage/OrderDetailsPage.tsx index 91163e436..b6b6033cf 100644 --- a/src/orders/components/OrderDetailsPage/OrderDetailsPage.tsx +++ b/src/orders/components/OrderDetailsPage/OrderDetailsPage.tsx @@ -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 = props => { const { - disabled, + loading, order, shop, saveButtonBarState, @@ -162,11 +162,11 @@ const OrderDetailsPage: React.FC = 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 = props => { notAllowedToFulfillUnpaid={notAllowedToFulfillUnpaid} lines={unfulfilled} onFulfill={onOrderFulfill} + loading={loading} /> ) : ( <> = { - disabled: false, + loading: false, onBillingAddressEdit: undefined, onTransactionAction: () => undefined, onFulfillmentApprove: () => undefined, diff --git a/src/orders/components/OrderDiscountCommonModal/ModalTitle.tsx b/src/orders/components/OrderDiscountCommonModal/ModalTitle.tsx index efc4fd629..484790114 100644 --- a/src/orders/components/OrderDiscountCommonModal/ModalTitle.tsx +++ b/src/orders/components/OrderDiscountCommonModal/ModalTitle.tsx @@ -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 = ({ <>
{title} - +
{withBorder && ( <> diff --git a/src/orders/components/OrderDiscountCommonModal/OrderDiscountCommonModal.tsx b/src/orders/components/OrderDiscountCommonModal/OrderDiscountCommonModal.tsx index e63541f04..4ffd4421e 100644 --- a/src/orders/components/OrderDiscountCommonModal/OrderDiscountCommonModal.tsx +++ b/src/orders/components/OrderDiscountCommonModal/OrderDiscountCommonModal.tsx @@ -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; existingDiscount: OrderDiscountCommonInput; - dialogPlacement: PopperPlacementType; - isOpen: boolean; confirmStatus: ConfirmButtonTransitionState; removeStatus: ConfirmButtonTransitionState; } @@ -128,12 +117,9 @@ const OrderDiscountCommonModal: React.FC = ({ maxPrice = { amount: null, currency: "" }, onConfirm, modalType, - anchorRef, onClose, onRemove, existingDiscount, - dialogPlacement, - isOpen, confirmStatus, removeStatus, }) => { @@ -147,7 +133,7 @@ const OrderDiscountCommonModal: React.FC = ({ 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 = ({ const initialData = getInitialData(); - const [isValueError, setValueError] = useState(false); + const [valueErrorMsg, setValueErrorMsg] = useState(null); const [reason, setReason] = useState(initialData.reason); const [value, setValue] = useState(initialData.value); const [calculationMode, setCalculationMode] = useState( @@ -196,24 +182,36 @@ const OrderDiscountCommonModal: React.FC = ({ ) => { 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 = ({ setReason(initialData.reason); setValue(initialData.value); setCalculationMode(initialData.calculationMode); - setValueError(false); + setValueErrorMsg(null); }; useEffect(setDefaultValues, [ @@ -246,21 +244,22 @@ const OrderDiscountCommonModal: React.FC = ({ 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,70 +273,64 @@ const OrderDiscountCommonModal: React.FC = ({ calculationMode === DiscountValueTypeEnum.FIXED ? currency : "%"; const isSubmitDisabled = - !getParsedDiscountValue() || isValueError || isAmountTooLarge(); + !getParsedDiscountValue() || !!valueErrorMsg || isAmountTooLarge(); return ( - - - - - setCalculationMode(event.target.value)} - /> - - - - - {intl.formatMessage(messages.discountReasonLabel)} - - ) => - setReason(event.target.value) - } - /> - - - {existingDiscount && ( -
- - {intl.formatMessage(buttonMessages.remove)} - -
- )} -
-
-
+ + + + setCalculationMode(event.target.value)} + /> + + + + + {intl.formatMessage(messages.discountReasonLabel)} + + ) => + setReason(event.target.value) + } + /> + + + {existingDiscount && ( +
+ + {intl.formatMessage(buttonMessages.remove)} + +
+ )} +
+
); }; diff --git a/src/orders/components/OrderDraftDetails/OrderDraftDetails.tsx b/src/orders/components/OrderDraftDetails/OrderDraftDetails.tsx index 352a438bc..4f5799f22 100644 --- a/src/orders/components/OrderDraftDetails/OrderDraftDetails.tsx +++ b/src/orders/components/OrderDraftDetails/OrderDraftDetails.tsx @@ -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 = ({ order, channelUsabilityData, errors, + loading, onOrderLineAdd, onOrderLineChange, onOrderLineRemove, @@ -70,6 +72,7 @@ const OrderDraftDetails: React.FC = ({ diff --git a/src/orders/components/OrderDraftDetailsDatagrid/OrderDraftDetailsDatagrid.tsx b/src/orders/components/OrderDraftDetailsDatagrid/OrderDraftDetailsDatagrid.tsx new file mode 100644 index 000000000..57b1131f3 --- /dev/null +++ b/src/orders/components/OrderDraftDetailsDatagrid/OrderDraftDetailsDatagrid.tsx @@ -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: , + 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 ( + + (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 => ( + undefined} + onQueryChange={picker.setQuery} + query={picker.query} + /> + )} + onChange={handleDatagridChange} + /> + + ); +}; diff --git a/src/orders/components/OrderDraftDetailsDatagrid/datagrid.ts b/src/orders/components/OrderDraftDetailsDatagrid/datagrid.ts new file mode 100644 index 000000000..6f9327079 --- /dev/null +++ b/src/orders/components/OrderDraftDetailsDatagrid/datagrid.ts @@ -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)); +} diff --git a/src/orders/components/OrderDraftDetailsDatagrid/index.ts b/src/orders/components/OrderDraftDetailsDatagrid/index.ts new file mode 100644 index 000000000..31312d7ba --- /dev/null +++ b/src/orders/components/OrderDraftDetailsDatagrid/index.ts @@ -0,0 +1 @@ +export * from "./OrderDraftDetailsDatagrid"; diff --git a/src/orders/components/OrderDraftDetailsDatagrid/messages.ts b/src/orders/components/OrderDraftDetailsDatagrid/messages.ts new file mode 100644 index 000000000..f51847064 --- /dev/null +++ b/src/orders/components/OrderDraftDetailsDatagrid/messages.ts @@ -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", + }, +}); diff --git a/src/orders/components/OrderDraftDetailsProducts/OrderDraftDetailsProducts.tsx b/src/orders/components/OrderDraftDetailsProducts/OrderDraftDetailsProducts.tsx index 58b9f85cd..e0dc08258 100644 --- a/src/orders/components/OrderDraftDetailsProducts/OrderDraftDetailsProducts.tsx +++ b/src/orders/components/OrderDraftDetailsProducts/OrderDraftDetailsProducts.tsx @@ -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 = ({ + 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 ( - - {!!lines.length && ( - - - - - - - - - - - - - - - - - - - - )} - - {!!lines.length ? ( - renderCollection(lines, line => ( - - {( - orderLineDiscountProps: OrderLineDiscountContextConsumerProps, - ) => ( - - error.orderLines?.some(id => id === line.id), - )} - onOrderLineChange={onOrderLineChange} - onOrderLineRemove={onOrderLineRemove} - /> - )} - - )) - ) : ( - <> - - - - {!!formErrors.length && ( - - {getOrderErrorMessage(formErrors[0], intl)} - - )} - - - - )} - - + ); }; + OrderDraftDetailsProducts.displayName = "OrderDraftDetailsProducts"; export default OrderDraftDetailsProducts; diff --git a/src/orders/components/OrderDraftDetailsProducts/TableLine.test.tsx b/src/orders/components/OrderDraftDetailsProducts/TableLine.test.tsx deleted file mode 100644 index c9f970ad9..000000000 --- a/src/orders/components/OrderDraftDetailsProducts/TableLine.test.tsx +++ /dev/null @@ -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(); - - // Assert - const tableLine = screen.getByTestId(`table-line-total-${mockedLine.id}`); - expect(tableLine).toHaveTextContent( - mockedLine.totalPrice.gross.currency.toString(), - ); - }); -}); diff --git a/src/orders/components/OrderDraftDetailsProducts/TableLine.tsx b/src/orders/components/OrderDraftDetailsProducts/TableLine.tsx deleted file mode 100644 index aa5e57a65..000000000 --- a/src/orders/components/OrderDraftDetailsProducts/TableLine.tsx +++ /dev/null @@ -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 = ({ - line, - error, - onOrderLineChange, - onOrderLineRemove, - orderLineDiscount, - addOrderLineDiscount, - removeOrderLineDiscount, - openDialog, - closeDialog, - orderLineDiscountRemoveStatus, - isDialogOpen, - totalDiscountedPrice, - unitUndiscountedPrice, - unitDiscountedPrice, - orderLineDiscountUpdateStatus, -}) => { - const classes = useStyles(); - const popperAnchorRef = useRef(null); - const { id, thumbnail, productName, productSku } = line; - - const alerts = useLineAlerts({ - line, - error, - }); - - const getUnitPriceLabel = () => { - const money = ; - - if (!!orderLineDiscount) { - return ( - <> - {money} - - - - - ); - } - - return {money}; - }; - - return ( - - - {!!alerts.length && ( - - )} - - thumbnail.url)} - > - {productName} - {productSku} - - - - - - {getUnitPriceLabel()} - - - - - - - onOrderLineRemove(id)}> - - - - - ); -}; - -export default TableLine; diff --git a/src/orders/components/OrderDraftDetailsProducts/TableLineAlert.tsx b/src/orders/components/OrderDraftDetailsProducts/TableLineAlert.tsx deleted file mode 100644 index 63147e71d..000000000 --- a/src/orders/components/OrderDraftDetailsProducts/TableLineAlert.tsx +++ /dev/null @@ -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 = ({ alerts, variant }) => { - if (!alerts.length) { - return null; - } - - const title = ; - - return ( - - - - - - - - - {title} - - - ); -}; -export default TableLineAlert; diff --git a/src/orders/components/OrderDraftDetailsProducts/TableLineForm.tsx b/src/orders/components/OrderDraftDetailsProducts/TableLineForm.tsx deleted file mode 100644 index 436175ad6..000000000 --- a/src/orders/components/OrderDraftDetailsProducts/TableLineForm.tsx +++ /dev/null @@ -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 = ({ - 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 ( -
handleSubmit(id, data)}> - {({ change, data, submit, set }) => { - const handleQuantityChange = createNonNegativeValueChangeHandler( - change, - ); - - return ( - - {debounce => ( - { - if (data.quantity < 1) { - set({ quantity: 1 }); - } - submit(); - }} - inputProps={{ min: 1 }} - /> - )} - - ); - }} -
- ); -}; - -export default TableLineForm; diff --git a/src/orders/components/OrderDraftDetailsProducts/messages.ts b/src/orders/components/OrderDraftDetailsProducts/messages.ts index bc5b3328a..8413b1082 100644 --- a/src/orders/components/OrderDraftDetailsProducts/messages.ts +++ b/src/orders/components/OrderDraftDetailsProducts/messages.ts @@ -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", }, }); diff --git a/src/orders/components/OrderDraftDetailsProducts/useLineAlerts.ts b/src/orders/components/OrderDraftDetailsProducts/useLineAlerts.ts deleted file mode 100644 index c330ebffb..000000000 --- a/src/orders/components/OrderDraftDetailsProducts/useLineAlerts.ts +++ /dev/null @@ -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; diff --git a/src/orders/components/OrderDraftDetailsSummary/OrderDraftDetailsSummary.tsx b/src/orders/components/OrderDraftDetailsSummary/OrderDraftDetailsSummary.tsx index adf6df24a..4d76fe542 100644 --- a/src/orders/components/OrderDraftDetailsSummary/OrderDraftDetailsSummary.tsx +++ b/src/orders/components/OrderDraftDetailsSummary/OrderDraftDetailsSummary.tsx @@ -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(null); - if (!order) { return null; } @@ -179,36 +178,47 @@ const OrderDraftDetailsSummary: React.FC< return ( - + - + - + - + - +
- - {intl.formatMessage(discountTitle)} - - + + + + + + + + + + {getOrderDiscountLabel(orderDiscount)}
{intl.formatMessage(messages.subtotal)}
{hasShippingMethods && getShippingMethodComponent()} @@ -230,13 +240,13 @@ const OrderDraftDetailsSummary: React.FC< )}
{intl.formatMessage(messages.taxes)}
{intl.formatMessage(messages.total)} diff --git a/src/orders/components/OrderDraftPage/OrderDraftPage.stories.tsx b/src/orders/components/OrderDraftPage/OrderDraftPage.stories.tsx index 257ceb79c..ba31f72e2 100644 --- a/src/orders/components/OrderDraftPage/OrderDraftPage.stories.tsx +++ b/src/orders/components/OrderDraftPage/OrderDraftPage.stories.tsx @@ -43,6 +43,7 @@ const order = draftOrder(placeholderImage); const props: Omit = { ...fetchMoreProps, + loading: false, disabled: false, fetchUsers: () => undefined, onBillingAddressEdit: undefined, diff --git a/src/orders/components/OrderDraftPage/OrderDraftPage.tsx b/src/orders/components/OrderDraftPage/OrderDraftPage.tsx index 497dea73a..01cef04ba 100644 --- a/src/orders/components/OrderDraftPage/OrderDraftPage.tsx +++ b/src/orders/components/OrderDraftPage/OrderDraftPage.tsx @@ -53,7 +53,7 @@ export interface OrderDraftPageProps extends FetchMoreProps { const OrderDraftPage: React.FC = props => { const { - disabled, + loading, fetchUsers, hasMore, saveButtonBarState, @@ -120,6 +120,7 @@ const OrderDraftPage: React.FC = 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 = props => { navigate(orderDraftListUrl())} onSubmit={onDraftFinalize} labels={{ diff --git a/src/orders/components/OrderFulfilledProductsCard/ExtraInfoLines.tsx b/src/orders/components/OrderFulfilledProductsCard/ExtraInfoLines.tsx index 87c6bd3f4..662fcdf73 100644 --- a/src/orders/components/OrderFulfilledProductsCard/ExtraInfoLines.tsx +++ b/src/orders/components/OrderFulfilledProductsCard/ExtraInfoLines.tsx @@ -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,48 +24,51 @@ const ExtraInfoLines: React.FC = ({ fulfillment }) => { const { warehouse, trackingNumber, status } = fulfillment; return ( - - - - {warehouse && ( - <> - {intl.formatMessage( - status === FulfillmentStatus.RETURNED - ? extraInfoMessages.restocked - : extraInfoMessages.fulfilled, - )} - - {getStringOrPlaceholder(warehouse?.name)} - - - )} - - - {trackingNumber && ( - - {trackingNumber} - - ), - }} - /> - )} - - - + + + {warehouse && ( + <> + {intl.formatMessage( + status === FulfillmentStatus.RETURNED + ? extraInfoMessages.restocked + : extraInfoMessages.fulfilled, + )} + + {getStringOrPlaceholder(warehouse?.name)} + + + )} + + + {trackingNumber && ( + + {trackingNumber} + + ), + }} + /> + )} + + ); }; diff --git a/src/orders/components/OrderFulfilledProductsCard/OrderFulfilledProductsCard.tsx b/src/orders/components/OrderFulfilledProductsCard/OrderFulfilledProductsCard.tsx index e519a9951..8383295dc 100644 --- a/src/orders/components/OrderFulfilledProductsCard/OrderFulfilledProductsCard.tsx +++ b/src/orders/components/OrderFulfilledProductsCard/OrderFulfilledProductsCard.tsx @@ -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< } /> - - - - {renderCollection(getLines(), line => ( - - ))} - - - + + { 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[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("-"); diff --git a/src/orders/components/OrderProductAddDialog/OrderProductAddDialog.tsx b/src/orders/components/OrderProductAddDialog/OrderProductAddDialog.tsx index 0c11b868d..882e59d29 100644 --- a/src/orders/components/OrderProductAddDialog/OrderProductAddDialog.tsx +++ b/src/orders/components/OrderProductAddDialog/OrderProductAddDialog.tsx @@ -169,12 +169,12 @@ const OrderProductAddDialog: React.FC = props => { scrollableTarget={scrollableTargetId} > - + {renderCollection( productChoicesWithValidVariants, (product, productIndex) => ( - + = props => { className={classes.avatar} thumbnail={maybe(() => product.thumbnail.url)} /> - + {maybe(() => product.name)} {maybe(() => product.variants, []) .filter(isValidVariant) .map((variant, variantIndex) => ( - + = props => { )} - + diff --git a/src/orders/components/OrderUnfulfilledProductsCard/OrderUnfulfilledProductsCard.tsx b/src/orders/components/OrderUnfulfilledProductsCard/OrderUnfulfilledProductsCard.tsx index e9550e48f..193b1773a 100644 --- a/src/orders/components/OrderUnfulfilledProductsCard/OrderUnfulfilledProductsCard.tsx +++ b/src/orders/components/OrderUnfulfilledProductsCard/OrderUnfulfilledProductsCard.tsx @@ -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} /> - - - - {renderCollection(lines, line => ( - - ))} - - + {showFulfillmentAction && (