Order details datagrid (#3325)
* Show available channels * Add price and updatedAt columns * Fix sorting, only sort on selected columns * Sort by channel * Allow delete name and product type * Fix show not product found * Extract mssages * Product datagrid custom column picker * Column picker in data grid in dirty hack for bug * fix storybook props * Restore Datagrid defalt column picker with custom render * Add sort by attributes * Use datagrid loading cells * Fix product searching * Show attributes before last updated * Readonly all fields in datagrid * Fix creating new datagrid row * Remove add new procut button from datagrid * Show only active sorted column * Temp fix for column filter * Fix column mismatch * Add comments and spred props to ColumnPicker * Cleanup * Update avatar size and styles * On row click with hover on row styles * Use new theme * Change placeholder image * Draw rounded image with border * Readonly product datagrid * Use new theme colors in datagrid * Add vertical borders control to datagrid * Add empty column to add padding * Add coursor to datagrid * Restore vertical borders, fix cursor pointer * Add custom freezed column * Initial tooltip for column * Move tooltip to datagrid * Adjust datagrid colors style, add possibility to select column * Change datagrid selected cells colors * Fix typo and extract messages * Base order datagrid * Cleanup Datagrid component * Cleanup and code refactor * Remove cursor pointer props from readonlyCell * Use money cell for total column * Add custom cell renderers and fix types * Simple tags implemenrtion for status and payment col * Add colors from theme * Make tagCell more dynamic * Refactor Datagrid file structure * Add loading indicators * Selecting column without cells in readonly * Add sort icons to orders list * Refactor after CR. fix typos * Change color of selected colum cell on hover * Improve selected header text contrast * Move useColumnPickerColumns to hooks dir with tests * Add less padding to column picker button * Remove double border top * Fix cursor pointer for tagCell and moneyCell * useGetCellContent hook * On loading show only one row * Add missing darkmod color for warning tag * Refactor columns in datagrid * Add new macaw theme provider to storybook * Fix passing props in datagrid * Trigger deployment * Fix column picker in products * useDatagridColumns * Fix one more time * Add column picker with default columns * Change color for selected header change to textBrandDefault * Remove unused code, move attributes colums as last * Cleanup useDatagridColumns * Improve DatagridProps * Static datagrid for products (#3144) * Migrate top nav of product list page to new MacawUI (#3290) * feat: migrate top nav of product page * feat: add proper deprecation links --------- Co-authored-by: Michał Droń <dron.official@yahoo.com> * Datagrin on order details intial * Adjust ExtraInfoLines * Remove padding on datagrid card content * Remove datagrid card paddding (#3310) * Disable column icon when no rows in orders * Datagrid row hover show only when readonly and row clickable * Implement card view for product list (#3292) * Add temporary view switcher * Add basic product tile view * Bump macaw-ui * Add ellipsis * Bump macaw-ui * Add status dot & fix non-rectangular thumbnails * Bump macaw-ui * Add variable size placeholder icon * Improve loader * Fix top nav menu key error * Add pagination * Add unit tests * Extract messages * Extract status color to function * CR Refactor * Hold product view state in local storage (#3315) * Remember view state for product list * Use util status function for status dots * Datagrin in orderDraftDetails * Remove not used components * Fix for empty column and hover in datagrid for product (#3324) * Remove datagrid card paddding (#3310) * Fix for empty column and hover in datagrid for product (#3324) * Use themeValues from macaw (#3326) * Upgrade macaw * Use themeValues * Use themeValues from macaw (#3326) * Upgrade macaw * Use themeValues * Add empty column from datagrid, improve theme types * Use theme type from typeof * Use empty column and themeValues * Filter empty column from default * New product header (#3346) * Extraxt messages * Remve title left padding * Fix switching view * Add margin right to nav button * Improve view switch * Update switch view icons * Add spacing to switch * Add more space * Add new filterbar to order list * Code refactor and tests * Refactor OrderDraftDetailsDatagrid * Extract messages * Refactor OrderDraftDetailsDatagrid * Update alert messages * Extract messages * Write unit tests * Improve switch component * Overwrite Pill styles * Common method to get status color for pills * Local Pill component POC * Add ThemeProvider to test wrapper * Extract messages * Refactor Pill * Fix Pill path * Fix tests mocks * Remove scrollbar and border bottom * Add custom border to to datagrid * Fix borders * Fix border bottom * Refactor and cleanup * Remove not needed selectionActions code * Move logic code t misc * Fix scrollbar and zindex datagrid borders * Fix product tiles condition * Use utils functions, remove not used code * Refactor to hooks * Loading prps instead of disabled * use getStatusColor * Move getMenuItems to separate function * Fix loading props * Use empty colum hook in OrderDetailsDatagrid * Fix empty column when save column change * Fix bottom line in layout overlap * Show moneCell with discounted price * Make quantity ediable in order draft datagrid * Readonly datagrid cells * Update onyl when column is quantity * Fix message * Keep first column in datagrid not removable * Fix for not existing column * Add loader over datagrid, fix problem with border top when empty text in variants * Fix error color and change color in datagrid * Use formatMoneyAmount * Fix remove order draft product with discount * Extract messages * Add product sku to order draft details datagrid * Fix loading state and change cell color * Add MoneyDiscuntedCell * Use MoneyDiscuntedCell in order draft details datagrid * Add trash bin icon * Restor discount modal for order draft summary * Fix problem with deleting quantity * Improve await for promises and handler zero quantity error * Fix column order issue * Add discount modal box shadow * Allow decimal as percentage value for discount * Fix max fixed value * Remove double border * Fix z-index issue on discount modal * Remove padding on order details datagrid * Add proper error message to common discount modal * Fix is submit disabled * Move status as last column in datagrid * Add padding to money discount cell editor * Make quantity column smaller * Fix recalculating disount value * Fix calculate change discount type * Store calculated value without triming decimal, trim decimal in input * Refactor money cells * markCellsDirty rename to areCellsDirty * Remove discount from MoneyCell * Use const to store row height in discount editor * Fix copy in discount modal * Remove past on money discount cell * Remove locale in product varaints * Fix nullable sku * Extract messages * Fix keeping always first column * Remove padding on tracking info * Fix story * Fix render 0 money amount * Fix displaying not empty string money * adding new tests: add new product, change quantity, add inline discount, delete product for grid - on orders details view (#3652) * adding new e2e for grid on orders details view * merging conflicts fix - and adding new TC numbers to new tests * trigger tests * failing tests fixes --------- Co-authored-by: Michał Droń <dron.official@yahoo.com> Co-authored-by: Krzysztof Żuraw <9116238+krzysztofzuraw@users.noreply.github.com> Co-authored-by: Michał Droń <droniu@droniu.dev> Co-authored-by: wojteknowacki <124166231+wojteknowacki@users.noreply.github.com> Co-authored-by: wojteknowacki <wojciech.nowacki@saleor.io>
This commit is contained in:
parent
757833ca57
commit
55337b5998
64 changed files with 1461 additions and 832 deletions
|
@ -3,10 +3,13 @@
|
||||||
|
|
||||||
import faker from "faker";
|
import faker from "faker";
|
||||||
|
|
||||||
import { ORDER_REFUND } from "../../elements/orders/order-refund";
|
import {
|
||||||
import { ORDERS_SELECTORS } from "../../elements/orders/orders-selectors";
|
BUTTON_SELECTORS,
|
||||||
import { BUTTON_SELECTORS } from "../../elements/shared/button-selectors";
|
ORDER_GRANT_REFUND,
|
||||||
import { SHARED_ELEMENTS } from "../../elements/shared/sharedElements";
|
ORDERS_SELECTORS,
|
||||||
|
SHARED_ELEMENTS,
|
||||||
|
} from "../../elements/";
|
||||||
|
import { MESSAGES } from "../../fixtures";
|
||||||
import { urlList } from "../../fixtures/urlList";
|
import { urlList } from "../../fixtures/urlList";
|
||||||
import { ONE_PERMISSION_USERS } from "../../fixtures/users";
|
import { ONE_PERMISSION_USERS } from "../../fixtures/users";
|
||||||
import {
|
import {
|
||||||
|
@ -22,6 +25,7 @@ import {
|
||||||
createFulfilledOrder,
|
createFulfilledOrder,
|
||||||
createOrder,
|
createOrder,
|
||||||
createReadyToFulfillOrder,
|
createReadyToFulfillOrder,
|
||||||
|
createUnconfirmedOrder,
|
||||||
} from "../../support/api/utils/ordersUtils";
|
} from "../../support/api/utils/ordersUtils";
|
||||||
import * as productsUtils from "../../support/api/utils/products/productsUtils";
|
import * as productsUtils from "../../support/api/utils/products/productsUtils";
|
||||||
import {
|
import {
|
||||||
|
@ -34,6 +38,12 @@ import {
|
||||||
} from "../../support/api/utils/taxesUtils";
|
} from "../../support/api/utils/taxesUtils";
|
||||||
import { selectChannelInPicker } from "../../support/pages/channelsPage";
|
import { selectChannelInPicker } from "../../support/pages/channelsPage";
|
||||||
import { finalizeDraftOrder } from "../../support/pages/draftOrderPage";
|
import { finalizeDraftOrder } from "../../support/pages/draftOrderPage";
|
||||||
|
import {
|
||||||
|
addNewProductToOrder,
|
||||||
|
applyFixedLineDiscountForProduct,
|
||||||
|
changeQuantityOfProducts,
|
||||||
|
deleteProductFromGridTableOnIndex,
|
||||||
|
} from "../../support/pages/ordersOperations";
|
||||||
|
|
||||||
describe("Orders", () => {
|
describe("Orders", () => {
|
||||||
const startsWith = "CyOrders-";
|
const startsWith = "CyOrders-";
|
||||||
|
@ -47,6 +57,9 @@ describe("Orders", () => {
|
||||||
let address;
|
let address;
|
||||||
let taxClass;
|
let taxClass;
|
||||||
|
|
||||||
|
const shippingPrice = 2;
|
||||||
|
const variantPrice = 1;
|
||||||
|
|
||||||
before(() => {
|
before(() => {
|
||||||
cy.clearSessionData().loginUserViaRequest();
|
cy.clearSessionData().loginUserViaRequest();
|
||||||
deleteCustomersStartsWith(startsWith);
|
deleteCustomersStartsWith(startsWith);
|
||||||
|
@ -73,6 +86,7 @@ describe("Orders", () => {
|
||||||
createShipping({
|
createShipping({
|
||||||
channelId: defaultChannel.id,
|
channelId: defaultChannel.id,
|
||||||
name: randomName,
|
name: randomName,
|
||||||
|
price: shippingPrice,
|
||||||
address,
|
address,
|
||||||
taxClassId: taxClass.id,
|
taxClassId: taxClass.id,
|
||||||
});
|
});
|
||||||
|
@ -95,6 +109,7 @@ describe("Orders", () => {
|
||||||
productsUtils.createProductInChannel({
|
productsUtils.createProductInChannel({
|
||||||
name: randomName,
|
name: randomName,
|
||||||
channelId: defaultChannel.id,
|
channelId: defaultChannel.id,
|
||||||
|
price: variantPrice,
|
||||||
warehouseId: warehouse.id,
|
warehouseId: warehouse.id,
|
||||||
productTypeId: productTypeResp.id,
|
productTypeId: productTypeResp.id,
|
||||||
attributeId: attributeResp.id,
|
attributeId: attributeResp.id,
|
||||||
|
@ -206,7 +221,7 @@ describe("Orders", () => {
|
||||||
cy.visit(urlList.orders + `${order.id}`);
|
cy.visit(urlList.orders + `${order.id}`);
|
||||||
cy.get(ORDERS_SELECTORS.refundButton)
|
cy.get(ORDERS_SELECTORS.refundButton)
|
||||||
.click()
|
.click()
|
||||||
.get(ORDER_REFUND.productsQuantityInput)
|
.get(ORDER_GRANT_REFUND.productsQuantityInput)
|
||||||
.type("1")
|
.type("1")
|
||||||
.addAliasToGraphRequest("OrderFulfillmentRefundProducts");
|
.addAliasToGraphRequest("OrderFulfillmentRefundProducts");
|
||||||
cy.get(BUTTON_SELECTORS.submit)
|
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,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
import faker from "faker";
|
import faker from "faker";
|
||||||
|
|
||||||
import { BUTTON_SELECTORS, PRODUCTS_LIST } from "../../../elements/";
|
import { BUTTON_SELECTORS, PRODUCTS_LIST } from "../../../elements/";
|
||||||
import { DIALOGS_MESSAGES } from "../../../fixtures/";
|
import { MESSAGES } from "../../../fixtures/";
|
||||||
import { urlList } from "../../../fixtures/urlList";
|
import { urlList } from "../../../fixtures/urlList";
|
||||||
import { getDefaultChannel, productsUtils } from "../../../support/api/utils/";
|
import { getDefaultChannel, productsUtils } from "../../../support/api/utils/";
|
||||||
import { ensureCanvasStatic } from "../../../support/customCommands/sharedElementsOperations/canvas";
|
import { ensureCanvasStatic } from "../../../support/customCommands/sharedElementsOperations/canvas";
|
||||||
|
@ -55,9 +55,7 @@ describe("Test for deleting products", () => {
|
||||||
cy.clickGridCell(0, 0);
|
cy.clickGridCell(0, 0);
|
||||||
cy.clickGridCell(0, 1);
|
cy.clickGridCell(0, 1);
|
||||||
cy.clickOnElement(BUTTON_SELECTORS.deleteProductsButton);
|
cy.clickOnElement(BUTTON_SELECTORS.deleteProductsButton);
|
||||||
cy.contains(DIALOGS_MESSAGES.confirmProductsDeletion).should(
|
cy.contains(MESSAGES.confirmProductsDeletion).should("be.visible");
|
||||||
"be.visible",
|
|
||||||
);
|
|
||||||
cy.addAliasToGraphRequest("productBulkDelete")
|
cy.addAliasToGraphRequest("productBulkDelete")
|
||||||
.clickSubmitButton()
|
.clickSubmitButton()
|
||||||
.waitForRequestAndCheckIfNoErrors("@productBulkDelete");
|
.waitForRequestAndCheckIfNoErrors("@productBulkDelete");
|
||||||
|
|
|
@ -24,7 +24,7 @@ export const ATTRIBUTES_DETAILS = {
|
||||||
entityTypeOptions: {
|
entityTypeOptions: {
|
||||||
PRODUCT: '[data-test-id="select-field-option-PRODUCT"]',
|
PRODUCT: '[data-test-id="select-field-option-PRODUCT"]',
|
||||||
PRODUCT_VARIANT: '[data-test-id="select-field-option-PRODUCT_VARIANT"]',
|
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"]',
|
selectUnitCheckbox: '[name="selectUnit"]',
|
||||||
unitSystemSelect: '[data-test-id="unit-system"]',
|
unitSystemSelect: '[data-test-id="unit-system"]',
|
||||||
|
|
|
@ -22,6 +22,8 @@ export { SALES_SELECTORS, VOUCHERS_SELECTORS } from "./discounts";
|
||||||
export { HOMEPAGE_SELECTORS } from "./homePage/homePage-selectors";
|
export { HOMEPAGE_SELECTORS } from "./homePage/homePage-selectors";
|
||||||
export { PAGINATION } from "./navigation";
|
export { PAGINATION } from "./navigation";
|
||||||
export {
|
export {
|
||||||
|
ADD_PRODUCT_TO_ORDER_DIALOG,
|
||||||
|
DRAFT_ORDER_SELECTORS,
|
||||||
DRAFT_ORDERS_LIST_SELECTORS,
|
DRAFT_ORDERS_LIST_SELECTORS,
|
||||||
ORDER_GRANT_REFUND,
|
ORDER_GRANT_REFUND,
|
||||||
ORDER_TRANSACTION_CREATE,
|
ORDER_TRANSACTION_CREATE,
|
||||||
|
|
|
@ -1,4 +1,9 @@
|
||||||
export { DRAFT_ORDERS_LIST_SELECTORS } from "./draft-orders-list-selectors";
|
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 { ORDER_GRANT_REFUND } from "./order-grant-refund";
|
||||||
export { ORDERS_SELECTORS } from "./orders-selectors";
|
|
||||||
export { ORDER_TRANSACTION_CREATE } from "./transaction-selectors";
|
export { ORDER_TRANSACTION_CREATE } from "./transaction-selectors";
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
export const ORDER_REFUND = {
|
|
||||||
productsQuantityInput: '[data-test-id*="quantity-input"]'
|
|
||||||
};
|
|
|
@ -9,10 +9,26 @@ export const ORDERS_SELECTORS = {
|
||||||
orderFulfillmentFrame: "[data-test-id='order-fulfillment']",
|
orderFulfillmentFrame: "[data-test-id='order-fulfillment']",
|
||||||
refundButton: '[data-test-id="refund-button"]',
|
refundButton: '[data-test-id="refund-button"]',
|
||||||
fulfillMenuButton: '[data-test-id="fulfill-menu"]',
|
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"]',
|
markAsPaidButton: '[data-test-id="markAsPaidButton"]',
|
||||||
|
grantRefundButton: '[data-test-id="grantRefundButton"]',
|
||||||
transactionReferenceInput: '[data-test-id="transaction-reference-input"]',
|
transactionReferenceInput: '[data-test-id="transaction-reference-input"]',
|
||||||
orderTransactionsList: '[data-test-id="orderTransactionsList"]',
|
orderTransactionsList: '[data-test-id="orderTransactionsList"]',
|
||||||
grantRefundButton: '[data-test-id="grantRefundButton"]',
|
|
||||||
captureManualTransactionButton:
|
captureManualTransactionButton:
|
||||||
'[data-test-id="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']",
|
||||||
|
};
|
||||||
|
|
|
@ -16,6 +16,7 @@ export const SHARED_ELEMENTS = {
|
||||||
selectOption: '[data-test-id*="select-field-option"]',
|
selectOption: '[data-test-id*="select-field-option"]',
|
||||||
svgImage: "svg",
|
svgImage: "svg",
|
||||||
fileInput: 'input[type="file"]',
|
fileInput: 'input[type="file"]',
|
||||||
|
pageHeader: '[data-test-id="page-header"]',
|
||||||
urlInput: 'input[type="url"]',
|
urlInput: 'input[type="url"]',
|
||||||
richTextEditor: {
|
richTextEditor: {
|
||||||
loader: '[class*="codex-editor__loader"]',
|
loader: '[class*="codex-editor__loader"]',
|
||||||
|
|
|
@ -2,4 +2,4 @@ export { bodyMockHomePage } from "./bodyMocks";
|
||||||
export { orderDraftCreateDemoResponse } from "./errors/demo/orderDratCreate";
|
export { orderDraftCreateDemoResponse } from "./errors/demo/orderDratCreate";
|
||||||
export { urlList } from "./urlList";
|
export { urlList } from "./urlList";
|
||||||
export { ONE_PERMISSION_USERS } from "./users";
|
export { ONE_PERMISSION_USERS } from "./users";
|
||||||
export { DIALOGS_MESSAGES } from "./messages";
|
export { MESSAGES } from "./messages";
|
||||||
|
|
|
@ -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?",
|
confirmProductsDeletion: "Are you sure you want to delete 2 products?",
|
||||||
};
|
};
|
||||||
|
|
|
@ -208,6 +208,28 @@ function assignVariantsToOrder(order, variantsList) {
|
||||||
orderRequest.addProductToOrder(order.id, variantElement.id);
|
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) {
|
export function addPayment(checkoutId) {
|
||||||
return checkoutRequest.addPayment({
|
return checkoutRequest.addPayment({
|
||||||
|
|
|
@ -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) {
|
export function pickAndSelectChannelOnCreateOrderFormByIndex(index) {
|
||||||
cy.get(ORDERS_SELECTORS.createOrderButton)
|
cy.get(ORDERS_SELECTORS.createOrderButton)
|
||||||
|
@ -9,3 +15,57 @@ export function pickAndSelectChannelOnCreateOrderFormByIndex(index) {
|
||||||
.eq(index)
|
.eq(index)
|
||||||
.click();
|
.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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
@ -2134,6 +2134,9 @@
|
||||||
"context": "checkbox label",
|
"context": "checkbox label",
|
||||||
"string": "Restrict order value"
|
"string": "Restrict order value"
|
||||||
},
|
},
|
||||||
|
"DhFqJF": {
|
||||||
|
"string": "Edit discount"
|
||||||
|
},
|
||||||
"Dhherd": {
|
"Dhherd": {
|
||||||
"context": "dialog header, title",
|
"context": "dialog header, title",
|
||||||
"string": "Invoice Generation"
|
"string": "Invoice Generation"
|
||||||
|
@ -3237,6 +3240,9 @@
|
||||||
"context": "cancelled fulfillment, section header",
|
"context": "cancelled fulfillment, section header",
|
||||||
"string": "Refunded and Returned"
|
"string": "Refunded and Returned"
|
||||||
},
|
},
|
||||||
|
"LKD6fB": {
|
||||||
|
"string": "Remove product"
|
||||||
|
},
|
||||||
"LKoIB1": {
|
"LKoIB1": {
|
||||||
"string": "Add search engine title and description to make this product easier to find"
|
"string": "Add search engine title and description to make this product easier to find"
|
||||||
},
|
},
|
||||||
|
@ -3647,10 +3653,6 @@
|
||||||
"context": "input label",
|
"context": "input label",
|
||||||
"string": "Stock reservation for authenticated user (in minutes)"
|
"string": "Stock reservation for authenticated user (in minutes)"
|
||||||
},
|
},
|
||||||
"Oad+ES": {
|
|
||||||
"context": "alert message",
|
|
||||||
"string": "This product is not published in this channel."
|
|
||||||
},
|
|
||||||
"ObRk1O": {
|
"ObRk1O": {
|
||||||
"string": "If this option is disabled, discount will be counted for every eligible product"
|
"string": "If this option is disabled, discount will be counted for every eligible product"
|
||||||
},
|
},
|
||||||
|
@ -3924,6 +3926,10 @@
|
||||||
"context": "staff added type order discount",
|
"context": "staff added type order discount",
|
||||||
"string": "Staff added"
|
"string": "Staff added"
|
||||||
},
|
},
|
||||||
|
"QL4a0Z": {
|
||||||
|
"context": "alert message",
|
||||||
|
"string": "Not published in this channel"
|
||||||
|
},
|
||||||
"QLVddq": {
|
"QLVddq": {
|
||||||
"context": "button",
|
"context": "button",
|
||||||
"string": "Create customer"
|
"string": "Create customer"
|
||||||
|
@ -4428,9 +4434,6 @@
|
||||||
"context": "searchbar placeholder",
|
"context": "searchbar placeholder",
|
||||||
"string": "Country"
|
"string": "Country"
|
||||||
},
|
},
|
||||||
"UD7/q8": {
|
|
||||||
"string": "No Products added to Order"
|
|
||||||
},
|
|
||||||
"UJnqdm": {
|
"UJnqdm": {
|
||||||
"context": "dialog header",
|
"context": "dialog header",
|
||||||
"string": "Unassign Attribute From Product Type"
|
"string": "Unassign Attribute From Product Type"
|
||||||
|
@ -4618,6 +4621,10 @@
|
||||||
"context": "error message",
|
"context": "error message",
|
||||||
"string": "Default shipping zone already exists"
|
"string": "Default shipping zone already exists"
|
||||||
},
|
},
|
||||||
|
"VIdXPy": {
|
||||||
|
"context": "value input helper text",
|
||||||
|
"string": "Cannot be higher than the price"
|
||||||
|
},
|
||||||
"VKWPBf": {
|
"VKWPBf": {
|
||||||
"context": "dialog header",
|
"context": "dialog header",
|
||||||
"string": "Delete Staff User Avatar"
|
"string": "Delete Staff User Avatar"
|
||||||
|
@ -5153,6 +5160,9 @@
|
||||||
"Yy/yDL": {
|
"Yy/yDL": {
|
||||||
"string": "Reset password"
|
"string": "Reset password"
|
||||||
},
|
},
|
||||||
|
"YzLUXA": {
|
||||||
|
"string": "Ensure this value is greater than 0."
|
||||||
|
},
|
||||||
"Z/7hyu": {
|
"Z/7hyu": {
|
||||||
"context": "card balance label",
|
"context": "card balance label",
|
||||||
"string": "Card Balance"
|
"string": "Card Balance"
|
||||||
|
@ -5195,6 +5205,10 @@
|
||||||
"ZIc5lM": {
|
"ZIc5lM": {
|
||||||
"string": "Product Name"
|
"string": "Product Name"
|
||||||
},
|
},
|
||||||
|
"ZJOX8n": {
|
||||||
|
"context": "alert message",
|
||||||
|
"string": "Product no longer exists"
|
||||||
|
},
|
||||||
"ZJPYFl": {
|
"ZJPYFl": {
|
||||||
"context": "accepted header names",
|
"context": "accepted header names",
|
||||||
"string": "Headers with in following format are accepted: <code>authorization*</code>, <code>x-*</code>"
|
"string": "Headers with in following format are accepted: <code>authorization*</code>, <code>x-*</code>"
|
||||||
|
@ -5737,6 +5751,10 @@
|
||||||
"context": "error message",
|
"context": "error message",
|
||||||
"string": "Cannot change the quantity because of insufficient stock"
|
"string": "Cannot change the quantity because of insufficient stock"
|
||||||
},
|
},
|
||||||
|
"dDCLFW": {
|
||||||
|
"context": "alert message",
|
||||||
|
"string": "Not available for sale this channel"
|
||||||
|
},
|
||||||
"dEUZg2": {
|
"dEUZg2": {
|
||||||
"context": "header",
|
"context": "header",
|
||||||
"string": "Menu Items"
|
"string": "Menu Items"
|
||||||
|
@ -7221,10 +7239,6 @@
|
||||||
"context": "gift card history message",
|
"context": "gift card history message",
|
||||||
"string": "Gift card was activated"
|
"string": "Gift card was activated"
|
||||||
},
|
},
|
||||||
"pEMxyy": {
|
|
||||||
"context": "alert message",
|
|
||||||
"string": "This product does no longer exist."
|
|
||||||
},
|
|
||||||
"pFVX6g": {
|
"pFVX6g": {
|
||||||
"string": "Variant with these attributes already exists"
|
"string": "Variant with these attributes already exists"
|
||||||
},
|
},
|
||||||
|
@ -8557,6 +8571,10 @@
|
||||||
"context": "modal header",
|
"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."
|
"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": {
|
"zKOGkU": {
|
||||||
"context": "time during discount is active, header",
|
"context": "time during discount is active, header",
|
||||||
"string": "Active Dates"
|
"string": "Active Dates"
|
||||||
|
@ -8565,10 +8583,6 @@
|
||||||
"context": "gift card removed success alert message",
|
"context": "gift card removed success alert message",
|
||||||
"string": "{selectedItemsCount,plural,one{Successfully deleted gift card} other{Successfully deleted gift cards}}"
|
"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": {
|
"zQX6xO": {
|
||||||
"context": "dialog header",
|
"context": "dialog header",
|
||||||
"string": "Delete App"
|
"string": "Delete App"
|
||||||
|
|
|
@ -35,6 +35,7 @@ const Checkbox: React.FC<CheckboxProps> = ({ helperText, error, ...props }) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<MuiCheckbox
|
<MuiCheckbox
|
||||||
|
data-test-id="checkbox"
|
||||||
{...rest}
|
{...rest}
|
||||||
onClick={
|
onClick={
|
||||||
disableClickPropagation
|
disableClickPropagation
|
||||||
|
|
|
@ -102,6 +102,7 @@ export interface DatagridProps {
|
||||||
freezeColumns?: DataEditorProps["freezeColumns"];
|
freezeColumns?: DataEditorProps["freezeColumns"];
|
||||||
verticalBorder?: DataEditorProps["verticalBorder"];
|
verticalBorder?: DataEditorProps["verticalBorder"];
|
||||||
columnSelect?: DataEditorProps["columnSelect"];
|
columnSelect?: DataEditorProps["columnSelect"];
|
||||||
|
showEmptyDatagrid?: boolean;
|
||||||
rowAnchor?: (item: Item) => string;
|
rowAnchor?: (item: Item) => string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -128,6 +129,7 @@ export const Datagrid: React.FC<DatagridProps> = ({
|
||||||
columnSelect = "none",
|
columnSelect = "none",
|
||||||
onColumnMoved,
|
onColumnMoved,
|
||||||
onColumnResize,
|
onColumnResize,
|
||||||
|
showEmptyDatagrid = false,
|
||||||
loading,
|
loading,
|
||||||
rowAnchor,
|
rowAnchor,
|
||||||
hasRowHover = false,
|
hasRowHover = false,
|
||||||
|
@ -156,6 +158,7 @@ export const Datagrid: React.FC<DatagridProps> = ({
|
||||||
|
|
||||||
const [selection, setSelection] = useState<GridSelection>();
|
const [selection, setSelection] = useState<GridSelection>();
|
||||||
const [hoverRow, setHoverRow] = useState<number | undefined>(undefined);
|
const [hoverRow, setHoverRow] = useState<number | undefined>(undefined);
|
||||||
|
const [areCellsDirty, setCellsDirty] = useState(true);
|
||||||
|
|
||||||
// Allow to listen to which row is selected and notfiy parent component
|
// Allow to listen to which row is selected and notfiy parent component
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -178,7 +181,12 @@ export const Datagrid: React.FC<DatagridProps> = ({
|
||||||
removed,
|
removed,
|
||||||
getChangeIndex,
|
getChangeIndex,
|
||||||
onRowAdded,
|
onRowAdded,
|
||||||
} = useDatagridChange(availableColumns, rows, onChange);
|
} = useDatagridChange(
|
||||||
|
availableColumns,
|
||||||
|
rows,
|
||||||
|
onChange,
|
||||||
|
(areCellsDirty: boolean) => setCellsDirty(areCellsDirty),
|
||||||
|
);
|
||||||
|
|
||||||
const rowsTotal = rows - removed.length + added.length;
|
const rowsTotal = rows - removed.length + added.length;
|
||||||
const hasMenuItem = !!menuItems(0).length;
|
const hasMenuItem = !!menuItems(0).length;
|
||||||
|
@ -197,7 +205,7 @@ export const Datagrid: React.FC<DatagridProps> = ({
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...getCellContent(item, opts),
|
...getCellContent(item, opts),
|
||||||
...(changed
|
...(changed && areCellsDirty
|
||||||
? {
|
? {
|
||||||
themeOverride: {
|
themeOverride: {
|
||||||
bgCell: themeValues.colors.background.surfaceBrandSubdued,
|
bgCell: themeValues.colors.background.surfaceBrandSubdued,
|
||||||
|
@ -214,13 +222,15 @@ export const Datagrid: React.FC<DatagridProps> = ({
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
availableColumns,
|
|
||||||
changes,
|
changes,
|
||||||
added,
|
added,
|
||||||
removed,
|
removed,
|
||||||
getChangeIndex,
|
getChangeIndex,
|
||||||
|
availableColumns,
|
||||||
getCellContent,
|
getCellContent,
|
||||||
themeValues,
|
areCellsDirty,
|
||||||
|
themeValues.colors.background.surfaceBrandSubdued,
|
||||||
|
themeValues.colors.background.surfaceCriticalDepressed,
|
||||||
getCellError,
|
getCellError,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
@ -452,7 +462,7 @@ export const Datagrid: React.FC<DatagridProps> = ({
|
||||||
</Header>
|
</Header>
|
||||||
)}
|
)}
|
||||||
<CardContent classes={{ root: classes.cardContentRoot }}>
|
<CardContent classes={{ root: classes.cardContentRoot }}>
|
||||||
{rowsTotal > 0 ? (
|
{rowsTotal > 0 || showEmptyDatagrid ? (
|
||||||
<>
|
<>
|
||||||
{selection?.rows && selection?.rows.length > 0 && (
|
{selection?.rows && selection?.rows.length > 0 && (
|
||||||
<div className={classes.actionBtnBar}>
|
<div className={classes.actionBtnBar}>
|
||||||
|
@ -549,16 +559,6 @@ export const Datagrid: React.FC<DatagridProps> = ({
|
||||||
}
|
}
|
||||||
rowMarkerWidth={48}
|
rowMarkerWidth={48}
|
||||||
/>
|
/>
|
||||||
<Box
|
|
||||||
position="relative"
|
|
||||||
backgroundColor="plain"
|
|
||||||
borderTopWidth={1}
|
|
||||||
borderTopStyle="solid"
|
|
||||||
borderColor="neutralPlain"
|
|
||||||
__maxWidth={contentMaxWidth}
|
|
||||||
margin="auto"
|
|
||||||
zIndex="2"
|
|
||||||
/>
|
|
||||||
{/* FIXME: https://github.com/glideapps/glide-data-grid/issues/505 */}
|
{/* FIXME: https://github.com/glideapps/glide-data-grid/issues/505 */}
|
||||||
{hasColumnGroups && (
|
{hasColumnGroups && (
|
||||||
<div className={classes.columnGroupFixer} />
|
<div className={classes.columnGroupFixer} />
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
import {
|
import {
|
||||||
CustomCell,
|
CustomCell,
|
||||||
CustomRenderer,
|
CustomRenderer,
|
||||||
getMiddleCenterBias,
|
|
||||||
GridCellKind,
|
GridCellKind,
|
||||||
ProvideEditorCallback,
|
ProvideEditorCallback,
|
||||||
} from "@glideapps/glide-data-grid";
|
} from "@glideapps/glide-data-grid";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { usePriceField } from "../../PriceField/usePriceField";
|
import { usePriceField } from "../../../PriceField/usePriceField";
|
||||||
|
import { drawCurrency, drawPrice } from "./utils";
|
||||||
|
|
||||||
interface MoneyCellProps {
|
interface MoneyCellProps {
|
||||||
readonly kind: "money-cell";
|
readonly kind: "money-cell";
|
||||||
|
@ -55,28 +55,13 @@ export const moneyCellRenderer = (): CustomRenderer<MoneyCell> => ({
|
||||||
const hasValue = value === 0 ? true : !!value;
|
const hasValue = value === 0 ? true : !!value;
|
||||||
const formatted = value?.toString() ?? "-";
|
const formatted = value?.toString() ?? "-";
|
||||||
|
|
||||||
ctx.fillStyle = theme.textDark;
|
drawPrice(ctx, theme, rect, formatted);
|
||||||
ctx.textAlign = "right";
|
|
||||||
ctx.fillText(
|
|
||||||
formatted,
|
|
||||||
rect.x + rect.width - 8,
|
|
||||||
rect.y + rect.height / 2 + getMiddleCenterBias(ctx, theme),
|
|
||||||
);
|
|
||||||
|
|
||||||
ctx.save();
|
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;
|
return true;
|
||||||
},
|
},
|
||||||
provideEditor: () => ({
|
provideEditor: () => ({
|
|
@ -0,0 +1,114 @@
|
||||||
|
import { Locale } from "@dashboard/components/Locale";
|
||||||
|
import OrderDiscountCommonModal from "@dashboard/orders/components/OrderDiscountCommonModal/OrderDiscountCommonModal";
|
||||||
|
import {
|
||||||
|
ORDER_LINE_DISCOUNT,
|
||||||
|
OrderDiscountCommonInput,
|
||||||
|
} from "@dashboard/orders/components/OrderDiscountCommonModal/types";
|
||||||
|
import { useOrderLineDiscountContext } from "@dashboard/products/components/OrderDiscountProviders/OrderLineDiscountProvider";
|
||||||
|
import {
|
||||||
|
CustomCell,
|
||||||
|
CustomRenderer,
|
||||||
|
GridCellKind,
|
||||||
|
} from "@glideapps/glide-data-grid";
|
||||||
|
import React, { useCallback } from "react";
|
||||||
|
|
||||||
|
import { cellHeight } from "../../styles";
|
||||||
|
import {
|
||||||
|
drawCurrency,
|
||||||
|
drawLineCrossedPrice,
|
||||||
|
drawPrice,
|
||||||
|
getFormattedMoney,
|
||||||
|
} from "./utils";
|
||||||
|
|
||||||
|
interface MoneyDiscountedCellProps {
|
||||||
|
readonly kind: "money-discounted-cell";
|
||||||
|
readonly currency: string;
|
||||||
|
readonly undiscounted?: string | number;
|
||||||
|
readonly value: number | string | null;
|
||||||
|
readonly lineItemId?: string;
|
||||||
|
readonly locale: Locale;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DATAGRID_BORDER_WIDTH = 1;
|
||||||
|
const ROW_HEIGHT = cellHeight + DATAGRID_BORDER_WIDTH;
|
||||||
|
|
||||||
|
export type MoneyDiscuntedCell = CustomCell<MoneyDiscountedCellProps>;
|
||||||
|
|
||||||
|
const MoneyDiscountedCellEditor = ({ onFinishedEditing, value }) => {
|
||||||
|
const getDiscountProviderValues = useOrderLineDiscountContext();
|
||||||
|
const editedLineId = value.data.lineItemId;
|
||||||
|
const discountProviderValues = editedLineId
|
||||||
|
? getDiscountProviderValues(editedLineId)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const handleDiscountConfirm = useCallback(
|
||||||
|
async (discount: OrderDiscountCommonInput) => {
|
||||||
|
await discountProviderValues.addOrderLineDiscount(discount);
|
||||||
|
onFinishedEditing(undefined);
|
||||||
|
},
|
||||||
|
[discountProviderValues, onFinishedEditing],
|
||||||
|
);
|
||||||
|
const handleDiscountRemove = useCallback(async () => {
|
||||||
|
await discountProviderValues.removeOrderLineDiscount();
|
||||||
|
onFinishedEditing(undefined);
|
||||||
|
}, [discountProviderValues, onFinishedEditing]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<OrderDiscountCommonModal
|
||||||
|
onClose={() => onFinishedEditing(undefined)}
|
||||||
|
modalType={ORDER_LINE_DISCOUNT}
|
||||||
|
maxPrice={discountProviderValues.unitUndiscountedPrice}
|
||||||
|
onConfirm={handleDiscountConfirm}
|
||||||
|
onRemove={handleDiscountRemove}
|
||||||
|
existingDiscount={discountProviderValues.orderLineDiscount}
|
||||||
|
confirmStatus={discountProviderValues.orderLineDiscountUpdateStatus}
|
||||||
|
removeStatus={discountProviderValues.orderLineDiscountRemoveStatus}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const moneyDiscountedCellRenderer =
|
||||||
|
(): CustomRenderer<MoneyDiscuntedCell> => ({
|
||||||
|
kind: GridCellKind.Custom,
|
||||||
|
isMatch: (c): c is MoneyDiscuntedCell =>
|
||||||
|
(c.data as any).kind === "money-discounted-cell",
|
||||||
|
draw: (args, cell) => {
|
||||||
|
const { ctx, theme, rect } = args;
|
||||||
|
const { currency, value, undiscounted, locale } = cell.data;
|
||||||
|
const hasValue = value === 0 ? true : !!value;
|
||||||
|
const formattedValue = getFormattedMoney(value, currency, locale, "-");
|
||||||
|
const formattedUndiscounted = getFormattedMoney(
|
||||||
|
undiscounted !== value ? undiscounted : "",
|
||||||
|
currency,
|
||||||
|
locale,
|
||||||
|
);
|
||||||
|
const formattedWithDiscount =
|
||||||
|
formattedUndiscounted + " " + formattedValue;
|
||||||
|
|
||||||
|
drawPrice(ctx, theme, rect, formattedWithDiscount);
|
||||||
|
|
||||||
|
// Draw crossed line above price without discount
|
||||||
|
if (undiscounted !== undefined && undiscounted !== value) {
|
||||||
|
drawLineCrossedPrice(
|
||||||
|
ctx,
|
||||||
|
rect,
|
||||||
|
formattedWithDiscount,
|
||||||
|
formattedUndiscounted,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.save();
|
||||||
|
|
||||||
|
drawCurrency(ctx, theme, rect, hasValue ? currency : "-");
|
||||||
|
|
||||||
|
ctx.restore();
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
provideEditor: () => ({
|
||||||
|
editor: MoneyDiscountedCellEditor,
|
||||||
|
styleOverride: {
|
||||||
|
padding: `${ROW_HEIGHT}px 0 0 0`,
|
||||||
|
},
|
||||||
|
disableStyling: true,
|
||||||
|
}),
|
||||||
|
});
|
2
src/components/Datagrid/customCells/Money/index.ts
Normal file
2
src/components/Datagrid/customCells/Money/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
export * from "./MoneyDiscountedCell";
|
||||||
|
export * from "./MoneyCell";
|
73
src/components/Datagrid/customCells/Money/utils.ts
Normal file
73
src/components/Datagrid/customCells/Money/utils.ts
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
import { Locale } from "@dashboard/components/Locale";
|
||||||
|
import { formatMoneyAmount } from "@dashboard/components/Money";
|
||||||
|
import {
|
||||||
|
getMiddleCenterBias,
|
||||||
|
Rectangle,
|
||||||
|
Theme,
|
||||||
|
} from "@glideapps/glide-data-grid";
|
||||||
|
|
||||||
|
const OFFSET = 8;
|
||||||
|
|
||||||
|
export function drawLineCrossedPrice(
|
||||||
|
ctx: CanvasRenderingContext2D,
|
||||||
|
rect: Rectangle,
|
||||||
|
discountedPrice: string,
|
||||||
|
undiscountedPrice: string,
|
||||||
|
) {
|
||||||
|
const { width: totalTextWidth } = ctx.measureText(discountedPrice);
|
||||||
|
const { width: undiscountedTextWidth } = ctx.measureText(undiscountedPrice);
|
||||||
|
|
||||||
|
ctx.fillRect(
|
||||||
|
rect.x + rect.width - OFFSET - totalTextWidth,
|
||||||
|
rect.y + rect.height / 2,
|
||||||
|
undiscountedTextWidth,
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function drawPrice(
|
||||||
|
ctx: CanvasRenderingContext2D,
|
||||||
|
theme: Theme,
|
||||||
|
rect: Rectangle,
|
||||||
|
text: string,
|
||||||
|
) {
|
||||||
|
ctx.fillStyle = theme.textDark;
|
||||||
|
ctx.textAlign = "right";
|
||||||
|
ctx.fillText(
|
||||||
|
text,
|
||||||
|
rect.x + rect.width - OFFSET,
|
||||||
|
rect.y + rect.height / 2 + getMiddleCenterBias(ctx, theme),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function drawCurrency(
|
||||||
|
ctx: CanvasRenderingContext2D,
|
||||||
|
theme: Theme,
|
||||||
|
rect: Rectangle,
|
||||||
|
currency: string,
|
||||||
|
) {
|
||||||
|
ctx.fillStyle = theme.textMedium;
|
||||||
|
ctx.textAlign = "left";
|
||||||
|
ctx.font = [
|
||||||
|
theme.baseFontStyle.replace(/bold/g, "normal"),
|
||||||
|
theme.fontFamily,
|
||||||
|
].join(" ");
|
||||||
|
ctx.fillText(
|
||||||
|
currency,
|
||||||
|
rect.x + 8,
|
||||||
|
rect.y + rect.height / 2 + getMiddleCenterBias(ctx, theme),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getFormattedMoney(
|
||||||
|
value: string | number,
|
||||||
|
currency: string,
|
||||||
|
locale: Locale,
|
||||||
|
placeholder = "",
|
||||||
|
) {
|
||||||
|
if (value !== undefined && value !== null && value !== "") {
|
||||||
|
return formatMoneyAmount({ amount: Number(value), currency }, locale);
|
||||||
|
}
|
||||||
|
|
||||||
|
return placeholder;
|
||||||
|
}
|
|
@ -2,6 +2,7 @@ import {
|
||||||
NumberCell,
|
NumberCell,
|
||||||
numberCellEmptyValue,
|
numberCellEmptyValue,
|
||||||
} from "@dashboard/components/Datagrid/customCells/NumberCell";
|
} from "@dashboard/components/Datagrid/customCells/NumberCell";
|
||||||
|
import { Locale } from "@dashboard/components/Locale";
|
||||||
import { GridCell, GridCellKind } from "@glideapps/glide-data-grid";
|
import { GridCell, GridCellKind } from "@glideapps/glide-data-grid";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
@ -9,7 +10,7 @@ import {
|
||||||
DropdownCellContentProps,
|
DropdownCellContentProps,
|
||||||
DropdownChoice,
|
DropdownChoice,
|
||||||
} from "./DropdownCell";
|
} from "./DropdownCell";
|
||||||
import { MoneyCell } from "./MoneyCell";
|
import { MoneyCell, MoneyDiscuntedCell } from "./Money";
|
||||||
import { ThumbnailCell } from "./ThumbnailCell";
|
import { ThumbnailCell } from "./ThumbnailCell";
|
||||||
|
|
||||||
const common = {
|
const common = {
|
||||||
|
@ -110,6 +111,41 @@ export function moneyCell(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface MoneyDiscountedCellData {
|
||||||
|
value: number | string | null;
|
||||||
|
discount?: string | number;
|
||||||
|
undiscounted?: string | number;
|
||||||
|
currency: string;
|
||||||
|
locale: Locale;
|
||||||
|
lineItemId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function moneyDiscountedCell(
|
||||||
|
{
|
||||||
|
value,
|
||||||
|
undiscounted,
|
||||||
|
currency,
|
||||||
|
locale,
|
||||||
|
lineItemId,
|
||||||
|
}: MoneyDiscountedCellData,
|
||||||
|
opts?: Partial<GridCell>,
|
||||||
|
): MoneyDiscuntedCell {
|
||||||
|
return {
|
||||||
|
...common,
|
||||||
|
...opts,
|
||||||
|
kind: GridCellKind.Custom,
|
||||||
|
data: {
|
||||||
|
kind: "money-discounted-cell",
|
||||||
|
value,
|
||||||
|
currency,
|
||||||
|
undiscounted,
|
||||||
|
lineItemId,
|
||||||
|
locale,
|
||||||
|
},
|
||||||
|
copyData: value?.toString() ?? "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function dropdownCell(
|
export function dropdownCell(
|
||||||
value: DropdownChoice,
|
value: DropdownChoice,
|
||||||
dataOpts: DropdownCellContentProps &
|
dataOpts: DropdownCellContentProps &
|
||||||
|
|
|
@ -3,7 +3,8 @@ import { useExtraCells } from "@glideapps/glide-data-grid-cells";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
|
|
||||||
import { dropdownCellRenderer } from "./DropdownCell";
|
import { dropdownCellRenderer } from "./DropdownCell";
|
||||||
import { moneyCellRenderer } from "./MoneyCell";
|
import { moneyCellRenderer } from "./Money/MoneyCell";
|
||||||
|
import { moneyDiscountedCellRenderer } from "./Money/MoneyDiscountedCell";
|
||||||
import { numberCellRenderer } from "./NumberCell";
|
import { numberCellRenderer } from "./NumberCell";
|
||||||
import { thumbnailCellRenderer } from "./ThumbnailCell";
|
import { thumbnailCellRenderer } from "./ThumbnailCell";
|
||||||
|
|
||||||
|
@ -14,6 +15,7 @@ export function useCustomCellRenderers() {
|
||||||
const renderers = useMemo(
|
const renderers = useMemo(
|
||||||
() => [
|
() => [
|
||||||
moneyCellRenderer(),
|
moneyCellRenderer(),
|
||||||
|
moneyDiscountedCellRenderer(),
|
||||||
numberCellRenderer(locale),
|
numberCellRenderer(locale),
|
||||||
dropdownCellRenderer,
|
dropdownCellRenderer,
|
||||||
thumbnailCellRenderer,
|
thumbnailCellRenderer,
|
||||||
|
|
|
@ -78,11 +78,11 @@ export function useColumnsDefault(
|
||||||
|
|
||||||
const columnChoices = useMemo(
|
const columnChoices = useMemo(
|
||||||
() =>
|
() =>
|
||||||
applyFilters(columns).map(({ id, title }) => ({
|
applyFilters(columns, availableColumns).map(({ id, title }) => ({
|
||||||
label: title,
|
label: title,
|
||||||
value: id,
|
value: id,
|
||||||
})),
|
})),
|
||||||
[columns],
|
[columns, availableColumns],
|
||||||
);
|
);
|
||||||
const availableColumnsChoices = useMemo(
|
const availableColumnsChoices = useMemo(
|
||||||
() =>
|
() =>
|
||||||
|
@ -113,22 +113,27 @@ export function useColumnsDefault(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyFilters(columns: readonly AvailableColumn[]) {
|
function applyFilters(
|
||||||
return columns.filter(byNoEmptyColumn).filter(byNotFirstColumn);
|
columns: readonly AvailableColumn[],
|
||||||
|
availableColumns?: readonly AvailableColumn[],
|
||||||
|
) {
|
||||||
|
return columns
|
||||||
|
.filter(byNoEmptyColumn)
|
||||||
|
.filter(byNotFirstColumn(availableColumns));
|
||||||
}
|
}
|
||||||
|
|
||||||
function byNoEmptyColumn(column: AvailableColumn) {
|
function byNoEmptyColumn(column: AvailableColumn) {
|
||||||
return column.id !== "empty";
|
return column.id !== "empty";
|
||||||
}
|
}
|
||||||
|
|
||||||
function byNotFirstColumn(
|
function byNotFirstColumn(availableColumns?: readonly AvailableColumn[]) {
|
||||||
_: AvailableColumn,
|
return (column: AvailableColumn, index: number, array: AvailableColumn[]) => {
|
||||||
index: number,
|
// Check not first column base on available columns to prevent base on columns that order can change
|
||||||
array: AvailableColumn[],
|
if (availableColumns) {
|
||||||
) {
|
const colIndex = availableColumns.findIndex(col => col.id === column.id);
|
||||||
if (array.some(col => col.id === "empty")) {
|
return availableColumns[0]?.id === "empty" ? colIndex > 1 : colIndex > 0;
|
||||||
return index > 1;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return index > 0;
|
return array[0]?.id === "empty" ? index > 1 : index > 0;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,7 +24,10 @@ export interface DatagridChangeOpts {
|
||||||
removed: number[];
|
removed: number[];
|
||||||
updates: DatagridChange[];
|
updates: DatagridChange[];
|
||||||
}
|
}
|
||||||
export type OnDatagridChange = (opts: DatagridChangeOpts) => void;
|
export type OnDatagridChange = (
|
||||||
|
opts: DatagridChangeOpts,
|
||||||
|
setMarkCellsDirty: (areCellsDirty: boolean) => void,
|
||||||
|
) => void;
|
||||||
|
|
||||||
export interface UseDatagridChangeState {
|
export interface UseDatagridChangeState {
|
||||||
added: number[];
|
added: number[];
|
||||||
|
@ -64,6 +67,7 @@ function useDatagridChange(
|
||||||
availableColumns: readonly AvailableColumn[],
|
availableColumns: readonly AvailableColumn[],
|
||||||
rows: number,
|
rows: number,
|
||||||
onChange?: OnDatagridChange,
|
onChange?: OnDatagridChange,
|
||||||
|
setMarkCellsDirty?: (areCellsDirty: boolean) => void,
|
||||||
) {
|
) {
|
||||||
const { added, setAdded, removed, setRemoved, changes } =
|
const { added, setAdded, removed, setRemoved, changes } =
|
||||||
useDatagridChangeStateContext();
|
useDatagridChangeStateContext();
|
||||||
|
@ -78,14 +82,17 @@ function useDatagridChange(
|
||||||
const notify = useCallback(
|
const notify = useCallback(
|
||||||
(updates: DatagridChange[], added: number[], removed: number[]) => {
|
(updates: DatagridChange[], added: number[], removed: number[]) => {
|
||||||
if (onChange) {
|
if (onChange) {
|
||||||
onChange({
|
onChange(
|
||||||
updates,
|
{
|
||||||
removed,
|
updates,
|
||||||
added,
|
removed,
|
||||||
});
|
added,
|
||||||
|
},
|
||||||
|
setMarkCellsDirty,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[onChange],
|
[onChange, setMarkCellsDirty],
|
||||||
);
|
);
|
||||||
|
|
||||||
const onCellEdited = useCallback(
|
const onCellEdited = useCallback(
|
||||||
|
|
|
@ -66,7 +66,7 @@ const useStyles = makeStyles(
|
||||||
boxShadow: "none !important",
|
boxShadow: "none !important",
|
||||||
padding: "0 !important",
|
padding: "0 !important",
|
||||||
},
|
},
|
||||||
"& input, & textarea": {
|
"& input:not([class*='MuiInputBase']), & textarea": {
|
||||||
appearance: "none",
|
appearance: "none",
|
||||||
background: "none",
|
background: "none",
|
||||||
border: "none",
|
border: "none",
|
||||||
|
@ -77,7 +77,7 @@ const useStyles = makeStyles(
|
||||||
padding: vars.space[3],
|
padding: vars.space[3],
|
||||||
outline: 0,
|
outline: 0,
|
||||||
},
|
},
|
||||||
'& input[type="number"]': {
|
"& input[type='number']:not([class*='MuiInputBase'])": {
|
||||||
textAlign: "right",
|
textAlign: "right",
|
||||||
width: "100%",
|
width: "100%",
|
||||||
},
|
},
|
||||||
|
|
|
@ -77,6 +77,7 @@ export const PriceField: React.FC<PriceFieldProps> = props => {
|
||||||
fullWidth
|
fullWidth
|
||||||
value={value}
|
value={value}
|
||||||
InputLabelProps={InputLabelProps}
|
InputLabelProps={InputLabelProps}
|
||||||
|
data-test-id="price-field"
|
||||||
InputProps={{
|
InputProps={{
|
||||||
...InputProps,
|
...InputProps,
|
||||||
endAdornment: currencySymbol ? (
|
endAdornment: currencySymbol ? (
|
||||||
|
|
|
@ -13,7 +13,7 @@ import { FormattedMessage } from "react-intl";
|
||||||
import { useStyles } from "./styles";
|
import { useStyles } from "./styles";
|
||||||
|
|
||||||
export interface RadioGroupFieldChoice<
|
export interface RadioGroupFieldChoice<
|
||||||
T extends string | number = string | number
|
T extends string | number = string | number,
|
||||||
> {
|
> {
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
value: T;
|
value: T;
|
||||||
|
@ -85,6 +85,7 @@ export const RadioGroupField: React.FC<RadioGroupFieldProps> = props => {
|
||||||
}}
|
}}
|
||||||
control={
|
control={
|
||||||
<Radio
|
<Radio
|
||||||
|
data-test-id={choice.value}
|
||||||
className={clsx({
|
className={clsx({
|
||||||
[classes.alignTop]: alignTop,
|
[classes.alignTop]: alignTop,
|
||||||
})}
|
})}
|
||||||
|
|
|
@ -0,0 +1,84 @@
|
||||||
|
import ColumnPicker from "@dashboard/components/ColumnPicker";
|
||||||
|
import Datagrid from "@dashboard/components/Datagrid/Datagrid";
|
||||||
|
import { useColumnsDefault } from "@dashboard/components/Datagrid/hooks/useColumnsDefault";
|
||||||
|
import {
|
||||||
|
DatagridChangeStateContext,
|
||||||
|
useDatagridChangeState,
|
||||||
|
} from "@dashboard/components/Datagrid/hooks/useDatagridChange";
|
||||||
|
import { OrderLineFragment } from "@dashboard/graphql";
|
||||||
|
import React from "react";
|
||||||
|
import { useIntl } from "react-intl";
|
||||||
|
|
||||||
|
import { messages } from "../OrderListDatagrid/messages";
|
||||||
|
import { useColumns, useGetCellContent } from "./datagrid";
|
||||||
|
|
||||||
|
interface OrderDetailsDatagridProps {
|
||||||
|
lines: OrderLineFragment[];
|
||||||
|
loading: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const OrderDetailsDatagrid = ({
|
||||||
|
lines,
|
||||||
|
loading,
|
||||||
|
}: OrderDetailsDatagridProps) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const datagrid = useDatagridChangeState();
|
||||||
|
|
||||||
|
const availableColumns = useColumns();
|
||||||
|
|
||||||
|
const {
|
||||||
|
availableColumnsChoices,
|
||||||
|
columnChoices,
|
||||||
|
columns,
|
||||||
|
defaultColumns,
|
||||||
|
onColumnMoved,
|
||||||
|
onColumnResize,
|
||||||
|
onColumnsChange,
|
||||||
|
picker,
|
||||||
|
} = useColumnsDefault(availableColumns);
|
||||||
|
|
||||||
|
const getCellContent = useGetCellContent({
|
||||||
|
columns,
|
||||||
|
data: lines,
|
||||||
|
loading,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DatagridChangeStateContext.Provider value={datagrid}>
|
||||||
|
<Datagrid
|
||||||
|
readonly
|
||||||
|
showEmptyDatagrid
|
||||||
|
rowMarkers="none"
|
||||||
|
columnSelect="single"
|
||||||
|
freezeColumns={1}
|
||||||
|
availableColumns={columns}
|
||||||
|
emptyText={intl.formatMessage(messages.emptyText)}
|
||||||
|
getCellContent={getCellContent}
|
||||||
|
getCellError={() => false}
|
||||||
|
menuItems={() => []}
|
||||||
|
rows={loading ? 1 : lines.length}
|
||||||
|
selectionActions={() => null}
|
||||||
|
onColumnResize={onColumnResize}
|
||||||
|
onColumnMoved={onColumnMoved}
|
||||||
|
renderColumnPicker={defaultProps => (
|
||||||
|
<ColumnPicker
|
||||||
|
{...defaultProps}
|
||||||
|
IconButtonProps={{
|
||||||
|
...defaultProps.IconButtonProps,
|
||||||
|
disabled: lines.length === 0,
|
||||||
|
}}
|
||||||
|
availableColumns={availableColumnsChoices}
|
||||||
|
initialColumns={columnChoices}
|
||||||
|
defaultColumns={defaultColumns}
|
||||||
|
onSave={onColumnsChange}
|
||||||
|
hasMore={false}
|
||||||
|
loading={false}
|
||||||
|
onFetchMore={() => undefined}
|
||||||
|
onQueryChange={picker.setQuery}
|
||||||
|
query={picker.query}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</DatagridChangeStateContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
110
src/orders/components/OrderDetailsDatagrid/datagrid.ts
Normal file
110
src/orders/components/OrderDetailsDatagrid/datagrid.ts
Normal file
|
@ -0,0 +1,110 @@
|
||||||
|
import {
|
||||||
|
loadingCell,
|
||||||
|
moneyCell,
|
||||||
|
readonlyTextCell,
|
||||||
|
thumbnailCell,
|
||||||
|
} from "@dashboard/components/Datagrid/customCells/cells";
|
||||||
|
import { GetCellContentOpts } from "@dashboard/components/Datagrid/Datagrid";
|
||||||
|
import { AvailableColumn } from "@dashboard/components/Datagrid/types";
|
||||||
|
import { OrderLineFragment } from "@dashboard/graphql";
|
||||||
|
import { getDatagridRowDataIndex } from "@dashboard/misc";
|
||||||
|
import { GridCell, Item } from "@glideapps/glide-data-grid";
|
||||||
|
import { useCallback, useMemo } from "react";
|
||||||
|
import { useIntl } from "react-intl";
|
||||||
|
|
||||||
|
import { columnsMessages } from "./messages";
|
||||||
|
|
||||||
|
export const useColumns = (): AvailableColumn[] => {
|
||||||
|
const intl = useIntl();
|
||||||
|
|
||||||
|
const columns = useMemo(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
id: "product",
|
||||||
|
title: intl.formatMessage(columnsMessages.product),
|
||||||
|
width: 300,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "sku",
|
||||||
|
title: intl.formatMessage(columnsMessages.sku),
|
||||||
|
width: 150,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "quantity",
|
||||||
|
title: intl.formatMessage(columnsMessages.quantity),
|
||||||
|
width: 80,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "price",
|
||||||
|
title: intl.formatMessage(columnsMessages.price),
|
||||||
|
width: 150,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "total",
|
||||||
|
title: intl.formatMessage(columnsMessages.total),
|
||||||
|
width: 150,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[intl],
|
||||||
|
);
|
||||||
|
|
||||||
|
return columns;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface GetCellContentProps {
|
||||||
|
columns: AvailableColumn[];
|
||||||
|
data: OrderLineFragment[];
|
||||||
|
loading: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useGetCellContent = ({
|
||||||
|
columns,
|
||||||
|
data,
|
||||||
|
loading,
|
||||||
|
}: GetCellContentProps) => {
|
||||||
|
const getCellContent = useCallback(
|
||||||
|
([column, row]: Item, { added, removed }: GetCellContentOpts): GridCell => {
|
||||||
|
if (loading) {
|
||||||
|
return loadingCell();
|
||||||
|
}
|
||||||
|
|
||||||
|
const columnId = columns[column].id;
|
||||||
|
const rowData = added.includes(row)
|
||||||
|
? undefined
|
||||||
|
: data[getDatagridRowDataIndex(row, removed)];
|
||||||
|
|
||||||
|
if (!rowData) {
|
||||||
|
return readonlyTextCell("", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (columnId) {
|
||||||
|
case "product":
|
||||||
|
return thumbnailCell(
|
||||||
|
rowData?.productName ?? "",
|
||||||
|
rowData.thumbnail?.url ?? "",
|
||||||
|
);
|
||||||
|
case "sku":
|
||||||
|
return readonlyTextCell(rowData.productSku ?? "", false);
|
||||||
|
case "quantity":
|
||||||
|
return readonlyTextCell(rowData.quantity.toString(), false);
|
||||||
|
case "price":
|
||||||
|
return moneyCell(
|
||||||
|
rowData.unitPrice.gross.amount,
|
||||||
|
rowData.unitPrice.gross.currency,
|
||||||
|
);
|
||||||
|
|
||||||
|
case "total":
|
||||||
|
return moneyCell(
|
||||||
|
rowData.totalPrice.gross.amount,
|
||||||
|
rowData.totalPrice.gross.currency,
|
||||||
|
);
|
||||||
|
|
||||||
|
default:
|
||||||
|
return readonlyTextCell("", false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[columns, data, loading],
|
||||||
|
);
|
||||||
|
|
||||||
|
return getCellContent;
|
||||||
|
};
|
1
src/orders/components/OrderDetailsDatagrid/index.ts
Normal file
1
src/orders/components/OrderDetailsDatagrid/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export * from "./OrderDetailsDatagrid";
|
29
src/orders/components/OrderDetailsDatagrid/messages.ts
Normal file
29
src/orders/components/OrderDetailsDatagrid/messages.ts
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
import { defineMessages } from "react-intl";
|
||||||
|
|
||||||
|
export const columnsMessages = defineMessages({
|
||||||
|
product: {
|
||||||
|
id: "WE8IFE",
|
||||||
|
defaultMessage: "Product",
|
||||||
|
description: "product name",
|
||||||
|
},
|
||||||
|
sku: {
|
||||||
|
id: "8J81ri",
|
||||||
|
defaultMessage: "SKU",
|
||||||
|
description: "ordered product sku",
|
||||||
|
},
|
||||||
|
quantity: {
|
||||||
|
id: "tvpAXl",
|
||||||
|
defaultMessage: "Quantity",
|
||||||
|
description: "ordered product quantity",
|
||||||
|
},
|
||||||
|
price: {
|
||||||
|
id: "b810WJ",
|
||||||
|
defaultMessage: "Price",
|
||||||
|
description: "product price",
|
||||||
|
},
|
||||||
|
total: {
|
||||||
|
id: "qT6YYk",
|
||||||
|
defaultMessage: "Total",
|
||||||
|
description: "order line total price",
|
||||||
|
},
|
||||||
|
});
|
|
@ -14,7 +14,7 @@ import React from "react";
|
||||||
import OrderDetailsPage, { OrderDetailsPageProps } from "./OrderDetailsPage";
|
import OrderDetailsPage, { OrderDetailsPageProps } from "./OrderDetailsPage";
|
||||||
|
|
||||||
const props: Omit<OrderDetailsPageProps, "classes"> = {
|
const props: Omit<OrderDetailsPageProps, "classes"> = {
|
||||||
disabled: false,
|
loading: false,
|
||||||
onBillingAddressEdit: undefined,
|
onBillingAddressEdit: undefined,
|
||||||
onTransactionAction: () => undefined,
|
onTransactionAction: () => undefined,
|
||||||
onFulfillmentApprove: () => undefined,
|
onFulfillmentApprove: () => undefined,
|
||||||
|
|
|
@ -50,7 +50,7 @@ export interface OrderDetailsPageProps {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
}>;
|
}>;
|
||||||
disabled: boolean;
|
loading: boolean;
|
||||||
saveButtonBarState: ConfirmButtonTransitionState;
|
saveButtonBarState: ConfirmButtonTransitionState;
|
||||||
errors: OrderErrorFragment[];
|
errors: OrderErrorFragment[];
|
||||||
onOrderLineAdd?: () => void;
|
onOrderLineAdd?: () => void;
|
||||||
|
@ -85,7 +85,7 @@ export interface OrderDetailsPageProps {
|
||||||
|
|
||||||
const OrderDetailsPage: React.FC<OrderDetailsPageProps> = props => {
|
const OrderDetailsPage: React.FC<OrderDetailsPageProps> = props => {
|
||||||
const {
|
const {
|
||||||
disabled,
|
loading,
|
||||||
order,
|
order,
|
||||||
shop,
|
shop,
|
||||||
saveButtonBarState,
|
saveButtonBarState,
|
||||||
|
@ -162,11 +162,11 @@ const OrderDetailsPage: React.FC<OrderDetailsPageProps> = props => {
|
||||||
|
|
||||||
const allowSave = () => {
|
const allowSave = () => {
|
||||||
if (!isOrderUnconfirmed) {
|
if (!isOrderUnconfirmed) {
|
||||||
return disabled;
|
return loading;
|
||||||
} else if (!order?.lines?.length) {
|
} else if (!order?.lines?.length) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return disabled;
|
return loading;
|
||||||
};
|
};
|
||||||
|
|
||||||
const selectCardMenuItems = filteredConditionalItems([
|
const selectCardMenuItems = filteredConditionalItems([
|
||||||
|
@ -231,12 +231,14 @@ const OrderDetailsPage: React.FC<OrderDetailsPageProps> = props => {
|
||||||
notAllowedToFulfillUnpaid={notAllowedToFulfillUnpaid}
|
notAllowedToFulfillUnpaid={notAllowedToFulfillUnpaid}
|
||||||
lines={unfulfilled}
|
lines={unfulfilled}
|
||||||
onFulfill={onOrderFulfill}
|
onFulfill={onOrderFulfill}
|
||||||
|
loading={loading}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<OrderDraftDetails
|
<OrderDraftDetails
|
||||||
order={order}
|
order={order}
|
||||||
errors={errors}
|
errors={errors}
|
||||||
|
loading={loading}
|
||||||
onOrderLineAdd={onOrderLineAdd}
|
onOrderLineAdd={onOrderLineAdd}
|
||||||
onOrderLineChange={onOrderLineChange}
|
onOrderLineChange={onOrderLineChange}
|
||||||
onOrderLineRemove={onOrderLineRemove}
|
onOrderLineRemove={onOrderLineRemove}
|
||||||
|
|
|
@ -16,7 +16,7 @@ import React from "react";
|
||||||
import OrderDetailsPage, { OrderDetailsPageProps } from "./OrderDetailsPage";
|
import OrderDetailsPage, { OrderDetailsPageProps } from "./OrderDetailsPage";
|
||||||
|
|
||||||
const props: Omit<OrderDetailsPageProps, "classes"> = {
|
const props: Omit<OrderDetailsPageProps, "classes"> = {
|
||||||
disabled: false,
|
loading: false,
|
||||||
onBillingAddressEdit: undefined,
|
onBillingAddressEdit: undefined,
|
||||||
onTransactionAction: () => undefined,
|
onTransactionAction: () => undefined,
|
||||||
onFulfillmentApprove: () => undefined,
|
onFulfillmentApprove: () => undefined,
|
||||||
|
|
|
@ -13,6 +13,9 @@ const useStyles = makeStyles(
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
padding: theme.spacing(3, 3, 0, 3),
|
padding: theme.spacing(3, 3, 0, 3),
|
||||||
},
|
},
|
||||||
|
closeIcon: {
|
||||||
|
cursor: "pointer",
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
{ name: "ModalTitle" },
|
{ name: "ModalTitle" },
|
||||||
);
|
);
|
||||||
|
@ -34,7 +37,7 @@ const ModalTitle: React.FC<ModalTitleProps> = ({
|
||||||
<>
|
<>
|
||||||
<div className={classes.container}>
|
<div className={classes.container}>
|
||||||
<Typography variant="h5">{title}</Typography>
|
<Typography variant="h5">{title}</Typography>
|
||||||
<CloseIcon onClick={onClose} />
|
<CloseIcon className={classes.closeIcon} onClick={onClose} />
|
||||||
</div>
|
</div>
|
||||||
{withBorder && (
|
{withBorder && (
|
||||||
<>
|
<>
|
||||||
|
|
|
@ -6,22 +6,10 @@ import RadioGroupField from "@dashboard/components/RadioGroupField";
|
||||||
import { DiscountValueTypeEnum, MoneyFragment } from "@dashboard/graphql";
|
import { DiscountValueTypeEnum, MoneyFragment } from "@dashboard/graphql";
|
||||||
import { useUpdateEffect } from "@dashboard/hooks/useUpdateEffect";
|
import { useUpdateEffect } from "@dashboard/hooks/useUpdateEffect";
|
||||||
import { buttonMessages } from "@dashboard/intl";
|
import { buttonMessages } from "@dashboard/intl";
|
||||||
import {
|
import { toFixed } from "@dashboard/utils/toFixed";
|
||||||
Card,
|
import { Card, CardContent, TextField, Typography } from "@material-ui/core";
|
||||||
CardContent,
|
|
||||||
Popper,
|
|
||||||
TextField,
|
|
||||||
Typography,
|
|
||||||
} from "@material-ui/core";
|
|
||||||
import { PopperPlacementType } from "@material-ui/core/Popper";
|
|
||||||
import { ConfirmButtonTransitionState, makeStyles } from "@saleor/macaw-ui";
|
import { ConfirmButtonTransitionState, makeStyles } from "@saleor/macaw-ui";
|
||||||
import React, {
|
import React, { ChangeEvent, useEffect, useRef, useState } from "react";
|
||||||
ChangeEvent,
|
|
||||||
MutableRefObject,
|
|
||||||
useEffect,
|
|
||||||
useRef,
|
|
||||||
useState,
|
|
||||||
} from "react";
|
|
||||||
import { defineMessages, useIntl } from "react-intl";
|
import { defineMessages, useIntl } from "react-intl";
|
||||||
|
|
||||||
import ModalTitle from "./ModalTitle";
|
import ModalTitle from "./ModalTitle";
|
||||||
|
@ -31,16 +19,10 @@ import {
|
||||||
OrderDiscountType,
|
OrderDiscountType,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
const fullNumbersRegex = /^[0-9]*$/;
|
|
||||||
const numbersRegex = /([0-9]+\.?[0-9]*)$/;
|
const numbersRegex = /([0-9]+\.?[0-9]*)$/;
|
||||||
const PERMIL = 0.01;
|
|
||||||
|
|
||||||
const useStyles = makeStyles(
|
const useStyles = makeStyles(
|
||||||
theme => ({
|
theme => ({
|
||||||
container: {
|
|
||||||
zIndex: 1000,
|
|
||||||
marginTop: theme.spacing(1),
|
|
||||||
},
|
|
||||||
removeButton: {
|
removeButton: {
|
||||||
"&:hover": {
|
"&:hover": {
|
||||||
backgroundColor: theme.palette.error.main,
|
backgroundColor: theme.palette.error.main,
|
||||||
|
@ -98,6 +80,16 @@ const messages = defineMessages({
|
||||||
defaultMessage: "Invalid value",
|
defaultMessage: "Invalid value",
|
||||||
description: "value input helper text",
|
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: {
|
discountValueLabel: {
|
||||||
id: "GAmGog",
|
id: "GAmGog",
|
||||||
defaultMessage: "Discount value",
|
defaultMessage: "Discount value",
|
||||||
|
@ -116,10 +108,7 @@ export interface OrderDiscountCommonModalProps {
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onRemove: () => void;
|
onRemove: () => void;
|
||||||
modalType: OrderDiscountType;
|
modalType: OrderDiscountType;
|
||||||
anchorRef: MutableRefObject<any>;
|
|
||||||
existingDiscount: OrderDiscountCommonInput;
|
existingDiscount: OrderDiscountCommonInput;
|
||||||
dialogPlacement: PopperPlacementType;
|
|
||||||
isOpen: boolean;
|
|
||||||
confirmStatus: ConfirmButtonTransitionState;
|
confirmStatus: ConfirmButtonTransitionState;
|
||||||
removeStatus: ConfirmButtonTransitionState;
|
removeStatus: ConfirmButtonTransitionState;
|
||||||
}
|
}
|
||||||
|
@ -128,12 +117,9 @@ const OrderDiscountCommonModal: React.FC<OrderDiscountCommonModalProps> = ({
|
||||||
maxPrice = { amount: null, currency: "" },
|
maxPrice = { amount: null, currency: "" },
|
||||||
onConfirm,
|
onConfirm,
|
||||||
modalType,
|
modalType,
|
||||||
anchorRef,
|
|
||||||
onClose,
|
onClose,
|
||||||
onRemove,
|
onRemove,
|
||||||
existingDiscount,
|
existingDiscount,
|
||||||
dialogPlacement,
|
|
||||||
isOpen,
|
|
||||||
confirmStatus,
|
confirmStatus,
|
||||||
removeStatus,
|
removeStatus,
|
||||||
}) => {
|
}) => {
|
||||||
|
@ -147,7 +133,7 @@ const OrderDiscountCommonModal: React.FC<OrderDiscountCommonModalProps> = ({
|
||||||
const stringifiedValue = existingDiscount.value.toString();
|
const stringifiedValue = existingDiscount.value.toString();
|
||||||
|
|
||||||
if (calculationMode === DiscountValueTypeEnum.FIXED) {
|
if (calculationMode === DiscountValueTypeEnum.FIXED) {
|
||||||
return parseFloat(stringifiedValue).toFixed(2);
|
return parseFloat(stringifiedValue).toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
return stringifiedValue;
|
return stringifiedValue;
|
||||||
|
@ -166,7 +152,7 @@ const OrderDiscountCommonModal: React.FC<OrderDiscountCommonModalProps> = ({
|
||||||
|
|
||||||
const initialData = getInitialData();
|
const initialData = getInitialData();
|
||||||
|
|
||||||
const [isValueError, setValueError] = useState<boolean>(false);
|
const [valueErrorMsg, setValueErrorMsg] = useState<string | null>(null);
|
||||||
const [reason, setReason] = useState<string>(initialData.reason);
|
const [reason, setReason] = useState<string>(initialData.reason);
|
||||||
const [value, setValue] = useState<string>(initialData.value);
|
const [value, setValue] = useState<string>(initialData.value);
|
||||||
const [calculationMode, setCalculationMode] = useState<DiscountValueTypeEnum>(
|
const [calculationMode, setCalculationMode] = useState<DiscountValueTypeEnum>(
|
||||||
|
@ -196,24 +182,36 @@ const OrderDiscountCommonModal: React.FC<OrderDiscountCommonModalProps> = ({
|
||||||
) => {
|
) => {
|
||||||
const value = event.target.value;
|
const value = event.target.value;
|
||||||
|
|
||||||
handleSetError(value);
|
setValueErrorMsg(getErrorMessage(value));
|
||||||
setValue(value);
|
setValue(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getParsedDiscountValue = () => parseFloat(value) || 0;
|
const getParsedDiscountValue = () => parseFloat(value) || 0;
|
||||||
|
|
||||||
const isAmountTooLarge = () => {
|
const isAmountTooLarge = (value?: string) => {
|
||||||
const topAmount = isDiscountTypePercentage ? 100 : maxAmount;
|
const topAmount = isDiscountTypePercentage ? 100 : maxAmount;
|
||||||
|
|
||||||
|
if (value) {
|
||||||
|
return (parseFloat(value) || 0) > topAmount;
|
||||||
|
}
|
||||||
|
|
||||||
return getParsedDiscountValue() > topAmount;
|
return getParsedDiscountValue() > topAmount;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSetError = (value: string) => {
|
const getErrorMessage = (value: string): string | null => {
|
||||||
const regexToCheck = isDiscountTypePercentage
|
if (isAmountTooLarge(value)) {
|
||||||
? fullNumbersRegex
|
if (calculationMode === DiscountValueTypeEnum.PERCENTAGE) {
|
||||||
: numbersRegex;
|
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 = () => {
|
const handleConfirm = () => {
|
||||||
|
@ -228,7 +226,7 @@ const OrderDiscountCommonModal: React.FC<OrderDiscountCommonModalProps> = ({
|
||||||
setReason(initialData.reason);
|
setReason(initialData.reason);
|
||||||
setValue(initialData.value);
|
setValue(initialData.value);
|
||||||
setCalculationMode(initialData.calculationMode);
|
setCalculationMode(initialData.calculationMode);
|
||||||
setValueError(false);
|
setValueErrorMsg(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(setDefaultValues, [
|
useEffect(setDefaultValues, [
|
||||||
|
@ -246,21 +244,22 @@ const OrderDiscountCommonModal: React.FC<OrderDiscountCommonModalProps> = ({
|
||||||
calculationMode === DiscountValueTypeEnum.FIXED;
|
calculationMode === DiscountValueTypeEnum.FIXED;
|
||||||
|
|
||||||
const recalculatedValueFromPercentageToFixed = (
|
const recalculatedValueFromPercentageToFixed = (
|
||||||
getParsedDiscountValue() *
|
(getParsedDiscountValue() * maxPrice.amount) /
|
||||||
PERMIL *
|
100
|
||||||
maxPrice.amount
|
).toString();
|
||||||
).toFixed(2);
|
|
||||||
|
|
||||||
const recalculatedValueFromFixedToPercentage = Math.round(
|
const recalculatedValueFromFixedToPercentage = (
|
||||||
(getParsedDiscountValue() * (1 / PERMIL)) / maxPrice.amount,
|
(getParsedDiscountValue() / maxPrice.amount) *
|
||||||
|
100
|
||||||
).toString();
|
).toString();
|
||||||
|
|
||||||
const recalculatedValue = changedFromPercentageToFixed
|
const recalculatedValue = changedFromPercentageToFixed
|
||||||
? recalculatedValueFromPercentageToFixed
|
? recalculatedValueFromPercentageToFixed
|
||||||
: recalculatedValueFromFixedToPercentage;
|
: recalculatedValueFromFixedToPercentage;
|
||||||
|
|
||||||
handleSetError(recalculatedValue);
|
setValueErrorMsg(getErrorMessage(recalculatedValue));
|
||||||
setValue(recalculatedValue);
|
setValue(recalculatedValue);
|
||||||
|
previousCalculationMode.current = calculationMode;
|
||||||
};
|
};
|
||||||
|
|
||||||
useUpdateEffect(handleValueConversion, [calculationMode]);
|
useUpdateEffect(handleValueConversion, [calculationMode]);
|
||||||
|
@ -274,70 +273,64 @@ const OrderDiscountCommonModal: React.FC<OrderDiscountCommonModalProps> = ({
|
||||||
calculationMode === DiscountValueTypeEnum.FIXED ? currency : "%";
|
calculationMode === DiscountValueTypeEnum.FIXED ? currency : "%";
|
||||||
|
|
||||||
const isSubmitDisabled =
|
const isSubmitDisabled =
|
||||||
!getParsedDiscountValue() || isValueError || isAmountTooLarge();
|
!getParsedDiscountValue() || !!valueErrorMsg || isAmountTooLarge();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popper
|
<Card>
|
||||||
open={isOpen}
|
<ModalTitle title={intl.formatMessage(dialogTitle)} onClose={onClose} />
|
||||||
anchorEl={anchorRef.current}
|
<CardContent>
|
||||||
className={classes.container}
|
<RadioGroupField
|
||||||
placement={dialogPlacement}
|
innerContainerClassName={classes.radioContainer}
|
||||||
>
|
choices={discountTypeChoices}
|
||||||
<Card>
|
name="discountType"
|
||||||
<ModalTitle title={intl.formatMessage(dialogTitle)} onClose={onClose} />
|
variant="inlineJustify"
|
||||||
<CardContent>
|
value={calculationMode}
|
||||||
<RadioGroupField
|
onChange={event => setCalculationMode(event.target.value)}
|
||||||
innerContainerClassName={classes.radioContainer}
|
/>
|
||||||
choices={discountTypeChoices}
|
<CardSpacer />
|
||||||
name="discountType"
|
<PriceField
|
||||||
variant="inlineJustify"
|
label={intl.formatMessage(messages.discountValueLabel)}
|
||||||
value={calculationMode}
|
error={!!valueErrorMsg}
|
||||||
onChange={event => setCalculationMode(event.target.value)}
|
hint={valueErrorMsg}
|
||||||
/>
|
value={toFixed(value, 2)}
|
||||||
<CardSpacer />
|
onChange={handleSetDiscountValue}
|
||||||
<PriceField
|
currencySymbol={valueFieldSymbol}
|
||||||
label={intl.formatMessage(messages.discountValueLabel)}
|
/>
|
||||||
error={isValueError}
|
<CardSpacer />
|
||||||
hint={isValueError && intl.formatMessage(messages.invalidValue)}
|
<Typography>
|
||||||
value={value}
|
{intl.formatMessage(messages.discountReasonLabel)}
|
||||||
onChange={handleSetDiscountValue}
|
</Typography>
|
||||||
currencySymbol={valueFieldSymbol}
|
<TextField
|
||||||
/>
|
className={classes.reasonInput}
|
||||||
<CardSpacer />
|
label={intl.formatMessage(messages.discountReasonLabel)}
|
||||||
<Typography>
|
value={reason}
|
||||||
{intl.formatMessage(messages.discountReasonLabel)}
|
data-test-id="discount-reason"
|
||||||
</Typography>
|
onChange={(event: ChangeEvent<HTMLInputElement>) =>
|
||||||
<TextField
|
setReason(event.target.value)
|
||||||
className={classes.reasonInput}
|
}
|
||||||
label={intl.formatMessage(messages.discountReasonLabel)}
|
/>
|
||||||
value={reason}
|
</CardContent>
|
||||||
onChange={(event: ChangeEvent<HTMLInputElement>) =>
|
<DialogButtons
|
||||||
setReason(event.target.value)
|
onConfirm={handleConfirm}
|
||||||
}
|
onClose={onClose}
|
||||||
/>
|
disabled={isSubmitDisabled}
|
||||||
</CardContent>
|
showBackButton={false}
|
||||||
<DialogButtons
|
confirmButtonState={confirmStatus}
|
||||||
onConfirm={handleConfirm}
|
>
|
||||||
onClose={onClose}
|
{existingDiscount && (
|
||||||
disabled={isSubmitDisabled}
|
<div className={classes.buttonWrapper}>
|
||||||
showBackButton={false}
|
<ConfirmButton
|
||||||
confirmButtonState={confirmStatus}
|
data-test-id="button-remove"
|
||||||
>
|
onClick={onRemove}
|
||||||
{existingDiscount && (
|
className={classes.removeButton}
|
||||||
<div className={classes.buttonWrapper}>
|
transitionState={removeStatus}
|
||||||
<ConfirmButton
|
>
|
||||||
data-test-id="button-remove"
|
{intl.formatMessage(buttonMessages.remove)}
|
||||||
onClick={onRemove}
|
</ConfirmButton>
|
||||||
className={classes.removeButton}
|
</div>
|
||||||
transitionState={removeStatus}
|
)}
|
||||||
>
|
</DialogButtons>
|
||||||
{intl.formatMessage(buttonMessages.remove)}
|
</Card>
|
||||||
</ConfirmButton>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</DialogButtons>
|
|
||||||
</Card>
|
|
||||||
</Popper>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -22,6 +22,7 @@ interface OrderDraftDetailsProps {
|
||||||
order: OrderDetailsFragment;
|
order: OrderDetailsFragment;
|
||||||
channelUsabilityData?: ChannelUsabilityDataQuery;
|
channelUsabilityData?: ChannelUsabilityDataQuery;
|
||||||
errors: OrderErrorFragment[];
|
errors: OrderErrorFragment[];
|
||||||
|
loading: boolean;
|
||||||
onOrderLineAdd: () => void;
|
onOrderLineAdd: () => void;
|
||||||
onOrderLineChange: (id: string, data: OrderLineInput) => void;
|
onOrderLineChange: (id: string, data: OrderLineInput) => void;
|
||||||
onOrderLineRemove: (id: string) => void;
|
onOrderLineRemove: (id: string) => void;
|
||||||
|
@ -32,6 +33,7 @@ const OrderDraftDetails: React.FC<OrderDraftDetailsProps> = ({
|
||||||
order,
|
order,
|
||||||
channelUsabilityData,
|
channelUsabilityData,
|
||||||
errors,
|
errors,
|
||||||
|
loading,
|
||||||
onOrderLineAdd,
|
onOrderLineAdd,
|
||||||
onOrderLineChange,
|
onOrderLineChange,
|
||||||
onOrderLineRemove,
|
onOrderLineRemove,
|
||||||
|
@ -70,6 +72,7 @@ const OrderDraftDetails: React.FC<OrderDraftDetailsProps> = ({
|
||||||
<OrderDraftDetailsProducts
|
<OrderDraftDetailsProducts
|
||||||
order={order}
|
order={order}
|
||||||
errors={errors}
|
errors={errors}
|
||||||
|
loading={loading}
|
||||||
onOrderLineChange={onOrderLineChange}
|
onOrderLineChange={onOrderLineChange}
|
||||||
onOrderLineRemove={onOrderLineRemove}
|
onOrderLineRemove={onOrderLineRemove}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -0,0 +1,126 @@
|
||||||
|
import ColumnPicker from "@dashboard/components/ColumnPicker";
|
||||||
|
import Datagrid from "@dashboard/components/Datagrid/Datagrid";
|
||||||
|
import { useColumnsDefault } from "@dashboard/components/Datagrid/hooks/useColumnsDefault";
|
||||||
|
import {
|
||||||
|
DatagridChangeOpts,
|
||||||
|
DatagridChangeStateContext,
|
||||||
|
useDatagridChangeState,
|
||||||
|
} from "@dashboard/components/Datagrid/hooks/useDatagridChange";
|
||||||
|
import { OrderDetailsFragment, OrderErrorFragment } from "@dashboard/graphql";
|
||||||
|
import { TrashBinIcon } from "@saleor/macaw-ui/next";
|
||||||
|
import React, { useCallback } from "react";
|
||||||
|
import { useIntl } from "react-intl";
|
||||||
|
|
||||||
|
import { FormData } from "../OrderDraftDetailsProducts/OrderDraftDetailsProducts";
|
||||||
|
import { useColumns, useGetCellContent } from "./datagrid";
|
||||||
|
import { messages } from "./messages";
|
||||||
|
|
||||||
|
interface OrderDraftDetailsDatagridProps {
|
||||||
|
loading: boolean;
|
||||||
|
lines: OrderDetailsFragment["lines"];
|
||||||
|
errors: OrderErrorFragment[];
|
||||||
|
onOrderLineChange: (id: string, data: FormData) => void;
|
||||||
|
onOrderLineRemove: (id: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const OrderDraftDetailsDatagrid = ({
|
||||||
|
lines,
|
||||||
|
errors,
|
||||||
|
onOrderLineChange,
|
||||||
|
onOrderLineRemove,
|
||||||
|
}: OrderDraftDetailsDatagridProps) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const datagrid = useDatagridChangeState();
|
||||||
|
|
||||||
|
const { availableColumns } = useColumns();
|
||||||
|
|
||||||
|
const {
|
||||||
|
availableColumnsChoices,
|
||||||
|
columnChoices,
|
||||||
|
columns,
|
||||||
|
defaultColumns,
|
||||||
|
onColumnMoved,
|
||||||
|
onColumnResize,
|
||||||
|
onColumnsChange,
|
||||||
|
picker,
|
||||||
|
} = useColumnsDefault(availableColumns);
|
||||||
|
|
||||||
|
const getCellContent = useGetCellContent({
|
||||||
|
columns,
|
||||||
|
lines,
|
||||||
|
errors,
|
||||||
|
});
|
||||||
|
|
||||||
|
const getMenuItems = useCallback(
|
||||||
|
index => [
|
||||||
|
{
|
||||||
|
label: intl.formatMessage(messages.deleteOrder),
|
||||||
|
Icon: <TrashBinIcon />,
|
||||||
|
onSelect: () => {
|
||||||
|
onOrderLineRemove(lines[index].id);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[intl, lines, onOrderLineRemove],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleDatagridChange = useCallback(
|
||||||
|
async (
|
||||||
|
{ updates }: DatagridChangeOpts,
|
||||||
|
setMarkCellsDirty: (areCellsDirty: boolean) => void,
|
||||||
|
) => {
|
||||||
|
await Promise.all(
|
||||||
|
updates.map(({ data, column, row }) => {
|
||||||
|
const orderId = lines[row].id;
|
||||||
|
|
||||||
|
if (column === "quantity" && data !== "") {
|
||||||
|
return onOrderLineChange(orderId, { quantity: data });
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
datagrid.changes.current = [];
|
||||||
|
setMarkCellsDirty(false);
|
||||||
|
},
|
||||||
|
[datagrid.changes, lines, onOrderLineChange],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DatagridChangeStateContext.Provider value={datagrid}>
|
||||||
|
<Datagrid
|
||||||
|
rowMarkers="none"
|
||||||
|
columnSelect="none"
|
||||||
|
freezeColumns={2}
|
||||||
|
verticalBorder={col => (col > 1 ? true : false)}
|
||||||
|
availableColumns={columns}
|
||||||
|
emptyText={intl.formatMessage(messages.emptyText)}
|
||||||
|
getCellContent={getCellContent}
|
||||||
|
getCellError={() => false}
|
||||||
|
menuItems={getMenuItems}
|
||||||
|
rows={lines.length}
|
||||||
|
selectionActions={() => null}
|
||||||
|
onColumnResize={onColumnResize}
|
||||||
|
onColumnMoved={onColumnMoved}
|
||||||
|
renderColumnPicker={defaultProps => (
|
||||||
|
<ColumnPicker
|
||||||
|
{...defaultProps}
|
||||||
|
IconButtonProps={{
|
||||||
|
...defaultProps.IconButtonProps,
|
||||||
|
disabled: lines.length === 0,
|
||||||
|
}}
|
||||||
|
availableColumns={availableColumnsChoices}
|
||||||
|
initialColumns={columnChoices}
|
||||||
|
defaultColumns={defaultColumns}
|
||||||
|
onSave={onColumnsChange}
|
||||||
|
hasMore={false}
|
||||||
|
loading={false}
|
||||||
|
onFetchMore={() => undefined}
|
||||||
|
onQueryChange={picker.setQuery}
|
||||||
|
query={picker.query}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
onChange={handleDatagridChange}
|
||||||
|
/>
|
||||||
|
</DatagridChangeStateContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
217
src/orders/components/OrderDraftDetailsDatagrid/datagrid.ts
Normal file
217
src/orders/components/OrderDraftDetailsDatagrid/datagrid.ts
Normal file
|
@ -0,0 +1,217 @@
|
||||||
|
import {
|
||||||
|
moneyCell,
|
||||||
|
moneyDiscountedCell,
|
||||||
|
readonlyTextCell,
|
||||||
|
tagsCell,
|
||||||
|
textCell,
|
||||||
|
thumbnailCell,
|
||||||
|
} from "@dashboard/components/Datagrid/customCells/cells";
|
||||||
|
import { GetCellContentOpts } from "@dashboard/components/Datagrid/Datagrid";
|
||||||
|
import { useEmptyColumn } from "@dashboard/components/Datagrid/hooks/useEmptyColumn";
|
||||||
|
import { AvailableColumn } from "@dashboard/components/Datagrid/types";
|
||||||
|
import { OrderDetailsFragment, OrderErrorFragment } from "@dashboard/graphql";
|
||||||
|
import useLocale from "@dashboard/hooks/useLocale";
|
||||||
|
import {
|
||||||
|
getDatagridRowDataIndex,
|
||||||
|
getStatusColor,
|
||||||
|
isFirstColumn,
|
||||||
|
} from "@dashboard/misc";
|
||||||
|
import { useOrderLineDiscountContext } from "@dashboard/products/components/OrderDiscountProviders/OrderLineDiscountProvider";
|
||||||
|
import getOrderErrorMessage from "@dashboard/utils/errors/order";
|
||||||
|
import { GridCell, Item } from "@glideapps/glide-data-grid";
|
||||||
|
import { DefaultTheme, useTheme } from "@saleor/macaw-ui/next";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
import { IntlShape, useIntl } from "react-intl";
|
||||||
|
|
||||||
|
import { lineAlertMessages } from "../OrderDraftDetailsProducts/messages";
|
||||||
|
import { columnsMessages } from "./messages";
|
||||||
|
|
||||||
|
export const useColumns = () => {
|
||||||
|
const emptyColumn = useEmptyColumn();
|
||||||
|
const intl = useIntl();
|
||||||
|
|
||||||
|
const availableColumns = useMemo(
|
||||||
|
() => [
|
||||||
|
emptyColumn,
|
||||||
|
{
|
||||||
|
id: "product",
|
||||||
|
title: intl.formatMessage(columnsMessages.product),
|
||||||
|
width: 300,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "sku",
|
||||||
|
title: "SKU",
|
||||||
|
width: 150,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "quantity",
|
||||||
|
title: intl.formatMessage(columnsMessages.quantity),
|
||||||
|
width: 80,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "price",
|
||||||
|
title: intl.formatMessage(columnsMessages.price),
|
||||||
|
width: 150,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "total",
|
||||||
|
title: intl.formatMessage(columnsMessages.total),
|
||||||
|
width: 150,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "status",
|
||||||
|
title: "Status",
|
||||||
|
width: 250,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[emptyColumn, intl],
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
availableColumns,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
interface GetCellContentProps {
|
||||||
|
columns: AvailableColumn[];
|
||||||
|
lines: OrderDetailsFragment["lines"];
|
||||||
|
errors: OrderErrorFragment[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useGetCellContent = ({
|
||||||
|
columns,
|
||||||
|
lines,
|
||||||
|
errors,
|
||||||
|
}: GetCellContentProps) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const { theme } = useTheme();
|
||||||
|
const { locale } = useLocale();
|
||||||
|
const getValues = useOrderLineDiscountContext();
|
||||||
|
|
||||||
|
return (
|
||||||
|
[column, row]: Item,
|
||||||
|
{ added, removed, changes, getChangeIndex }: GetCellContentOpts,
|
||||||
|
): GridCell => {
|
||||||
|
if (isFirstColumn(column)) {
|
||||||
|
return readonlyTextCell("", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
const columnId = columns[column].id;
|
||||||
|
const change = changes.current[getChangeIndex(columnId, row)]?.data;
|
||||||
|
const rowData = added.includes(row)
|
||||||
|
? undefined
|
||||||
|
: lines[getDatagridRowDataIndex(row, removed)];
|
||||||
|
|
||||||
|
if (!rowData) {
|
||||||
|
return readonlyTextCell("", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { unitUndiscountedPrice, unitDiscountedPrice } = getValues(
|
||||||
|
rowData.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
switch (columnId) {
|
||||||
|
case "product":
|
||||||
|
return thumbnailCell(
|
||||||
|
rowData?.productName ?? "",
|
||||||
|
rowData.thumbnail?.url ?? "",
|
||||||
|
{
|
||||||
|
readonly: true,
|
||||||
|
allowOverlay: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
case "quantity":
|
||||||
|
return textCell(change || rowData.quantity.toString());
|
||||||
|
case "price":
|
||||||
|
return moneyDiscountedCell(
|
||||||
|
{
|
||||||
|
value: unitDiscountedPrice.amount,
|
||||||
|
currency: unitDiscountedPrice.currency,
|
||||||
|
undiscounted: unitUndiscountedPrice.amount,
|
||||||
|
lineItemId: rowData.id,
|
||||||
|
locale,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
allowOverlay: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
case "status":
|
||||||
|
const orderErrors = getOrderErrors(errors, rowData.id);
|
||||||
|
const status = getOrderLineStatus(intl, rowData, orderErrors);
|
||||||
|
|
||||||
|
return tagsCell(
|
||||||
|
status.map(toTagValue(theme)),
|
||||||
|
status.map(status => status.status),
|
||||||
|
{
|
||||||
|
readonly: true,
|
||||||
|
allowOverlay: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
case "sku":
|
||||||
|
return readonlyTextCell(rowData?.productSku ?? "", false);
|
||||||
|
case "total":
|
||||||
|
return moneyCell(
|
||||||
|
rowData.totalPrice.gross.amount,
|
||||||
|
rowData.totalPrice.gross.currency,
|
||||||
|
{
|
||||||
|
readonly: true,
|
||||||
|
allowOverlay: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
default:
|
||||||
|
return readonlyTextCell("", false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
function toTagValue(currentTheme: DefaultTheme) {
|
||||||
|
return ({ status, type }: OrderStatus) => ({
|
||||||
|
color: getStatusColor(type, currentTheme),
|
||||||
|
tag: status,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OrderStatus {
|
||||||
|
type: "warning" | "error";
|
||||||
|
status: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getOrderLineStatus = (
|
||||||
|
intl: IntlShape,
|
||||||
|
line: OrderDetailsFragment["lines"][number],
|
||||||
|
error?: OrderErrorFragment,
|
||||||
|
): OrderStatus[] => {
|
||||||
|
const statuses = [];
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
statuses.push({
|
||||||
|
type: "error",
|
||||||
|
status: getOrderErrorMessage(error, intl),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const product = line.variant?.product;
|
||||||
|
|
||||||
|
if (!product) {
|
||||||
|
statuses.push({
|
||||||
|
type: "warning",
|
||||||
|
status: intl.formatMessage(lineAlertMessages.notExists),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const isAvailableForPurchase = product?.isAvailableForPurchase;
|
||||||
|
|
||||||
|
if (product && !isAvailableForPurchase) {
|
||||||
|
statuses.push({
|
||||||
|
type: "warning",
|
||||||
|
status: intl.formatMessage(lineAlertMessages.notAvailable),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return statuses;
|
||||||
|
};
|
||||||
|
|
||||||
|
function getOrderErrors(errors: OrderErrorFragment[], id: string) {
|
||||||
|
return errors.find(error => error.orderLines?.some(lineId => lineId === id));
|
||||||
|
}
|
1
src/orders/components/OrderDraftDetailsDatagrid/index.ts
Normal file
1
src/orders/components/OrderDraftDetailsDatagrid/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export * from "./OrderDraftDetailsDatagrid";
|
38
src/orders/components/OrderDraftDetailsDatagrid/messages.ts
Normal file
38
src/orders/components/OrderDraftDetailsDatagrid/messages.ts
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
import { defineMessages } from "react-intl";
|
||||||
|
|
||||||
|
export const columnsMessages = defineMessages({
|
||||||
|
product: {
|
||||||
|
id: "x/ZVlU",
|
||||||
|
defaultMessage: "Product",
|
||||||
|
},
|
||||||
|
quantity: {
|
||||||
|
id: "nEWp+k",
|
||||||
|
defaultMessage: "Quantity",
|
||||||
|
description: "quantity of ordered products",
|
||||||
|
},
|
||||||
|
price: {
|
||||||
|
id: "32dfzI",
|
||||||
|
defaultMessage: "Price",
|
||||||
|
description: "price or ordered products",
|
||||||
|
},
|
||||||
|
total: {
|
||||||
|
id: "lVwmf5",
|
||||||
|
defaultMessage: "Total",
|
||||||
|
description: "total price of ordered products",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const messages = defineMessages({
|
||||||
|
emptyText: {
|
||||||
|
id: "Q1Uzbb",
|
||||||
|
defaultMessage: "No products found",
|
||||||
|
},
|
||||||
|
deleteOrder: {
|
||||||
|
id: "LKD6fB",
|
||||||
|
defaultMessage: "Remove product",
|
||||||
|
},
|
||||||
|
editDiscount: {
|
||||||
|
id: "DhFqJF",
|
||||||
|
defaultMessage: "Edit discount",
|
||||||
|
},
|
||||||
|
});
|
|
@ -1,56 +1,18 @@
|
||||||
import ResponsiveTable from "@dashboard/components/ResponsiveTable";
|
|
||||||
import Skeleton from "@dashboard/components/Skeleton";
|
import Skeleton from "@dashboard/components/Skeleton";
|
||||||
import TableRowLink from "@dashboard/components/TableRowLink";
|
|
||||||
import { OrderDetailsFragment, OrderErrorFragment } from "@dashboard/graphql";
|
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 { makeStyles } from "@saleor/macaw-ui";
|
||||||
import React from "react";
|
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 {
|
export interface FormData {
|
||||||
quantity: number;
|
quantity: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const useStyles = makeStyles(
|
const useStyles = makeStyles(
|
||||||
theme => ({
|
theme => ({
|
||||||
colAction: {
|
|
||||||
width: theme.spacing(10),
|
|
||||||
},
|
|
||||||
colName: {
|
|
||||||
width: "auto",
|
|
||||||
},
|
|
||||||
colNameLabel: {},
|
|
||||||
colPrice: {},
|
|
||||||
colQuantity: {},
|
|
||||||
colTotal: {},
|
|
||||||
skeleton: {
|
skeleton: {
|
||||||
margin: theme.spacing(0, 4),
|
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" },
|
{ name: "OrderDraftDetailsProducts" },
|
||||||
);
|
);
|
||||||
|
@ -58,19 +20,21 @@ const useStyles = makeStyles(
|
||||||
interface OrderDraftDetailsProductsProps {
|
interface OrderDraftDetailsProductsProps {
|
||||||
order?: OrderDetailsFragment;
|
order?: OrderDetailsFragment;
|
||||||
errors: OrderErrorFragment[];
|
errors: OrderErrorFragment[];
|
||||||
|
loading: boolean;
|
||||||
onOrderLineChange: (id: string, data: FormData) => void;
|
onOrderLineChange: (id: string, data: FormData) => void;
|
||||||
onOrderLineRemove: (id: string) => void;
|
onOrderLineRemove: (id: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const OrderDraftDetailsProducts: React.FC<
|
const OrderDraftDetailsProducts: React.FC<OrderDraftDetailsProductsProps> = ({
|
||||||
OrderDraftDetailsProductsProps
|
order,
|
||||||
> = props => {
|
errors,
|
||||||
const { order, errors, onOrderLineChange, onOrderLineRemove } = props;
|
loading,
|
||||||
|
onOrderLineChange,
|
||||||
|
onOrderLineRemove,
|
||||||
|
}) => {
|
||||||
|
const classes = useStyles();
|
||||||
|
|
||||||
const lines = order?.lines ?? [];
|
const lines = order?.lines ?? [];
|
||||||
|
|
||||||
const intl = useIntl();
|
|
||||||
const classes = useStyles(props);
|
|
||||||
|
|
||||||
const formErrors = errors.filter(error => error.field === "lines");
|
const formErrors = errors.filter(error => error.field === "lines");
|
||||||
|
|
||||||
if (order === undefined) {
|
if (order === undefined) {
|
||||||
|
@ -78,79 +42,15 @@ const OrderDraftDetailsProducts: React.FC<
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ResponsiveTable className={classes.table}>
|
<OrderDraftDetailsDatagrid
|
||||||
{!!lines.length && (
|
lines={lines}
|
||||||
<TableHead>
|
loading={loading}
|
||||||
<TableRowLink>
|
onOrderLineRemove={onOrderLineRemove}
|
||||||
<TableCell className={classes.colName} colSpan={2}>
|
onOrderLineChange={onOrderLineChange}
|
||||||
<span className={classes.colNameLabel}>
|
errors={formErrors}
|
||||||
<FormattedMessage id="x/ZVlU" defaultMessage="Product" />
|
/>
|
||||||
</span>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className={classes.colQuantity}>
|
|
||||||
<FormattedMessage
|
|
||||||
id="nEWp+k"
|
|
||||||
defaultMessage="Quantity"
|
|
||||||
description="quantity of ordered products"
|
|
||||||
/>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className={classes.colPrice}>
|
|
||||||
<FormattedMessage
|
|
||||||
id="32dfzI"
|
|
||||||
defaultMessage="Price"
|
|
||||||
description="price or ordered products"
|
|
||||||
/>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className={classes.colTotal}>
|
|
||||||
<FormattedMessage
|
|
||||||
id="lVwmf5"
|
|
||||||
defaultMessage="Total"
|
|
||||||
description="total price of ordered products"
|
|
||||||
/>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className={classes.colAction} />
|
|
||||||
</TableRowLink>
|
|
||||||
</TableHead>
|
|
||||||
)}
|
|
||||||
<TableBody>
|
|
||||||
{!!lines.length ? (
|
|
||||||
renderCollection(lines, line => (
|
|
||||||
<OrderLineDiscountConsumer key={line.id} orderLineId={line.id}>
|
|
||||||
{(
|
|
||||||
orderLineDiscountProps: OrderLineDiscountContextConsumerProps,
|
|
||||||
) => (
|
|
||||||
<TableLine
|
|
||||||
{...orderLineDiscountProps}
|
|
||||||
line={line}
|
|
||||||
error={formErrors.find(error =>
|
|
||||||
error.orderLines?.some(id => id === line.id),
|
|
||||||
)}
|
|
||||||
onOrderLineChange={onOrderLineChange}
|
|
||||||
onOrderLineRemove={onOrderLineRemove}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</OrderLineDiscountConsumer>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<TableRowLink>
|
|
||||||
<TableCell colSpan={5}>
|
|
||||||
<FormattedMessage
|
|
||||||
id="UD7/q8"
|
|
||||||
defaultMessage="No Products added to Order"
|
|
||||||
/>
|
|
||||||
{!!formErrors.length && (
|
|
||||||
<Typography variant="body2" className={classes.errorInfo}>
|
|
||||||
{getOrderErrorMessage(formErrors[0], intl)}
|
|
||||||
</Typography>
|
|
||||||
)}
|
|
||||||
</TableCell>
|
|
||||||
</TableRowLink>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</TableBody>
|
|
||||||
</ResponsiveTable>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
OrderDraftDetailsProducts.displayName = "OrderDraftDetailsProducts";
|
OrderDraftDetailsProducts.displayName = "OrderDraftDetailsProducts";
|
||||||
export default OrderDraftDetailsProducts;
|
export default OrderDraftDetailsProducts;
|
||||||
|
|
|
@ -1,54 +0,0 @@
|
||||||
import { order } from "@dashboard/orders/fixtures";
|
|
||||||
import { render, screen } from "@testing-library/react";
|
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
import TableLine from "./TableLine";
|
|
||||||
|
|
||||||
jest.mock("react-intl", () => ({
|
|
||||||
useIntl: jest.fn(() => ({
|
|
||||||
formatMessage: jest.fn(x => x.defaultMessage),
|
|
||||||
})),
|
|
||||||
defineMessages: jest.fn(x => x),
|
|
||||||
}));
|
|
||||||
|
|
||||||
jest.mock("@saleor/macaw-ui", () => ({
|
|
||||||
useStyles: jest.fn(() => () => ({})),
|
|
||||||
makeStyles: jest.fn(() => () => ({})),
|
|
||||||
Avatar: jest.fn(() => () => <></>),
|
|
||||||
IconButton: jest.fn(() => () => <></>),
|
|
||||||
DeleteIcon: jest.fn(() => () => <></>),
|
|
||||||
ImageIcon: jest.fn(() => () => <></>),
|
|
||||||
}));
|
|
||||||
|
|
||||||
const mockedOrder = order("");
|
|
||||||
|
|
||||||
describe("TableLine rendering", () => {
|
|
||||||
it("renders with values from API", async () => {
|
|
||||||
// Arrange
|
|
||||||
const mockedLine = mockedOrder.lines[0];
|
|
||||||
const props = {
|
|
||||||
line: mockedLine,
|
|
||||||
onOrderLineChange: jest.fn(),
|
|
||||||
onOrderLineRemove: jest.fn(),
|
|
||||||
addOrderLineDiscount: jest.fn(),
|
|
||||||
removeOrderLineDiscount: jest.fn(),
|
|
||||||
orderLineDiscountUpdateStatus: "default" as const,
|
|
||||||
orderLineDiscountRemoveStatus: "default" as const,
|
|
||||||
openDialog: jest.fn(),
|
|
||||||
closeDialog: jest.fn(),
|
|
||||||
isDialogOpen: false,
|
|
||||||
totalDiscountedPrice: mockedLine.totalPrice.gross,
|
|
||||||
unitUndiscountedPrice: mockedLine.undiscountedUnitPrice.gross,
|
|
||||||
unitDiscountedPrice: mockedLine.unitPrice.gross,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Act
|
|
||||||
render(<TableLine {...props} />);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
const tableLine = screen.getByTestId(`table-line-total-${mockedLine.id}`);
|
|
||||||
expect(tableLine).toHaveTextContent(
|
|
||||||
mockedLine.totalPrice.gross.currency.toString(),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,165 +0,0 @@
|
||||||
import Link from "@dashboard/components/Link";
|
|
||||||
import Money from "@dashboard/components/Money";
|
|
||||||
import TableCellAvatar from "@dashboard/components/TableCellAvatar";
|
|
||||||
import { AVATAR_MARGIN } from "@dashboard/components/TableCellAvatar/Avatar";
|
|
||||||
import TableRowLink from "@dashboard/components/TableRowLink";
|
|
||||||
import {
|
|
||||||
OrderErrorFragment,
|
|
||||||
OrderLineFragment,
|
|
||||||
OrderLineInput,
|
|
||||||
} from "@dashboard/graphql";
|
|
||||||
import { OrderLineDiscountContextConsumerProps } from "@dashboard/products/components/OrderDiscountProviders/OrderLineDiscountProvider";
|
|
||||||
import { TableCell, Typography } from "@material-ui/core";
|
|
||||||
import { DeleteIcon, IconButton, makeStyles } from "@saleor/macaw-ui";
|
|
||||||
import clsx from "clsx";
|
|
||||||
import React, { useRef } from "react";
|
|
||||||
|
|
||||||
import { maybe } from "../../../misc";
|
|
||||||
import OrderDiscountCommonModal from "../OrderDiscountCommonModal";
|
|
||||||
import { ORDER_LINE_DISCOUNT } from "../OrderDiscountCommonModal/types";
|
|
||||||
import TableLineAlert from "./TableLineAlert";
|
|
||||||
import TableLineForm from "./TableLineForm";
|
|
||||||
import useLineAlerts from "./useLineAlerts";
|
|
||||||
|
|
||||||
const useStyles = makeStyles(
|
|
||||||
theme => ({
|
|
||||||
colStatusEmpty: {
|
|
||||||
"&:first-child:not(.MuiTableCell-paddingCheckbox)": {
|
|
||||||
paddingRight: 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
colAction: {
|
|
||||||
width: `calc(76px + ${theme.spacing(0.5)})`,
|
|
||||||
},
|
|
||||||
colName: {
|
|
||||||
width: "auto",
|
|
||||||
},
|
|
||||||
colNameLabel: {
|
|
||||||
marginLeft: AVATAR_MARGIN,
|
|
||||||
},
|
|
||||||
colPrice: {
|
|
||||||
textAlign: "right",
|
|
||||||
},
|
|
||||||
colQuantity: {
|
|
||||||
textAlign: "right",
|
|
||||||
},
|
|
||||||
colTotal: {
|
|
||||||
textAlign: "right",
|
|
||||||
},
|
|
||||||
strike: {
|
|
||||||
textDecoration: "line-through",
|
|
||||||
color: theme.palette.grey[400],
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
{ name: "OrderDraftDetailsProducts" },
|
|
||||||
);
|
|
||||||
|
|
||||||
interface TableLineProps extends OrderLineDiscountContextConsumerProps {
|
|
||||||
line: OrderLineFragment;
|
|
||||||
error?: OrderErrorFragment;
|
|
||||||
onOrderLineChange: (id: string, data: OrderLineInput) => void;
|
|
||||||
onOrderLineRemove: (id: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const TableLine: React.FC<TableLineProps> = ({
|
|
||||||
line,
|
|
||||||
error,
|
|
||||||
onOrderLineChange,
|
|
||||||
onOrderLineRemove,
|
|
||||||
orderLineDiscount,
|
|
||||||
addOrderLineDiscount,
|
|
||||||
removeOrderLineDiscount,
|
|
||||||
openDialog,
|
|
||||||
closeDialog,
|
|
||||||
orderLineDiscountRemoveStatus,
|
|
||||||
isDialogOpen,
|
|
||||||
totalDiscountedPrice,
|
|
||||||
unitUndiscountedPrice,
|
|
||||||
unitDiscountedPrice,
|
|
||||||
orderLineDiscountUpdateStatus,
|
|
||||||
}) => {
|
|
||||||
const classes = useStyles();
|
|
||||||
const popperAnchorRef = useRef<HTMLTableRowElement | null>(null);
|
|
||||||
const { id, thumbnail, productName, productSku } = line;
|
|
||||||
|
|
||||||
const alerts = useLineAlerts({
|
|
||||||
line,
|
|
||||||
error,
|
|
||||||
});
|
|
||||||
|
|
||||||
const getUnitPriceLabel = () => {
|
|
||||||
const money = <Money money={unitUndiscountedPrice} />;
|
|
||||||
|
|
||||||
if (!!orderLineDiscount) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Typography className={classes.strike}>{money}</Typography>
|
|
||||||
<Link onClick={openDialog}>
|
|
||||||
<Money money={unitDiscountedPrice} />
|
|
||||||
</Link>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return <Link onClick={openDialog}>{money}</Link>;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TableRowLink key={id}>
|
|
||||||
<TableCell
|
|
||||||
className={clsx({
|
|
||||||
[classes.colStatusEmpty]: !alerts.length,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{!!alerts.length && (
|
|
||||||
<TableLineAlert
|
|
||||||
alerts={alerts}
|
|
||||||
variant={!!error ? "error" : "warning"}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</TableCell>
|
|
||||||
<TableCellAvatar
|
|
||||||
className={classes.colName}
|
|
||||||
thumbnail={maybe(() => thumbnail.url)}
|
|
||||||
>
|
|
||||||
<Typography variant="body2">{productName}</Typography>
|
|
||||||
<Typography variant="caption">{productSku}</Typography>
|
|
||||||
</TableCellAvatar>
|
|
||||||
<TableCell className={classes.colQuantity}>
|
|
||||||
<TableLineForm line={line} onOrderLineChange={onOrderLineChange} />
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className={classes.colPrice} ref={popperAnchorRef}>
|
|
||||||
{getUnitPriceLabel()}
|
|
||||||
<OrderDiscountCommonModal
|
|
||||||
isOpen={isDialogOpen}
|
|
||||||
anchorRef={popperAnchorRef}
|
|
||||||
onClose={closeDialog}
|
|
||||||
modalType={ORDER_LINE_DISCOUNT}
|
|
||||||
maxPrice={unitUndiscountedPrice}
|
|
||||||
onConfirm={addOrderLineDiscount}
|
|
||||||
onRemove={removeOrderLineDiscount}
|
|
||||||
existingDiscount={orderLineDiscount}
|
|
||||||
confirmStatus={orderLineDiscountUpdateStatus}
|
|
||||||
removeStatus={orderLineDiscountRemoveStatus}
|
|
||||||
dialogPlacement="bottom-end"
|
|
||||||
/>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className={classes.colTotal}>
|
|
||||||
<Money
|
|
||||||
money={{
|
|
||||||
amount: totalDiscountedPrice.amount,
|
|
||||||
currency: totalDiscountedPrice.currency,
|
|
||||||
}}
|
|
||||||
data-test-id={`table-line-total-${line.id}`}
|
|
||||||
/>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className={classes.colAction}>
|
|
||||||
<IconButton variant="secondary" onClick={() => onOrderLineRemove(id)}>
|
|
||||||
<DeleteIcon color="primary" />
|
|
||||||
</IconButton>
|
|
||||||
</TableCell>
|
|
||||||
</TableRowLink>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default TableLine;
|
|
|
@ -1,33 +0,0 @@
|
||||||
import { IndicatorOutlined, TooltipMountWrapper } from "@saleor/macaw-ui";
|
|
||||||
import { Tooltip } from "@saleor/macaw-ui/next";
|
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
import OrderAlerts from "../OrderAlerts";
|
|
||||||
|
|
||||||
interface TableLineAlertProps {
|
|
||||||
alerts?: string[];
|
|
||||||
variant: "warning" | "error";
|
|
||||||
}
|
|
||||||
|
|
||||||
const TableLineAlert: React.FC<TableLineAlertProps> = ({ alerts, variant }) => {
|
|
||||||
if (!alerts.length) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const title = <OrderAlerts alerts={alerts} />;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Tooltip>
|
|
||||||
<Tooltip.Trigger>
|
|
||||||
<TooltipMountWrapper>
|
|
||||||
<IndicatorOutlined icon={variant} />
|
|
||||||
</TooltipMountWrapper>
|
|
||||||
</Tooltip.Trigger>
|
|
||||||
<Tooltip.Content side="bottom">
|
|
||||||
<Tooltip.Arrow />
|
|
||||||
{title}
|
|
||||||
</Tooltip.Content>
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
export default TableLineAlert;
|
|
|
@ -1,75 +0,0 @@
|
||||||
import DebounceForm from "@dashboard/components/DebounceForm";
|
|
||||||
import Form from "@dashboard/components/Form";
|
|
||||||
import { OrderLineFragment, OrderLineInput } from "@dashboard/graphql";
|
|
||||||
import createNonNegativeValueChangeHandler from "@dashboard/utils/handlers/nonNegativeValueChangeHandler";
|
|
||||||
import { TextField } from "@material-ui/core";
|
|
||||||
import { makeStyles } from "@saleor/macaw-ui";
|
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
const useStyles = makeStyles(
|
|
||||||
() => ({
|
|
||||||
quantityField: {
|
|
||||||
"& input": {
|
|
||||||
padding: "12px 12px 10px",
|
|
||||||
textAlign: "right",
|
|
||||||
},
|
|
||||||
width: 100,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
{ name: "TableLineForm" },
|
|
||||||
);
|
|
||||||
interface TableLineFormProps {
|
|
||||||
line: OrderLineFragment;
|
|
||||||
onOrderLineChange: (id: string, data: OrderLineInput) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const TableLineForm: React.FC<TableLineFormProps> = ({
|
|
||||||
line,
|
|
||||||
onOrderLineChange,
|
|
||||||
}) => {
|
|
||||||
const classes = useStyles({});
|
|
||||||
const { id, quantity } = line;
|
|
||||||
|
|
||||||
const handleSubmit = (id: string, data: OrderLineInput) => {
|
|
||||||
const quantity = data?.quantity >= 1 ? Math.floor(data.quantity) : 1;
|
|
||||||
onOrderLineChange(id, { quantity });
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Form initial={{ quantity }} onSubmit={data => handleSubmit(id, data)}>
|
|
||||||
{({ change, data, submit, set }) => {
|
|
||||||
const handleQuantityChange = createNonNegativeValueChangeHandler(
|
|
||||||
change,
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DebounceForm
|
|
||||||
change={handleQuantityChange}
|
|
||||||
submit={submit}
|
|
||||||
time={200}
|
|
||||||
>
|
|
||||||
{debounce => (
|
|
||||||
<TextField
|
|
||||||
className={classes.quantityField}
|
|
||||||
fullWidth
|
|
||||||
name="quantity"
|
|
||||||
type="number"
|
|
||||||
value={data.quantity}
|
|
||||||
onChange={debounce}
|
|
||||||
onBlur={() => {
|
|
||||||
if (data.quantity < 1) {
|
|
||||||
set({ quantity: 1 });
|
|
||||||
}
|
|
||||||
submit();
|
|
||||||
}}
|
|
||||||
inputProps={{ min: 1 }}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</DebounceForm>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
</Form>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default TableLineForm;
|
|
|
@ -2,18 +2,18 @@ import { defineMessages } from "react-intl";
|
||||||
|
|
||||||
export const lineAlertMessages = defineMessages({
|
export const lineAlertMessages = defineMessages({
|
||||||
notPublished: {
|
notPublished: {
|
||||||
id: "Oad+ES",
|
id: "QL4a0Z",
|
||||||
defaultMessage: "This product is not published in this channel.",
|
defaultMessage: "Not published in this channel",
|
||||||
description: "alert message",
|
description: "alert message",
|
||||||
},
|
},
|
||||||
notAvailable: {
|
notAvailable: {
|
||||||
id: "zO+l0L",
|
id: "dDCLFW",
|
||||||
defaultMessage: "This product is not available for sale in this channel.",
|
defaultMessage: "Not available for sale this channel",
|
||||||
description: "alert message",
|
description: "alert message",
|
||||||
},
|
},
|
||||||
notExists: {
|
notExists: {
|
||||||
id: "pEMxyy",
|
id: "ZJOX8n",
|
||||||
defaultMessage: "This product does no longer exist.",
|
defaultMessage: "Product no longer exists",
|
||||||
description: "alert message",
|
description: "alert message",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -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;
|
|
|
@ -12,7 +12,8 @@ import { getFormErrors } from "@dashboard/utils/errors";
|
||||||
import getOrderErrorMessage from "@dashboard/utils/errors/order";
|
import getOrderErrorMessage from "@dashboard/utils/errors/order";
|
||||||
import { Typography } from "@material-ui/core";
|
import { Typography } from "@material-ui/core";
|
||||||
import { makeStyles } from "@saleor/macaw-ui";
|
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 { useIntl } from "react-intl";
|
||||||
|
|
||||||
import OrderDiscountCommonModal from "../OrderDiscountCommonModal";
|
import OrderDiscountCommonModal from "../OrderDiscountCommonModal";
|
||||||
|
@ -87,8 +88,6 @@ const OrderDraftDetailsSummary: React.FC<
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const classes = useStyles(props);
|
const classes = useStyles(props);
|
||||||
|
|
||||||
const popperAnchorRef = useRef<HTMLTableRowElement | null>(null);
|
|
||||||
|
|
||||||
if (!order) {
|
if (!order) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -179,36 +178,47 @@ const OrderDraftDetailsSummary: React.FC<
|
||||||
return (
|
return (
|
||||||
<table className={classes.root}>
|
<table className={classes.root}>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr className={classes.relativeRow} ref={popperAnchorRef}>
|
<tr className={classes.relativeRow}>
|
||||||
<td>
|
<td>
|
||||||
<Link onClick={openDialog}>
|
<Popover open={isDialogOpen}>
|
||||||
{intl.formatMessage(discountTitle)}
|
<Popover.Trigger>
|
||||||
</Link>
|
<Button
|
||||||
<OrderDiscountCommonModal
|
variant="tertiary"
|
||||||
dialogPlacement="bottom-start"
|
onClick={isDialogOpen ? closeDialog : openDialog}
|
||||||
modalType={ORDER_DISCOUNT}
|
>
|
||||||
anchorRef={popperAnchorRef}
|
<Link>{intl.formatMessage(discountTitle)}</Link>
|
||||||
existingDiscount={orderDiscount}
|
</Button>
|
||||||
maxPrice={undiscountedPrice}
|
</Popover.Trigger>
|
||||||
isOpen={isDialogOpen}
|
<Popover.Content
|
||||||
onConfirm={addOrderDiscount}
|
align="start"
|
||||||
onClose={closeDialog}
|
className={sprinkles({ zIndex: "3" })}
|
||||||
onRemove={removeOrderDiscount}
|
>
|
||||||
confirmStatus={orderDiscountAddStatus}
|
<Box boxShadow="overlay">
|
||||||
removeStatus={orderDiscountRemoveStatus}
|
<OrderDiscountCommonModal
|
||||||
/>
|
modalType={ORDER_DISCOUNT}
|
||||||
|
existingDiscount={orderDiscount}
|
||||||
|
maxPrice={undiscountedPrice}
|
||||||
|
onConfirm={addOrderDiscount}
|
||||||
|
onClose={closeDialog}
|
||||||
|
onRemove={removeOrderDiscount}
|
||||||
|
confirmStatus={orderDiscountAddStatus}
|
||||||
|
removeStatus={orderDiscountRemoveStatus}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Popover.Content>
|
||||||
|
</Popover>
|
||||||
</td>
|
</td>
|
||||||
<td className={classes.textRight}>
|
<td className={classes.textRight}>
|
||||||
{getOrderDiscountLabel(orderDiscount)}
|
{getOrderDiscountLabel(orderDiscount)}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr data-test-id="order-subtotal-price">
|
||||||
<td>{intl.formatMessage(messages.subtotal)}</td>
|
<td>{intl.formatMessage(messages.subtotal)}</td>
|
||||||
<td className={classes.textRight}>
|
<td className={classes.textRight}>
|
||||||
<Money money={subtotal.gross} />
|
<Money money={subtotal.gross} />
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr data-test-id="order-add-shipping-line">
|
||||||
<td>
|
<td>
|
||||||
{hasShippingMethods && getShippingMethodComponent()}
|
{hasShippingMethods && getShippingMethodComponent()}
|
||||||
|
|
||||||
|
@ -230,13 +240,13 @@ const OrderDraftDetailsSummary: React.FC<
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr data-test-id="order-taxes-price">
|
||||||
<td>{intl.formatMessage(messages.taxes)}</td>
|
<td>{intl.formatMessage(messages.taxes)}</td>
|
||||||
<td className={classes.textRight}>
|
<td className={classes.textRight}>
|
||||||
<Money money={order.total.tax} />
|
<Money money={order.total.tax} />
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr data-test-id="order-total-price">
|
||||||
<td>{intl.formatMessage(messages.total)}</td>
|
<td>{intl.formatMessage(messages.total)}</td>
|
||||||
<td className={classes.textRight}>
|
<td className={classes.textRight}>
|
||||||
<Money money={total.gross} />
|
<Money money={total.gross} />
|
||||||
|
|
|
@ -43,6 +43,7 @@ const order = draftOrder(placeholderImage);
|
||||||
|
|
||||||
const props: Omit<OrderDraftPageProps, "classes"> = {
|
const props: Omit<OrderDraftPageProps, "classes"> = {
|
||||||
...fetchMoreProps,
|
...fetchMoreProps,
|
||||||
|
loading: false,
|
||||||
disabled: false,
|
disabled: false,
|
||||||
fetchUsers: () => undefined,
|
fetchUsers: () => undefined,
|
||||||
onBillingAddressEdit: undefined,
|
onBillingAddressEdit: undefined,
|
||||||
|
|
|
@ -53,7 +53,7 @@ export interface OrderDraftPageProps extends FetchMoreProps {
|
||||||
|
|
||||||
const OrderDraftPage: React.FC<OrderDraftPageProps> = props => {
|
const OrderDraftPage: React.FC<OrderDraftPageProps> = props => {
|
||||||
const {
|
const {
|
||||||
disabled,
|
loading,
|
||||||
fetchUsers,
|
fetchUsers,
|
||||||
hasMore,
|
hasMore,
|
||||||
saveButtonBarState,
|
saveButtonBarState,
|
||||||
|
@ -120,6 +120,7 @@ const OrderDraftPage: React.FC<OrderDraftPageProps> = props => {
|
||||||
order={order as OrderDetailsFragment}
|
order={order as OrderDetailsFragment}
|
||||||
channelUsabilityData={channelUsabilityData}
|
channelUsabilityData={channelUsabilityData}
|
||||||
errors={errors}
|
errors={errors}
|
||||||
|
loading={loading}
|
||||||
onOrderLineAdd={onOrderLineAdd}
|
onOrderLineAdd={onOrderLineAdd}
|
||||||
onOrderLineChange={onOrderLineChange}
|
onOrderLineChange={onOrderLineChange}
|
||||||
onOrderLineRemove={onOrderLineRemove}
|
onOrderLineRemove={onOrderLineRemove}
|
||||||
|
@ -152,7 +153,7 @@ const OrderDraftPage: React.FC<OrderDraftPageProps> = props => {
|
||||||
</DetailPageLayout.RightSidebar>
|
</DetailPageLayout.RightSidebar>
|
||||||
<Savebar
|
<Savebar
|
||||||
state={saveButtonBarState}
|
state={saveButtonBarState}
|
||||||
disabled={disabled}
|
disabled={loading}
|
||||||
onCancel={() => navigate(orderDraftListUrl())}
|
onCancel={() => navigate(orderDraftListUrl())}
|
||||||
onSubmit={onDraftFinalize}
|
onSubmit={onDraftFinalize}
|
||||||
labels={{
|
labels={{
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import TableRowLink from "@dashboard/components/TableRowLink";
|
|
||||||
import { FulfillmentStatus, OrderDetailsFragment } from "@dashboard/graphql";
|
import { FulfillmentStatus, OrderDetailsFragment } from "@dashboard/graphql";
|
||||||
import { getStringOrPlaceholder } from "@dashboard/misc";
|
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 clsx from "clsx";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { FormattedMessage, useIntl } from "react-intl";
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
|
@ -9,8 +9,6 @@ import { FormattedMessage, useIntl } from "react-intl";
|
||||||
import { extraInfoMessages } from "./messages";
|
import { extraInfoMessages } from "./messages";
|
||||||
import useStyles from "./styles";
|
import useStyles from "./styles";
|
||||||
|
|
||||||
const NUMBER_OF_COLUMNS = 5;
|
|
||||||
|
|
||||||
interface ExtraInfoLinesProps {
|
interface ExtraInfoLinesProps {
|
||||||
fulfillment?: OrderDetailsFragment["fulfillments"][0];
|
fulfillment?: OrderDetailsFragment["fulfillments"][0];
|
||||||
}
|
}
|
||||||
|
@ -26,48 +24,51 @@ const ExtraInfoLines: React.FC<ExtraInfoLinesProps> = ({ fulfillment }) => {
|
||||||
const { warehouse, trackingNumber, status } = fulfillment;
|
const { warehouse, trackingNumber, status } = fulfillment;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableRowLink>
|
<Box
|
||||||
<TableCell className={classes.infoRow} colSpan={NUMBER_OF_COLUMNS}>
|
paddingY={7}
|
||||||
<Typography color="textSecondary" variant="body2">
|
borderColor="neutralHighlight"
|
||||||
{warehouse && (
|
borderBottomStyle={"solid"}
|
||||||
<>
|
borderBottomWidth={1}
|
||||||
{intl.formatMessage(
|
>
|
||||||
status === FulfillmentStatus.RETURNED
|
<Typography color="textSecondary" variant="body2">
|
||||||
? extraInfoMessages.restocked
|
{warehouse && (
|
||||||
: extraInfoMessages.fulfilled,
|
<>
|
||||||
)}
|
{intl.formatMessage(
|
||||||
<Typography
|
status === FulfillmentStatus.RETURNED
|
||||||
className={clsx(classes.infoLabel, {
|
? extraInfoMessages.restocked
|
||||||
[classes.infoLabelWithMargin]: !!trackingNumber,
|
: extraInfoMessages.fulfilled,
|
||||||
})}
|
)}
|
||||||
color="textPrimary"
|
<Typography
|
||||||
variant="body2"
|
className={clsx(classes.infoLabel, {
|
||||||
>
|
[classes.infoLabelWithMargin]: !!trackingNumber,
|
||||||
{getStringOrPlaceholder(warehouse?.name)}
|
})}
|
||||||
</Typography>
|
color="textPrimary"
|
||||||
</>
|
variant="body2"
|
||||||
)}
|
>
|
||||||
</Typography>
|
{getStringOrPlaceholder(warehouse?.name)}
|
||||||
<Typography color="textSecondary" variant="body2">
|
</Typography>
|
||||||
{trackingNumber && (
|
</>
|
||||||
<FormattedMessage
|
)}
|
||||||
{...extraInfoMessages.tracking}
|
</Typography>
|
||||||
values={{
|
<Typography color="textSecondary" variant="body2">
|
||||||
trackingNumber: (
|
{trackingNumber && (
|
||||||
<Typography
|
<FormattedMessage
|
||||||
className={classes.infoLabel}
|
{...extraInfoMessages.tracking}
|
||||||
color="textPrimary"
|
values={{
|
||||||
variant="body2"
|
trackingNumber: (
|
||||||
>
|
<Typography
|
||||||
{trackingNumber}
|
className={classes.infoLabel}
|
||||||
</Typography>
|
color="textPrimary"
|
||||||
),
|
variant="body2"
|
||||||
}}
|
>
|
||||||
/>
|
{trackingNumber}
|
||||||
)}
|
</Typography>
|
||||||
</Typography>
|
),
|
||||||
</TableCell>
|
}}
|
||||||
</TableRowLink>
|
/>
|
||||||
|
)}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,17 +1,14 @@
|
||||||
import CardSpacer from "@dashboard/components/CardSpacer";
|
import CardSpacer from "@dashboard/components/CardSpacer";
|
||||||
import ResponsiveTable from "@dashboard/components/ResponsiveTable";
|
|
||||||
import { FulfillmentStatus, OrderDetailsFragment } from "@dashboard/graphql";
|
import { FulfillmentStatus, OrderDetailsFragment } from "@dashboard/graphql";
|
||||||
import TrashIcon from "@dashboard/icons/Trash";
|
import TrashIcon from "@dashboard/icons/Trash";
|
||||||
import { orderHasTransactions } from "@dashboard/orders/types";
|
import { orderHasTransactions } from "@dashboard/orders/types";
|
||||||
import { mergeRepeatedOrderLines } from "@dashboard/orders/utils/data";
|
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 { IconButton } from "@saleor/macaw-ui";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { renderCollection } from "../../../misc";
|
|
||||||
import OrderCardTitle from "../OrderCardTitle";
|
import OrderCardTitle from "../OrderCardTitle";
|
||||||
import TableHeader from "../OrderProductsCardElements/OrderProductsCardHeader";
|
import { OrderDetailsDatagrid } from "../OrderDetailsDatagrid";
|
||||||
import TableLine from "../OrderProductsCardElements/OrderProductsTableRow";
|
|
||||||
import ActionButtons from "./ActionButtons";
|
import ActionButtons from "./ActionButtons";
|
||||||
import ExtraInfoLines from "./ExtraInfoLines";
|
import ExtraInfoLines from "./ExtraInfoLines";
|
||||||
import useStyles from "./styles";
|
import useStyles from "./styles";
|
||||||
|
@ -55,10 +52,12 @@ const OrderFulfilledProductsCard: React.FC<
|
||||||
|
|
||||||
const getLines = () => {
|
const getLines = () => {
|
||||||
if (statusesToMergeLines.includes(fulfillment?.status)) {
|
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 (
|
return (
|
||||||
|
@ -85,15 +84,8 @@ const OrderFulfilledProductsCard: React.FC<
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<ResponsiveTable className={classes.table}>
|
<OrderDetailsDatagrid lines={getLines()} loading={false} />
|
||||||
<TableHeader />
|
<ExtraInfoLines fulfillment={fulfillment} />
|
||||||
<TableBody>
|
|
||||||
{renderCollection(getLines(), line => (
|
|
||||||
<TableLine key={line.id} line={line} />
|
|
||||||
))}
|
|
||||||
</TableBody>
|
|
||||||
<ExtraInfoLines fulfillment={fulfillment} />
|
|
||||||
</ResponsiveTable>
|
|
||||||
<ActionButtons
|
<ActionButtons
|
||||||
orderId={order?.id}
|
orderId={order?.id}
|
||||||
status={fulfillment?.status}
|
status={fulfillment?.status}
|
||||||
|
|
|
@ -27,9 +27,6 @@ const useStyles = makeStyles(
|
||||||
infoLabelWithMargin: {
|
infoLabelWithMargin: {
|
||||||
marginBottom: theme.spacing(),
|
marginBottom: theme.spacing(),
|
||||||
},
|
},
|
||||||
infoRow: {
|
|
||||||
padding: theme.spacing(2, 3),
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
{ name: "OrderFulfilledProductsCard" },
|
{ name: "OrderFulfilledProductsCard" },
|
||||||
);
|
);
|
||||||
|
|
|
@ -8,7 +8,6 @@ import { GetCellContentOpts } from "@dashboard/components/Datagrid/Datagrid";
|
||||||
import { useEmptyColumn } from "@dashboard/components/Datagrid/hooks/useEmptyColumn";
|
import { useEmptyColumn } from "@dashboard/components/Datagrid/hooks/useEmptyColumn";
|
||||||
import { AvailableColumn } from "@dashboard/components/Datagrid/types";
|
import { AvailableColumn } from "@dashboard/components/Datagrid/types";
|
||||||
import { Locale } from "@dashboard/components/Locale";
|
import { Locale } from "@dashboard/components/Locale";
|
||||||
import { formatMoneyAmount } from "@dashboard/components/Money";
|
|
||||||
import { OrderListQuery } from "@dashboard/graphql";
|
import { OrderListQuery } from "@dashboard/graphql";
|
||||||
import useLocale from "@dashboard/hooks/useLocale";
|
import useLocale from "@dashboard/hooks/useLocale";
|
||||||
import {
|
import {
|
||||||
|
@ -128,7 +127,7 @@ export const useGetCellContent = ({ columns, orders }: GetCellContentProps) => {
|
||||||
case "status":
|
case "status":
|
||||||
return getStatusCellContent(intl, themeValues, currentTheme, rowData);
|
return getStatusCellContent(intl, themeValues, currentTheme, rowData);
|
||||||
case "total":
|
case "total":
|
||||||
return getTotalCellContent(locale, rowData);
|
return getTotalCellContent(rowData);
|
||||||
default:
|
default:
|
||||||
return textCell("");
|
return textCell("");
|
||||||
}
|
}
|
||||||
|
@ -213,15 +212,12 @@ export function getStatusCellContent(
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getTotalCellContent(
|
export function getTotalCellContent(
|
||||||
locale: Locale,
|
|
||||||
rowData: RelayToFlat<OrderListQuery["orders"]>[number],
|
rowData: RelayToFlat<OrderListQuery["orders"]>[number],
|
||||||
) {
|
) {
|
||||||
if (rowData?.total?.gross) {
|
if (rowData?.total?.gross) {
|
||||||
return moneyCell(
|
return moneyCell(rowData.total.gross.amount, rowData.total.gross.currency, {
|
||||||
formatMoneyAmount(rowData.total.gross, locale),
|
cursor: "pointer",
|
||||||
rowData.total.gross.currency,
|
});
|
||||||
{ cursor: "pointer" },
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return readonlyTextCell("-");
|
return readonlyTextCell("-");
|
||||||
|
|
|
@ -169,12 +169,12 @@ const OrderProductAddDialog: React.FC<OrderProductAddDialogProps> = props => {
|
||||||
scrollableTarget={scrollableTargetId}
|
scrollableTarget={scrollableTargetId}
|
||||||
>
|
>
|
||||||
<ResponsiveTable key="table">
|
<ResponsiveTable key="table">
|
||||||
<TableBody>
|
<TableBody data-test-id="add-products-table">
|
||||||
{renderCollection(
|
{renderCollection(
|
||||||
productChoicesWithValidVariants,
|
productChoicesWithValidVariants,
|
||||||
(product, productIndex) => (
|
(product, productIndex) => (
|
||||||
<React.Fragment key={product ? product.id : "skeleton"}>
|
<React.Fragment key={product ? product.id : "skeleton"}>
|
||||||
<TableRowLink>
|
<TableRowLink data-test-id="product">
|
||||||
<TableCell
|
<TableCell
|
||||||
padding="checkbox"
|
padding="checkbox"
|
||||||
className={classes.productCheckboxCell}
|
className={classes.productCheckboxCell}
|
||||||
|
@ -199,14 +199,18 @@ const OrderProductAddDialog: React.FC<OrderProductAddDialogProps> = props => {
|
||||||
className={classes.avatar}
|
className={classes.avatar}
|
||||||
thumbnail={maybe(() => product.thumbnail.url)}
|
thumbnail={maybe(() => product.thumbnail.url)}
|
||||||
/>
|
/>
|
||||||
<TableCell className={classes.colName} colSpan={2}>
|
<TableCell
|
||||||
|
className={classes.colName}
|
||||||
|
colSpan={2}
|
||||||
|
data-test-id="product-name"
|
||||||
|
>
|
||||||
{maybe(() => product.name)}
|
{maybe(() => product.name)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRowLink>
|
</TableRowLink>
|
||||||
{maybe(() => product.variants, [])
|
{maybe(() => product.variants, [])
|
||||||
.filter(isValidVariant)
|
.filter(isValidVariant)
|
||||||
.map((variant, variantIndex) => (
|
.map((variant, variantIndex) => (
|
||||||
<TableRowLink key={variant.id}>
|
<TableRowLink key={variant.id} data-test-id="variant">
|
||||||
<TableCell />
|
<TableCell />
|
||||||
<TableCell className={classes.colVariantCheckbox}>
|
<TableCell className={classes.colVariantCheckbox}>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
|
@ -242,7 +246,10 @@ const OrderProductAddDialog: React.FC<OrderProductAddDialogProps> = props => {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className={classes.textRight}>
|
<TableCell
|
||||||
|
className={classes.textRight}
|
||||||
|
data-test-id="variant-price"
|
||||||
|
>
|
||||||
<OrderPriceLabel pricing={variant.pricing} />
|
<OrderPriceLabel pricing={variant.pricing} />
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRowLink>
|
</TableRowLink>
|
||||||
|
|
|
@ -1,22 +1,13 @@
|
||||||
import { Button } from "@dashboard/components/Button";
|
import { Button } from "@dashboard/components/Button";
|
||||||
import CardSpacer from "@dashboard/components/CardSpacer";
|
import CardSpacer from "@dashboard/components/CardSpacer";
|
||||||
import ResponsiveTable from "@dashboard/components/ResponsiveTable";
|
|
||||||
import { OrderLineFragment } from "@dashboard/graphql";
|
import { OrderLineFragment } from "@dashboard/graphql";
|
||||||
import { commonMessages } from "@dashboard/intl";
|
import { commonMessages } from "@dashboard/intl";
|
||||||
import { renderCollection } from "@dashboard/misc";
|
import { Card, CardActions, CardContent, Typography } from "@material-ui/core";
|
||||||
import {
|
|
||||||
Card,
|
|
||||||
CardActions,
|
|
||||||
CardContent,
|
|
||||||
TableBody,
|
|
||||||
Typography,
|
|
||||||
} from "@material-ui/core";
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { FormattedMessage } from "react-intl";
|
import { FormattedMessage } from "react-intl";
|
||||||
|
|
||||||
import OrderCardTitle from "../OrderCardTitle";
|
import OrderCardTitle from "../OrderCardTitle";
|
||||||
import TableHeader from "../OrderProductsCardElements/OrderProductsCardHeader";
|
import { OrderDetailsDatagrid } from "../OrderDetailsDatagrid";
|
||||||
import TableLine from "../OrderProductsCardElements/OrderProductsTableRow";
|
|
||||||
import { useStyles } from "./styles";
|
import { useStyles } from "./styles";
|
||||||
|
|
||||||
interface OrderUnfulfilledProductsCardProps {
|
interface OrderUnfulfilledProductsCardProps {
|
||||||
|
@ -24,13 +15,18 @@ interface OrderUnfulfilledProductsCardProps {
|
||||||
notAllowedToFulfillUnpaid: boolean;
|
notAllowedToFulfillUnpaid: boolean;
|
||||||
lines: OrderLineFragment[];
|
lines: OrderLineFragment[];
|
||||||
onFulfill: () => void;
|
onFulfill: () => void;
|
||||||
|
loading: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const OrderUnfulfilledProductsCard: React.FC<
|
const OrderUnfulfilledProductsCard: React.FC<
|
||||||
OrderUnfulfilledProductsCardProps
|
OrderUnfulfilledProductsCardProps
|
||||||
> = props => {
|
> = ({
|
||||||
const { showFulfillmentAction, notAllowedToFulfillUnpaid, lines, onFulfill } =
|
showFulfillmentAction,
|
||||||
props;
|
notAllowedToFulfillUnpaid,
|
||||||
|
lines,
|
||||||
|
onFulfill,
|
||||||
|
loading,
|
||||||
|
}) => {
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
|
|
||||||
if (!lines.length) {
|
if (!lines.length) {
|
||||||
|
@ -47,14 +43,7 @@ const OrderUnfulfilledProductsCard: React.FC<
|
||||||
className={classes.cardTitle}
|
className={classes.cardTitle}
|
||||||
/>
|
/>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<ResponsiveTable className={classes.table}>
|
<OrderDetailsDatagrid lines={lines} loading={loading} />
|
||||||
<TableHeader />
|
|
||||||
<TableBody>
|
|
||||||
{renderCollection(lines, line => (
|
|
||||||
<TableLine key={line.id} isOrderLine line={line} />
|
|
||||||
))}
|
|
||||||
</TableBody>
|
|
||||||
</ResponsiveTable>
|
|
||||||
{showFulfillmentAction && (
|
{showFulfillmentAction && (
|
||||||
<CardActions className={classes.actions}>
|
<CardActions className={classes.actions}>
|
||||||
<Button
|
<Button
|
||||||
|
|
|
@ -210,6 +210,7 @@ export const OrderDraftDetails: React.FC<OrderDraftDetailsProps> = ({
|
||||||
<OrderDiscountProvider order={order}>
|
<OrderDiscountProvider order={order}>
|
||||||
<OrderLineDiscountProvider order={order}>
|
<OrderLineDiscountProvider order={order}>
|
||||||
<OrderDraftPage
|
<OrderDraftPage
|
||||||
|
loading={loading}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
errors={errors}
|
errors={errors}
|
||||||
onNoteAdd={variables =>
|
onNoteAdd={variables =>
|
||||||
|
@ -224,7 +225,6 @@ export const OrderDraftDetails: React.FC<OrderDraftDetailsProps> = ({
|
||||||
hasMore={users?.data?.search?.pageInfo?.hasNextPage || false}
|
hasMore={users?.data?.search?.pageInfo?.hasNextPage || false}
|
||||||
onFetchMore={loadMoreCustomers}
|
onFetchMore={loadMoreCustomers}
|
||||||
fetchUsers={searchUsers}
|
fetchUsers={searchUsers}
|
||||||
loading={users.loading}
|
|
||||||
usersLoading={users.loading}
|
usersLoading={users.loading}
|
||||||
onCustomerEdit={handleCustomerChange}
|
onCustomerEdit={handleCustomerChange}
|
||||||
onDraftFinalize={() => orderDraftFinalize.mutate({ id })}
|
onDraftFinalize={() => orderDraftFinalize.mutate({ id })}
|
||||||
|
|
|
@ -186,7 +186,7 @@ export const OrderNormalDetails: React.FC<OrderNormalDetailsProps> = ({
|
||||||
/>
|
/>
|
||||||
<OrderDetailsPage
|
<OrderDetailsPage
|
||||||
onOrderReturn={() => navigate(orderReturnUrl(id))}
|
onOrderReturn={() => navigate(orderReturnUrl(id))}
|
||||||
disabled={
|
loading={
|
||||||
updateMetadataOpts.loading || updatePrivateMetadataOpts.loading
|
updateMetadataOpts.loading || updatePrivateMetadataOpts.loading
|
||||||
}
|
}
|
||||||
errors={errors}
|
errors={errors}
|
||||||
|
|
|
@ -183,7 +183,7 @@ export const OrderUnconfirmedDetails: React.FC<
|
||||||
<OrderLineDiscountProvider order={order}>
|
<OrderLineDiscountProvider order={order}>
|
||||||
<OrderDetailsPage
|
<OrderDetailsPage
|
||||||
onOrderReturn={() => navigate(orderReturnUrl(id))}
|
onOrderReturn={() => navigate(orderReturnUrl(id))}
|
||||||
disabled={
|
loading={
|
||||||
updateMetadataOpts.loading || updatePrivateMetadataOpts.loading
|
updateMetadataOpts.loading || updatePrivateMetadataOpts.loading
|
||||||
}
|
}
|
||||||
errors={errors}
|
errors={errors}
|
||||||
|
|
|
@ -9,7 +9,7 @@ import { getDefaultNotifierSuccessErrorData } from "@dashboard/hooks/useNotifier
|
||||||
import { getById } from "@dashboard/misc";
|
import { getById } from "@dashboard/misc";
|
||||||
import { OrderDiscountCommonInput } from "@dashboard/orders/components/OrderDiscountCommonModal/types";
|
import { OrderDiscountCommonInput } from "@dashboard/orders/components/OrderDiscountCommonModal/types";
|
||||||
import { ConfirmButtonTransitionState } from "@saleor/macaw-ui";
|
import { ConfirmButtonTransitionState } from "@saleor/macaw-ui";
|
||||||
import React, { createContext, useState } from "react";
|
import React, { createContext, useContext, useState } from "react";
|
||||||
import { useIntl } from "react-intl";
|
import { useIntl } from "react-intl";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
@ -44,6 +44,16 @@ interface DiscountProviderProps {
|
||||||
export const OrderLineDiscountContext =
|
export const OrderLineDiscountContext =
|
||||||
createContext<GetOrderLineDiscountContextConsumerProps>(null);
|
createContext<GetOrderLineDiscountContextConsumerProps>(null);
|
||||||
|
|
||||||
|
export const useOrderLineDiscountContext = () => {
|
||||||
|
const context = useContext(OrderLineDiscountContext);
|
||||||
|
|
||||||
|
if (context === null) {
|
||||||
|
throw new Error("You are outside order line discount context");
|
||||||
|
}
|
||||||
|
|
||||||
|
return context;
|
||||||
|
};
|
||||||
|
|
||||||
export const OrderLineDiscountProvider: React.FC<DiscountProviderProps> = ({
|
export const OrderLineDiscountProvider: React.FC<DiscountProviderProps> = ({
|
||||||
children,
|
children,
|
||||||
order,
|
order,
|
||||||
|
|
|
@ -78,7 +78,7 @@ export const ProductVariants: React.FC<ProductVariantsProps> = ({
|
||||||
getColumnData(c, channels, warehouses, variantAttributes, intl),
|
getColumnData(c, channels, warehouses, variantAttributes, intl),
|
||||||
)
|
)
|
||||||
: [],
|
: [],
|
||||||
[variantAttributes, warehouses, channels],
|
[variantAttributes, warehouses, channels, intl],
|
||||||
);
|
);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
@ -103,7 +103,7 @@ export const ProductVariants: React.FC<ProductVariantsProps> = ({
|
||||||
searchAttributeValues: onAttributeValuesSearch,
|
searchAttributeValues: onAttributeValuesSearch,
|
||||||
...opts,
|
...opts,
|
||||||
}),
|
}),
|
||||||
[columns, variants],
|
[channels, columns, onAttributeValuesSearch, variants],
|
||||||
);
|
);
|
||||||
|
|
||||||
const getCellError = React.useCallback(
|
const getCellError = React.useCallback(
|
||||||
|
@ -117,7 +117,7 @@ export const ProductVariants: React.FC<ProductVariantsProps> = ({
|
||||||
searchAttributeValues: onAttributeValuesSearch,
|
searchAttributeValues: onAttributeValuesSearch,
|
||||||
...opts,
|
...opts,
|
||||||
}),
|
}),
|
||||||
[columns, variants, errors],
|
[errors, columns, channels, variants, onAttributeValuesSearch],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -70,6 +70,10 @@ const messages = defineMessages({
|
||||||
defaultMessage: "Shipping method is required for this order",
|
defaultMessage: "Shipping method is required for this order",
|
||||||
description: "error message",
|
description: "error message",
|
||||||
},
|
},
|
||||||
|
noZeroValue: {
|
||||||
|
defaultMessage: "Ensure this value is greater than 0.",
|
||||||
|
id: "YzLUXA",
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
function getOrderErrorMessage(
|
function getOrderErrorMessage(
|
||||||
|
@ -104,6 +108,8 @@ function getOrderErrorMessage(
|
||||||
return intl.formatMessage(messages.shippingRequired);
|
return intl.formatMessage(messages.shippingRequired);
|
||||||
case OrderErrorCode.VOID_INACTIVE_PAYMENT:
|
case OrderErrorCode.VOID_INACTIVE_PAYMENT:
|
||||||
return intl.formatMessage(messages.cannotVoid);
|
return intl.formatMessage(messages.cannotVoid);
|
||||||
|
case OrderErrorCode.ZERO_QUANTITY:
|
||||||
|
return intl.formatMessage(messages.noZeroValue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
14
src/utils/toFixed.test.ts
Normal file
14
src/utils/toFixed.test.ts
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import { toFixed } from "./toFixed";
|
||||||
|
|
||||||
|
describe("toFixed", () => {
|
||||||
|
test("should return empty string if num is empty", () => {
|
||||||
|
expect(toFixed("", 2)).toBe("");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should return number with fixed decimal places", () => {
|
||||||
|
expect(toFixed("1.234567", 2)).toBe("1.23");
|
||||||
|
expect(toFixed("1.234567", 3)).toBe("1.234");
|
||||||
|
expect(toFixed("1.234567", 0)).toBe("1");
|
||||||
|
expect(toFixed("1.234567", 1)).toBe("1.2");
|
||||||
|
});
|
||||||
|
});
|
8
src/utils/toFixed.ts
Normal file
8
src/utils/toFixed.ts
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
export function toFixed(num: string | number, fixed: number) {
|
||||||
|
if (num === "" || num === null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const re = new RegExp("^-?\\d+(?:.\\d{0," + (fixed || -1) + "})?");
|
||||||
|
return num.toString().match(re)[0];
|
||||||
|
}
|
Loading…
Reference in a new issue