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 { ORDER_REFUND } from "../../elements/orders/order-refund";
|
||||
import { ORDERS_SELECTORS } from "../../elements/orders/orders-selectors";
|
||||
import { BUTTON_SELECTORS } from "../../elements/shared/button-selectors";
|
||||
import { SHARED_ELEMENTS } from "../../elements/shared/sharedElements";
|
||||
import {
|
||||
BUTTON_SELECTORS,
|
||||
ORDER_GRANT_REFUND,
|
||||
ORDERS_SELECTORS,
|
||||
SHARED_ELEMENTS,
|
||||
} from "../../elements/";
|
||||
import { MESSAGES } from "../../fixtures";
|
||||
import { urlList } from "../../fixtures/urlList";
|
||||
import { ONE_PERMISSION_USERS } from "../../fixtures/users";
|
||||
import {
|
||||
|
@ -22,6 +25,7 @@ import {
|
|||
createFulfilledOrder,
|
||||
createOrder,
|
||||
createReadyToFulfillOrder,
|
||||
createUnconfirmedOrder,
|
||||
} from "../../support/api/utils/ordersUtils";
|
||||
import * as productsUtils from "../../support/api/utils/products/productsUtils";
|
||||
import {
|
||||
|
@ -34,6 +38,12 @@ import {
|
|||
} from "../../support/api/utils/taxesUtils";
|
||||
import { selectChannelInPicker } from "../../support/pages/channelsPage";
|
||||
import { finalizeDraftOrder } from "../../support/pages/draftOrderPage";
|
||||
import {
|
||||
addNewProductToOrder,
|
||||
applyFixedLineDiscountForProduct,
|
||||
changeQuantityOfProducts,
|
||||
deleteProductFromGridTableOnIndex,
|
||||
} from "../../support/pages/ordersOperations";
|
||||
|
||||
describe("Orders", () => {
|
||||
const startsWith = "CyOrders-";
|
||||
|
@ -47,6 +57,9 @@ describe("Orders", () => {
|
|||
let address;
|
||||
let taxClass;
|
||||
|
||||
const shippingPrice = 2;
|
||||
const variantPrice = 1;
|
||||
|
||||
before(() => {
|
||||
cy.clearSessionData().loginUserViaRequest();
|
||||
deleteCustomersStartsWith(startsWith);
|
||||
|
@ -73,6 +86,7 @@ describe("Orders", () => {
|
|||
createShipping({
|
||||
channelId: defaultChannel.id,
|
||||
name: randomName,
|
||||
price: shippingPrice,
|
||||
address,
|
||||
taxClassId: taxClass.id,
|
||||
});
|
||||
|
@ -95,6 +109,7 @@ describe("Orders", () => {
|
|||
productsUtils.createProductInChannel({
|
||||
name: randomName,
|
||||
channelId: defaultChannel.id,
|
||||
price: variantPrice,
|
||||
warehouseId: warehouse.id,
|
||||
productTypeId: productTypeResp.id,
|
||||
attributeId: attributeResp.id,
|
||||
|
@ -206,7 +221,7 @@ describe("Orders", () => {
|
|||
cy.visit(urlList.orders + `${order.id}`);
|
||||
cy.get(ORDERS_SELECTORS.refundButton)
|
||||
.click()
|
||||
.get(ORDER_REFUND.productsQuantityInput)
|
||||
.get(ORDER_GRANT_REFUND.productsQuantityInput)
|
||||
.type("1")
|
||||
.addAliasToGraphRequest("OrderFulfillmentRefundProducts");
|
||||
cy.get(BUTTON_SELECTORS.submit)
|
||||
|
@ -221,4 +236,103 @@ describe("Orders", () => {
|
|||
});
|
||||
},
|
||||
);
|
||||
|
||||
it(
|
||||
"should add line item discount (for single product in order) . TC: SALEOR_2125",
|
||||
{ tags: ["@orders", "@allEnv", "@stable"] },
|
||||
() => {
|
||||
const totalPrice = variantPrice + shippingPrice;
|
||||
const inlineDiscount = 0.5;
|
||||
const discountReason = "product damaged";
|
||||
createUnconfirmedOrder({
|
||||
customerId: customer.id,
|
||||
channelId: defaultChannel.id,
|
||||
shippingMethod,
|
||||
variantsList,
|
||||
address,
|
||||
}).then(unconfirmedOrderResponse => {
|
||||
cy.visit(urlList.orders + `${unconfirmedOrderResponse.order.id}`);
|
||||
applyFixedLineDiscountForProduct(inlineDiscount, discountReason);
|
||||
cy.get(ORDERS_SELECTORS.priceCellFirstRowOrderDetails).should(
|
||||
"have.text",
|
||||
inlineDiscount,
|
||||
);
|
||||
cy.get(ORDERS_SELECTORS.orderSummarySubtotalPriceRow).should(
|
||||
"contain.text",
|
||||
variantPrice - inlineDiscount,
|
||||
);
|
||||
cy.get(ORDERS_SELECTORS.orderSummaryTotalPriceRow).should(
|
||||
"contain.text",
|
||||
totalPrice - inlineDiscount,
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it(
|
||||
"should remove product from unconfirmed order . TC: SALEOR_2126",
|
||||
{ tags: ["@orders", "@allEnv", "@stable"] },
|
||||
() => {
|
||||
createUnconfirmedOrder({
|
||||
customerId: customer.id,
|
||||
channelId: defaultChannel.id,
|
||||
shippingMethod,
|
||||
variantsList,
|
||||
address,
|
||||
}).then(unconfirmedOrderResponse => {
|
||||
cy.visit(urlList.orders + `${unconfirmedOrderResponse.order.id}`);
|
||||
deleteProductFromGridTableOnIndex(0);
|
||||
cy.contains(MESSAGES.noProductFound).should("be.visible");
|
||||
cy.get(ORDERS_SELECTORS.productDeleteFromRowButton).should("not.exist");
|
||||
});
|
||||
},
|
||||
);
|
||||
it(
|
||||
"should change quantity of products on order detail view . TC: SALEOR_2127",
|
||||
{ tags: ["@orders", "@allEnv", "@stable"] },
|
||||
() => {
|
||||
createUnconfirmedOrder({
|
||||
customerId: customer.id,
|
||||
channelId: defaultChannel.id,
|
||||
shippingMethod,
|
||||
variantsList,
|
||||
address,
|
||||
}).then(unconfirmedOrderResponse => {
|
||||
cy.visit(urlList.orders + `${unconfirmedOrderResponse.order.id}`);
|
||||
|
||||
changeQuantityOfProducts();
|
||||
|
||||
cy.get(ORDERS_SELECTORS.orderSummarySubtotalPriceRow).should(
|
||||
"contain.text",
|
||||
variantPrice * 2,
|
||||
);
|
||||
cy.get(ORDERS_SELECTORS.orderSummaryTotalPriceRow).should(
|
||||
"contain.text",
|
||||
shippingPrice + variantPrice * 2,
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
it(
|
||||
"should add new product on order detail view . TC: SALEOR_2128",
|
||||
{ tags: ["@orders", "@allEnv", "@stable"] },
|
||||
() => {
|
||||
createUnconfirmedOrder({
|
||||
customerId: customer.id,
|
||||
channelId: defaultChannel.id,
|
||||
shippingMethod,
|
||||
variantsList,
|
||||
address,
|
||||
}).then(unconfirmedOrderResponse => {
|
||||
cy.visit(urlList.orders + `${unconfirmedOrderResponse.order.id}`);
|
||||
cy.get(ORDERS_SELECTORS.dataGridTable).should("be.visible");
|
||||
addNewProductToOrder().then(productName => {
|
||||
cy.get(ORDERS_SELECTORS.productNameSecondRowOrderDetails).should(
|
||||
"contain.text",
|
||||
productName,
|
||||
);
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
import faker from "faker";
|
||||
|
||||
import { BUTTON_SELECTORS, PRODUCTS_LIST } from "../../../elements/";
|
||||
import { DIALOGS_MESSAGES } from "../../../fixtures/";
|
||||
import { MESSAGES } from "../../../fixtures/";
|
||||
import { urlList } from "../../../fixtures/urlList";
|
||||
import { getDefaultChannel, productsUtils } from "../../../support/api/utils/";
|
||||
import { ensureCanvasStatic } from "../../../support/customCommands/sharedElementsOperations/canvas";
|
||||
|
@ -55,9 +55,7 @@ describe("Test for deleting products", () => {
|
|||
cy.clickGridCell(0, 0);
|
||||
cy.clickGridCell(0, 1);
|
||||
cy.clickOnElement(BUTTON_SELECTORS.deleteProductsButton);
|
||||
cy.contains(DIALOGS_MESSAGES.confirmProductsDeletion).should(
|
||||
"be.visible",
|
||||
);
|
||||
cy.contains(MESSAGES.confirmProductsDeletion).should("be.visible");
|
||||
cy.addAliasToGraphRequest("productBulkDelete")
|
||||
.clickSubmitButton()
|
||||
.waitForRequestAndCheckIfNoErrors("@productBulkDelete");
|
||||
|
|
|
@ -24,7 +24,7 @@ export const ATTRIBUTES_DETAILS = {
|
|||
entityTypeOptions: {
|
||||
PRODUCT: '[data-test-id="select-field-option-PRODUCT"]',
|
||||
PRODUCT_VARIANT: '[data-test-id="select-field-option-PRODUCT_VARIANT"]',
|
||||
PAGE: '[data-test-id*="PAGE"]',
|
||||
PAGE: '[data-test-id="select-field-option-PAGE"]',
|
||||
},
|
||||
selectUnitCheckbox: '[name="selectUnit"]',
|
||||
unitSystemSelect: '[data-test-id="unit-system"]',
|
||||
|
|
|
@ -22,6 +22,8 @@ export { SALES_SELECTORS, VOUCHERS_SELECTORS } from "./discounts";
|
|||
export { HOMEPAGE_SELECTORS } from "./homePage/homePage-selectors";
|
||||
export { PAGINATION } from "./navigation";
|
||||
export {
|
||||
ADD_PRODUCT_TO_ORDER_DIALOG,
|
||||
DRAFT_ORDER_SELECTORS,
|
||||
DRAFT_ORDERS_LIST_SELECTORS,
|
||||
ORDER_GRANT_REFUND,
|
||||
ORDER_TRANSACTION_CREATE,
|
||||
|
|
|
@ -1,4 +1,9 @@
|
|||
export { DRAFT_ORDERS_LIST_SELECTORS } from "./draft-orders-list-selectors";
|
||||
export {
|
||||
ADD_PRODUCT_TO_ORDER_DIALOG,
|
||||
ORDERS_SELECTORS,
|
||||
} from "./orders-selectors";
|
||||
|
||||
export { DRAFT_ORDER_SELECTORS } from "./draft-order-selectors";
|
||||
export { ORDER_GRANT_REFUND } from "./order-grant-refund";
|
||||
export { ORDERS_SELECTORS } from "./orders-selectors";
|
||||
export { ORDER_TRANSACTION_CREATE } from "./transaction-selectors";
|
||||
|
|
|
@ -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']",
|
||||
refundButton: '[data-test-id="refund-button"]',
|
||||
fulfillMenuButton: '[data-test-id="fulfill-menu"]',
|
||||
priceCellFirstRowOrderDetails: "[id='glide-cell-4-0']",
|
||||
productNameSecondRowOrderDetails: "[id='glide-cell-1-1']",
|
||||
quantityCellFirstRowOrderDetails: "[id='glide-cell-3-0']",
|
||||
discountFixedPriceButton: '[data-test-id="FIXED"]',
|
||||
discountAmountField: '[data-test-id="price-field"]',
|
||||
discountReasonField: '[data-test-id="discount-reason"]',
|
||||
orderSummarySubtotalPriceRow: '[data-test-id="order-subtotal-price"]',
|
||||
orderSummaryTotalPriceRow: '[data-test-id="order-total-price"]',
|
||||
dataGridTable: "[data-testid='data-grid-canvas']",
|
||||
productDeleteFromRowButton: "[data-test-id='row-action-button']",
|
||||
markAsPaidButton: '[data-test-id="markAsPaidButton"]',
|
||||
grantRefundButton: '[data-test-id="grantRefundButton"]',
|
||||
transactionReferenceInput: '[data-test-id="transaction-reference-input"]',
|
||||
orderTransactionsList: '[data-test-id="orderTransactionsList"]',
|
||||
grantRefundButton: '[data-test-id="grantRefundButton"]',
|
||||
captureManualTransactionButton:
|
||||
'[data-test-id="captureManualTransactionButton"]',
|
||||
};
|
||||
export const ADD_PRODUCT_TO_ORDER_DIALOG = {
|
||||
productRow: "[data-test-id='product']",
|
||||
productName: "[data-test-id='product-name']",
|
||||
productVariant: "[data-test-id='variant']",
|
||||
checkbox: "[data-test-id='checkbox']",
|
||||
};
|
||||
|
|
|
@ -16,6 +16,7 @@ export const SHARED_ELEMENTS = {
|
|||
selectOption: '[data-test-id*="select-field-option"]',
|
||||
svgImage: "svg",
|
||||
fileInput: 'input[type="file"]',
|
||||
pageHeader: '[data-test-id="page-header"]',
|
||||
urlInput: 'input[type="url"]',
|
||||
richTextEditor: {
|
||||
loader: '[class*="codex-editor__loader"]',
|
||||
|
|
|
@ -2,4 +2,4 @@ export { bodyMockHomePage } from "./bodyMocks";
|
|||
export { orderDraftCreateDemoResponse } from "./errors/demo/orderDratCreate";
|
||||
export { urlList } from "./urlList";
|
||||
export { ONE_PERMISSION_USERS } from "./users";
|
||||
export { DIALOGS_MESSAGES } from "./messages";
|
||||
export { MESSAGES } from "./messages";
|
||||
|
|
|
@ -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?",
|
||||
};
|
||||
|
|
|
@ -208,6 +208,28 @@ function assignVariantsToOrder(order, variantsList) {
|
|||
orderRequest.addProductToOrder(order.id, variantElement.id);
|
||||
});
|
||||
}
|
||||
export function createUnconfirmedOrder({
|
||||
customerId,
|
||||
shippingMethod,
|
||||
channelId,
|
||||
variantsList,
|
||||
address,
|
||||
}) {
|
||||
let order;
|
||||
return orderRequest
|
||||
.createDraftOrder({ customerId, channelId, address })
|
||||
.then(orderResp => {
|
||||
order = orderResp;
|
||||
assignVariantsToOrder(order, variantsList);
|
||||
})
|
||||
.then(orderResp => {
|
||||
shippingMethod = getShippingMethodIdFromCheckout(
|
||||
orderResp.order,
|
||||
shippingMethod.name,
|
||||
);
|
||||
orderRequest.addShippingMethod(order.id, shippingMethod);
|
||||
});
|
||||
}
|
||||
|
||||
export function addPayment(checkoutId) {
|
||||
return checkoutRequest.addPayment({
|
||||
|
|
|
@ -1,4 +1,10 @@
|
|||
import { CHANNEL_FORM_SELECTORS, ORDERS_SELECTORS } from "../../elements";
|
||||
import {
|
||||
ADD_PRODUCT_TO_ORDER_DIALOG,
|
||||
CHANNEL_FORM_SELECTORS,
|
||||
DRAFT_ORDER_SELECTORS,
|
||||
ORDERS_SELECTORS,
|
||||
SHARED_ELEMENTS,
|
||||
} from "../../elements";
|
||||
|
||||
export function pickAndSelectChannelOnCreateOrderFormByIndex(index) {
|
||||
cy.get(ORDERS_SELECTORS.createOrderButton)
|
||||
|
@ -9,3 +15,57 @@ export function pickAndSelectChannelOnCreateOrderFormByIndex(index) {
|
|||
.eq(index)
|
||||
.click();
|
||||
}
|
||||
export function applyFixedLineDiscountForProduct(
|
||||
discountAmount,
|
||||
discountReason,
|
||||
) {
|
||||
cy.get(ORDERS_SELECTORS.dataGridTable).should("be.visible");
|
||||
cy.get(ORDERS_SELECTORS.priceCellFirstRowOrderDetails)
|
||||
.dblclick({ force: true })
|
||||
.type("2", { force: true });
|
||||
cy.get(ORDERS_SELECTORS.discountFixedPriceButton).click();
|
||||
cy.get(ORDERS_SELECTORS.discountAmountField).type(discountAmount);
|
||||
cy.get(ORDERS_SELECTORS.discountReasonField).type(discountReason);
|
||||
cy.addAliasToGraphRequest("OrderLineDiscountUpdate")
|
||||
.clickSubmitButton()
|
||||
.click()
|
||||
.waitForRequestAndCheckIfNoErrors("@OrderLineDiscountUpdate");
|
||||
}
|
||||
export function changeQuantityOfProducts() {
|
||||
cy.get(ORDERS_SELECTORS.dataGridTable).should("be.visible");
|
||||
cy.get(ORDERS_SELECTORS.quantityCellFirstRowOrderDetails)
|
||||
.dblclick({ force: true })
|
||||
.type("2", { force: true });
|
||||
cy.addAliasToGraphRequest("OrderLineUpdate")
|
||||
// grid expects focus to be dismissed from cell - because of that extra action needed which blur focus from cell (other more elegant build in actions was not working)
|
||||
.get(SHARED_ELEMENTS.pageHeader)
|
||||
.click()
|
||||
.waitForRequestAndCheckIfNoErrors("@OrderLineUpdate");
|
||||
}
|
||||
export function deleteProductFromGridTableOnIndex(trIndex = 0) {
|
||||
cy.get(ORDERS_SELECTORS.dataGridTable).should("be.visible");
|
||||
cy.addAliasToGraphRequest("OrderLineDelete")
|
||||
.get(ORDERS_SELECTORS.productDeleteFromRowButton)
|
||||
.eq(trIndex)
|
||||
.click()
|
||||
.wait("@OrderLineDelete");
|
||||
}
|
||||
export function addNewProductToOrder(productIndex = 0, variantIndex = 0) {
|
||||
cy.get(DRAFT_ORDER_SELECTORS.addProducts).click();
|
||||
return cy
|
||||
.get(ADD_PRODUCT_TO_ORDER_DIALOG.productRow)
|
||||
.eq(productIndex)
|
||||
.find(ADD_PRODUCT_TO_ORDER_DIALOG.productName)
|
||||
.invoke("text")
|
||||
.then(productName => {
|
||||
cy.get(ADD_PRODUCT_TO_ORDER_DIALOG.productVariant)
|
||||
.eq(variantIndex)
|
||||
.find(ADD_PRODUCT_TO_ORDER_DIALOG.checkbox)
|
||||
.click();
|
||||
cy.addAliasToGraphRequest("OrderLinesAdd")
|
||||
.get('[type="submit"]')
|
||||
.click()
|
||||
.waitForRequestAndCheckIfNoErrors("@OrderLinesAdd");
|
||||
cy.wrap(productName);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -2134,6 +2134,9 @@
|
|||
"context": "checkbox label",
|
||||
"string": "Restrict order value"
|
||||
},
|
||||
"DhFqJF": {
|
||||
"string": "Edit discount"
|
||||
},
|
||||
"Dhherd": {
|
||||
"context": "dialog header, title",
|
||||
"string": "Invoice Generation"
|
||||
|
@ -3237,6 +3240,9 @@
|
|||
"context": "cancelled fulfillment, section header",
|
||||
"string": "Refunded and Returned"
|
||||
},
|
||||
"LKD6fB": {
|
||||
"string": "Remove product"
|
||||
},
|
||||
"LKoIB1": {
|
||||
"string": "Add search engine title and description to make this product easier to find"
|
||||
},
|
||||
|
@ -3647,10 +3653,6 @@
|
|||
"context": "input label",
|
||||
"string": "Stock reservation for authenticated user (in minutes)"
|
||||
},
|
||||
"Oad+ES": {
|
||||
"context": "alert message",
|
||||
"string": "This product is not published in this channel."
|
||||
},
|
||||
"ObRk1O": {
|
||||
"string": "If this option is disabled, discount will be counted for every eligible product"
|
||||
},
|
||||
|
@ -3924,6 +3926,10 @@
|
|||
"context": "staff added type order discount",
|
||||
"string": "Staff added"
|
||||
},
|
||||
"QL4a0Z": {
|
||||
"context": "alert message",
|
||||
"string": "Not published in this channel"
|
||||
},
|
||||
"QLVddq": {
|
||||
"context": "button",
|
||||
"string": "Create customer"
|
||||
|
@ -4428,9 +4434,6 @@
|
|||
"context": "searchbar placeholder",
|
||||
"string": "Country"
|
||||
},
|
||||
"UD7/q8": {
|
||||
"string": "No Products added to Order"
|
||||
},
|
||||
"UJnqdm": {
|
||||
"context": "dialog header",
|
||||
"string": "Unassign Attribute From Product Type"
|
||||
|
@ -4618,6 +4621,10 @@
|
|||
"context": "error message",
|
||||
"string": "Default shipping zone already exists"
|
||||
},
|
||||
"VIdXPy": {
|
||||
"context": "value input helper text",
|
||||
"string": "Cannot be higher than the price"
|
||||
},
|
||||
"VKWPBf": {
|
||||
"context": "dialog header",
|
||||
"string": "Delete Staff User Avatar"
|
||||
|
@ -5153,6 +5160,9 @@
|
|||
"Yy/yDL": {
|
||||
"string": "Reset password"
|
||||
},
|
||||
"YzLUXA": {
|
||||
"string": "Ensure this value is greater than 0."
|
||||
},
|
||||
"Z/7hyu": {
|
||||
"context": "card balance label",
|
||||
"string": "Card Balance"
|
||||
|
@ -5195,6 +5205,10 @@
|
|||
"ZIc5lM": {
|
||||
"string": "Product Name"
|
||||
},
|
||||
"ZJOX8n": {
|
||||
"context": "alert message",
|
||||
"string": "Product no longer exists"
|
||||
},
|
||||
"ZJPYFl": {
|
||||
"context": "accepted header names",
|
||||
"string": "Headers with in following format are accepted: <code>authorization*</code>, <code>x-*</code>"
|
||||
|
@ -5737,6 +5751,10 @@
|
|||
"context": "error message",
|
||||
"string": "Cannot change the quantity because of insufficient stock"
|
||||
},
|
||||
"dDCLFW": {
|
||||
"context": "alert message",
|
||||
"string": "Not available for sale this channel"
|
||||
},
|
||||
"dEUZg2": {
|
||||
"context": "header",
|
||||
"string": "Menu Items"
|
||||
|
@ -7221,10 +7239,6 @@
|
|||
"context": "gift card history message",
|
||||
"string": "Gift card was activated"
|
||||
},
|
||||
"pEMxyy": {
|
||||
"context": "alert message",
|
||||
"string": "This product does no longer exist."
|
||||
},
|
||||
"pFVX6g": {
|
||||
"string": "Variant with these attributes already exists"
|
||||
},
|
||||
|
@ -8557,6 +8571,10 @@
|
|||
"context": "modal header",
|
||||
"string": "Media from the URL you supply will be shown in the media gallery. You will be able to define the order of the gallery."
|
||||
},
|
||||
"zHx85l": {
|
||||
"context": "value input helper text",
|
||||
"string": "Cannot be higher than 100%"
|
||||
},
|
||||
"zKOGkU": {
|
||||
"context": "time during discount is active, header",
|
||||
"string": "Active Dates"
|
||||
|
@ -8565,10 +8583,6 @@
|
|||
"context": "gift card removed success alert message",
|
||||
"string": "{selectedItemsCount,plural,one{Successfully deleted gift card} other{Successfully deleted gift cards}}"
|
||||
},
|
||||
"zO+l0L": {
|
||||
"context": "alert message",
|
||||
"string": "This product is not available for sale in this channel."
|
||||
},
|
||||
"zQX6xO": {
|
||||
"context": "dialog header",
|
||||
"string": "Delete App"
|
||||
|
|
|
@ -35,6 +35,7 @@ const Checkbox: React.FC<CheckboxProps> = ({ helperText, error, ...props }) => {
|
|||
return (
|
||||
<>
|
||||
<MuiCheckbox
|
||||
data-test-id="checkbox"
|
||||
{...rest}
|
||||
onClick={
|
||||
disableClickPropagation
|
||||
|
|
|
@ -102,6 +102,7 @@ export interface DatagridProps {
|
|||
freezeColumns?: DataEditorProps["freezeColumns"];
|
||||
verticalBorder?: DataEditorProps["verticalBorder"];
|
||||
columnSelect?: DataEditorProps["columnSelect"];
|
||||
showEmptyDatagrid?: boolean;
|
||||
rowAnchor?: (item: Item) => string;
|
||||
}
|
||||
|
||||
|
@ -128,6 +129,7 @@ export const Datagrid: React.FC<DatagridProps> = ({
|
|||
columnSelect = "none",
|
||||
onColumnMoved,
|
||||
onColumnResize,
|
||||
showEmptyDatagrid = false,
|
||||
loading,
|
||||
rowAnchor,
|
||||
hasRowHover = false,
|
||||
|
@ -156,6 +158,7 @@ export const Datagrid: React.FC<DatagridProps> = ({
|
|||
|
||||
const [selection, setSelection] = useState<GridSelection>();
|
||||
const [hoverRow, setHoverRow] = useState<number | undefined>(undefined);
|
||||
const [areCellsDirty, setCellsDirty] = useState(true);
|
||||
|
||||
// Allow to listen to which row is selected and notfiy parent component
|
||||
useEffect(() => {
|
||||
|
@ -178,7 +181,12 @@ export const Datagrid: React.FC<DatagridProps> = ({
|
|||
removed,
|
||||
getChangeIndex,
|
||||
onRowAdded,
|
||||
} = useDatagridChange(availableColumns, rows, onChange);
|
||||
} = useDatagridChange(
|
||||
availableColumns,
|
||||
rows,
|
||||
onChange,
|
||||
(areCellsDirty: boolean) => setCellsDirty(areCellsDirty),
|
||||
);
|
||||
|
||||
const rowsTotal = rows - removed.length + added.length;
|
||||
const hasMenuItem = !!menuItems(0).length;
|
||||
|
@ -197,7 +205,7 @@ export const Datagrid: React.FC<DatagridProps> = ({
|
|||
|
||||
return {
|
||||
...getCellContent(item, opts),
|
||||
...(changed
|
||||
...(changed && areCellsDirty
|
||||
? {
|
||||
themeOverride: {
|
||||
bgCell: themeValues.colors.background.surfaceBrandSubdued,
|
||||
|
@ -214,13 +222,15 @@ export const Datagrid: React.FC<DatagridProps> = ({
|
|||
};
|
||||
},
|
||||
[
|
||||
availableColumns,
|
||||
changes,
|
||||
added,
|
||||
removed,
|
||||
getChangeIndex,
|
||||
availableColumns,
|
||||
getCellContent,
|
||||
themeValues,
|
||||
areCellsDirty,
|
||||
themeValues.colors.background.surfaceBrandSubdued,
|
||||
themeValues.colors.background.surfaceCriticalDepressed,
|
||||
getCellError,
|
||||
],
|
||||
);
|
||||
|
@ -452,7 +462,7 @@ export const Datagrid: React.FC<DatagridProps> = ({
|
|||
</Header>
|
||||
)}
|
||||
<CardContent classes={{ root: classes.cardContentRoot }}>
|
||||
{rowsTotal > 0 ? (
|
||||
{rowsTotal > 0 || showEmptyDatagrid ? (
|
||||
<>
|
||||
{selection?.rows && selection?.rows.length > 0 && (
|
||||
<div className={classes.actionBtnBar}>
|
||||
|
@ -549,16 +559,6 @@ export const Datagrid: React.FC<DatagridProps> = ({
|
|||
}
|
||||
rowMarkerWidth={48}
|
||||
/>
|
||||
<Box
|
||||
position="relative"
|
||||
backgroundColor="plain"
|
||||
borderTopWidth={1}
|
||||
borderTopStyle="solid"
|
||||
borderColor="neutralPlain"
|
||||
__maxWidth={contentMaxWidth}
|
||||
margin="auto"
|
||||
zIndex="2"
|
||||
/>
|
||||
{/* FIXME: https://github.com/glideapps/glide-data-grid/issues/505 */}
|
||||
{hasColumnGroups && (
|
||||
<div className={classes.columnGroupFixer} />
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
import {
|
||||
CustomCell,
|
||||
CustomRenderer,
|
||||
getMiddleCenterBias,
|
||||
GridCellKind,
|
||||
ProvideEditorCallback,
|
||||
} from "@glideapps/glide-data-grid";
|
||||
import React from "react";
|
||||
|
||||
import { usePriceField } from "../../PriceField/usePriceField";
|
||||
import { usePriceField } from "../../../PriceField/usePriceField";
|
||||
import { drawCurrency, drawPrice } from "./utils";
|
||||
|
||||
interface MoneyCellProps {
|
||||
readonly kind: "money-cell";
|
||||
|
@ -55,28 +55,13 @@ export const moneyCellRenderer = (): CustomRenderer<MoneyCell> => ({
|
|||
const hasValue = value === 0 ? true : !!value;
|
||||
const formatted = value?.toString() ?? "-";
|
||||
|
||||
ctx.fillStyle = theme.textDark;
|
||||
ctx.textAlign = "right";
|
||||
ctx.fillText(
|
||||
formatted,
|
||||
rect.x + rect.width - 8,
|
||||
rect.y + rect.height / 2 + getMiddleCenterBias(ctx, theme),
|
||||
);
|
||||
drawPrice(ctx, theme, rect, formatted);
|
||||
|
||||
ctx.save();
|
||||
ctx.fillStyle = theme.textMedium;
|
||||
ctx.textAlign = "left";
|
||||
ctx.font = [
|
||||
theme.baseFontStyle.replace(/bold/g, "normal"),
|
||||
theme.fontFamily,
|
||||
].join(" ");
|
||||
ctx.fillText(
|
||||
hasValue ? currency : "-",
|
||||
rect.x + 8,
|
||||
rect.y + rect.height / 2 + getMiddleCenterBias(ctx, theme),
|
||||
);
|
||||
ctx.restore();
|
||||
|
||||
drawCurrency(ctx, theme, rect, hasValue ? currency : "-");
|
||||
|
||||
ctx.restore();
|
||||
return true;
|
||||
},
|
||||
provideEditor: () => ({
|
|
@ -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,
|
||||
numberCellEmptyValue,
|
||||
} from "@dashboard/components/Datagrid/customCells/NumberCell";
|
||||
import { Locale } from "@dashboard/components/Locale";
|
||||
import { GridCell, GridCellKind } from "@glideapps/glide-data-grid";
|
||||
|
||||
import {
|
||||
|
@ -9,7 +10,7 @@ import {
|
|||
DropdownCellContentProps,
|
||||
DropdownChoice,
|
||||
} from "./DropdownCell";
|
||||
import { MoneyCell } from "./MoneyCell";
|
||||
import { MoneyCell, MoneyDiscuntedCell } from "./Money";
|
||||
import { ThumbnailCell } from "./ThumbnailCell";
|
||||
|
||||
const common = {
|
||||
|
@ -110,6 +111,41 @@ export function moneyCell(
|
|||
};
|
||||
}
|
||||
|
||||
interface MoneyDiscountedCellData {
|
||||
value: number | string | null;
|
||||
discount?: string | number;
|
||||
undiscounted?: string | number;
|
||||
currency: string;
|
||||
locale: Locale;
|
||||
lineItemId?: string;
|
||||
}
|
||||
|
||||
export function moneyDiscountedCell(
|
||||
{
|
||||
value,
|
||||
undiscounted,
|
||||
currency,
|
||||
locale,
|
||||
lineItemId,
|
||||
}: MoneyDiscountedCellData,
|
||||
opts?: Partial<GridCell>,
|
||||
): MoneyDiscuntedCell {
|
||||
return {
|
||||
...common,
|
||||
...opts,
|
||||
kind: GridCellKind.Custom,
|
||||
data: {
|
||||
kind: "money-discounted-cell",
|
||||
value,
|
||||
currency,
|
||||
undiscounted,
|
||||
lineItemId,
|
||||
locale,
|
||||
},
|
||||
copyData: value?.toString() ?? "",
|
||||
};
|
||||
}
|
||||
|
||||
export function dropdownCell(
|
||||
value: DropdownChoice,
|
||||
dataOpts: DropdownCellContentProps &
|
||||
|
|
|
@ -3,7 +3,8 @@ import { useExtraCells } from "@glideapps/glide-data-grid-cells";
|
|||
import { useMemo } from "react";
|
||||
|
||||
import { dropdownCellRenderer } from "./DropdownCell";
|
||||
import { moneyCellRenderer } from "./MoneyCell";
|
||||
import { moneyCellRenderer } from "./Money/MoneyCell";
|
||||
import { moneyDiscountedCellRenderer } from "./Money/MoneyDiscountedCell";
|
||||
import { numberCellRenderer } from "./NumberCell";
|
||||
import { thumbnailCellRenderer } from "./ThumbnailCell";
|
||||
|
||||
|
@ -14,6 +15,7 @@ export function useCustomCellRenderers() {
|
|||
const renderers = useMemo(
|
||||
() => [
|
||||
moneyCellRenderer(),
|
||||
moneyDiscountedCellRenderer(),
|
||||
numberCellRenderer(locale),
|
||||
dropdownCellRenderer,
|
||||
thumbnailCellRenderer,
|
||||
|
|
|
@ -78,11 +78,11 @@ export function useColumnsDefault(
|
|||
|
||||
const columnChoices = useMemo(
|
||||
() =>
|
||||
applyFilters(columns).map(({ id, title }) => ({
|
||||
applyFilters(columns, availableColumns).map(({ id, title }) => ({
|
||||
label: title,
|
||||
value: id,
|
||||
})),
|
||||
[columns],
|
||||
[columns, availableColumns],
|
||||
);
|
||||
const availableColumnsChoices = useMemo(
|
||||
() =>
|
||||
|
@ -113,22 +113,27 @@ export function useColumnsDefault(
|
|||
};
|
||||
}
|
||||
|
||||
function applyFilters(columns: readonly AvailableColumn[]) {
|
||||
return columns.filter(byNoEmptyColumn).filter(byNotFirstColumn);
|
||||
function applyFilters(
|
||||
columns: readonly AvailableColumn[],
|
||||
availableColumns?: readonly AvailableColumn[],
|
||||
) {
|
||||
return columns
|
||||
.filter(byNoEmptyColumn)
|
||||
.filter(byNotFirstColumn(availableColumns));
|
||||
}
|
||||
|
||||
function byNoEmptyColumn(column: AvailableColumn) {
|
||||
return column.id !== "empty";
|
||||
}
|
||||
|
||||
function byNotFirstColumn(
|
||||
_: AvailableColumn,
|
||||
index: number,
|
||||
array: AvailableColumn[],
|
||||
) {
|
||||
if (array.some(col => col.id === "empty")) {
|
||||
return index > 1;
|
||||
function byNotFirstColumn(availableColumns?: readonly AvailableColumn[]) {
|
||||
return (column: AvailableColumn, index: number, array: AvailableColumn[]) => {
|
||||
// Check not first column base on available columns to prevent base on columns that order can change
|
||||
if (availableColumns) {
|
||||
const colIndex = availableColumns.findIndex(col => col.id === column.id);
|
||||
return availableColumns[0]?.id === "empty" ? colIndex > 1 : colIndex > 0;
|
||||
}
|
||||
|
||||
return index > 0;
|
||||
return array[0]?.id === "empty" ? index > 1 : index > 0;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -24,7 +24,10 @@ export interface DatagridChangeOpts {
|
|||
removed: number[];
|
||||
updates: DatagridChange[];
|
||||
}
|
||||
export type OnDatagridChange = (opts: DatagridChangeOpts) => void;
|
||||
export type OnDatagridChange = (
|
||||
opts: DatagridChangeOpts,
|
||||
setMarkCellsDirty: (areCellsDirty: boolean) => void,
|
||||
) => void;
|
||||
|
||||
export interface UseDatagridChangeState {
|
||||
added: number[];
|
||||
|
@ -64,6 +67,7 @@ function useDatagridChange(
|
|||
availableColumns: readonly AvailableColumn[],
|
||||
rows: number,
|
||||
onChange?: OnDatagridChange,
|
||||
setMarkCellsDirty?: (areCellsDirty: boolean) => void,
|
||||
) {
|
||||
const { added, setAdded, removed, setRemoved, changes } =
|
||||
useDatagridChangeStateContext();
|
||||
|
@ -78,14 +82,17 @@ function useDatagridChange(
|
|||
const notify = useCallback(
|
||||
(updates: DatagridChange[], added: number[], removed: number[]) => {
|
||||
if (onChange) {
|
||||
onChange({
|
||||
onChange(
|
||||
{
|
||||
updates,
|
||||
removed,
|
||||
added,
|
||||
});
|
||||
},
|
||||
setMarkCellsDirty,
|
||||
);
|
||||
}
|
||||
},
|
||||
[onChange],
|
||||
[onChange, setMarkCellsDirty],
|
||||
);
|
||||
|
||||
const onCellEdited = useCallback(
|
||||
|
|
|
@ -66,7 +66,7 @@ const useStyles = makeStyles(
|
|||
boxShadow: "none !important",
|
||||
padding: "0 !important",
|
||||
},
|
||||
"& input, & textarea": {
|
||||
"& input:not([class*='MuiInputBase']), & textarea": {
|
||||
appearance: "none",
|
||||
background: "none",
|
||||
border: "none",
|
||||
|
@ -77,7 +77,7 @@ const useStyles = makeStyles(
|
|||
padding: vars.space[3],
|
||||
outline: 0,
|
||||
},
|
||||
'& input[type="number"]': {
|
||||
"& input[type='number']:not([class*='MuiInputBase'])": {
|
||||
textAlign: "right",
|
||||
width: "100%",
|
||||
},
|
||||
|
|
|
@ -77,6 +77,7 @@ export const PriceField: React.FC<PriceFieldProps> = props => {
|
|||
fullWidth
|
||||
value={value}
|
||||
InputLabelProps={InputLabelProps}
|
||||
data-test-id="price-field"
|
||||
InputProps={{
|
||||
...InputProps,
|
||||
endAdornment: currencySymbol ? (
|
||||
|
|
|
@ -13,7 +13,7 @@ import { FormattedMessage } from "react-intl";
|
|||
import { useStyles } from "./styles";
|
||||
|
||||
export interface RadioGroupFieldChoice<
|
||||
T extends string | number = string | number
|
||||
T extends string | number = string | number,
|
||||
> {
|
||||
disabled?: boolean;
|
||||
value: T;
|
||||
|
@ -85,6 +85,7 @@ export const RadioGroupField: React.FC<RadioGroupFieldProps> = props => {
|
|||
}}
|
||||
control={
|
||||
<Radio
|
||||
data-test-id={choice.value}
|
||||
className={clsx({
|
||||
[classes.alignTop]: alignTop,
|
||||
})}
|
||||
|
|
|
@ -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";
|
||||
|
||||
const props: Omit<OrderDetailsPageProps, "classes"> = {
|
||||
disabled: false,
|
||||
loading: false,
|
||||
onBillingAddressEdit: undefined,
|
||||
onTransactionAction: () => undefined,
|
||||
onFulfillmentApprove: () => undefined,
|
||||
|
|
|
@ -50,7 +50,7 @@ export interface OrderDetailsPageProps {
|
|||
id: string;
|
||||
name: string;
|
||||
}>;
|
||||
disabled: boolean;
|
||||
loading: boolean;
|
||||
saveButtonBarState: ConfirmButtonTransitionState;
|
||||
errors: OrderErrorFragment[];
|
||||
onOrderLineAdd?: () => void;
|
||||
|
@ -85,7 +85,7 @@ export interface OrderDetailsPageProps {
|
|||
|
||||
const OrderDetailsPage: React.FC<OrderDetailsPageProps> = props => {
|
||||
const {
|
||||
disabled,
|
||||
loading,
|
||||
order,
|
||||
shop,
|
||||
saveButtonBarState,
|
||||
|
@ -162,11 +162,11 @@ const OrderDetailsPage: React.FC<OrderDetailsPageProps> = props => {
|
|||
|
||||
const allowSave = () => {
|
||||
if (!isOrderUnconfirmed) {
|
||||
return disabled;
|
||||
return loading;
|
||||
} else if (!order?.lines?.length) {
|
||||
return true;
|
||||
}
|
||||
return disabled;
|
||||
return loading;
|
||||
};
|
||||
|
||||
const selectCardMenuItems = filteredConditionalItems([
|
||||
|
@ -231,12 +231,14 @@ const OrderDetailsPage: React.FC<OrderDetailsPageProps> = props => {
|
|||
notAllowedToFulfillUnpaid={notAllowedToFulfillUnpaid}
|
||||
lines={unfulfilled}
|
||||
onFulfill={onOrderFulfill}
|
||||
loading={loading}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<OrderDraftDetails
|
||||
order={order}
|
||||
errors={errors}
|
||||
loading={loading}
|
||||
onOrderLineAdd={onOrderLineAdd}
|
||||
onOrderLineChange={onOrderLineChange}
|
||||
onOrderLineRemove={onOrderLineRemove}
|
||||
|
|
|
@ -16,7 +16,7 @@ import React from "react";
|
|||
import OrderDetailsPage, { OrderDetailsPageProps } from "./OrderDetailsPage";
|
||||
|
||||
const props: Omit<OrderDetailsPageProps, "classes"> = {
|
||||
disabled: false,
|
||||
loading: false,
|
||||
onBillingAddressEdit: undefined,
|
||||
onTransactionAction: () => undefined,
|
||||
onFulfillmentApprove: () => undefined,
|
||||
|
|
|
@ -13,6 +13,9 @@ const useStyles = makeStyles(
|
|||
alignItems: "center",
|
||||
padding: theme.spacing(3, 3, 0, 3),
|
||||
},
|
||||
closeIcon: {
|
||||
cursor: "pointer",
|
||||
},
|
||||
}),
|
||||
{ name: "ModalTitle" },
|
||||
);
|
||||
|
@ -34,7 +37,7 @@ const ModalTitle: React.FC<ModalTitleProps> = ({
|
|||
<>
|
||||
<div className={classes.container}>
|
||||
<Typography variant="h5">{title}</Typography>
|
||||
<CloseIcon onClick={onClose} />
|
||||
<CloseIcon className={classes.closeIcon} onClick={onClose} />
|
||||
</div>
|
||||
{withBorder && (
|
||||
<>
|
||||
|
|
|
@ -6,22 +6,10 @@ import RadioGroupField from "@dashboard/components/RadioGroupField";
|
|||
import { DiscountValueTypeEnum, MoneyFragment } from "@dashboard/graphql";
|
||||
import { useUpdateEffect } from "@dashboard/hooks/useUpdateEffect";
|
||||
import { buttonMessages } from "@dashboard/intl";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
Popper,
|
||||
TextField,
|
||||
Typography,
|
||||
} from "@material-ui/core";
|
||||
import { PopperPlacementType } from "@material-ui/core/Popper";
|
||||
import { toFixed } from "@dashboard/utils/toFixed";
|
||||
import { Card, CardContent, TextField, Typography } from "@material-ui/core";
|
||||
import { ConfirmButtonTransitionState, makeStyles } from "@saleor/macaw-ui";
|
||||
import React, {
|
||||
ChangeEvent,
|
||||
MutableRefObject,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import React, { ChangeEvent, useEffect, useRef, useState } from "react";
|
||||
import { defineMessages, useIntl } from "react-intl";
|
||||
|
||||
import ModalTitle from "./ModalTitle";
|
||||
|
@ -31,16 +19,10 @@ import {
|
|||
OrderDiscountType,
|
||||
} from "./types";
|
||||
|
||||
const fullNumbersRegex = /^[0-9]*$/;
|
||||
const numbersRegex = /([0-9]+\.?[0-9]*)$/;
|
||||
const PERMIL = 0.01;
|
||||
|
||||
const useStyles = makeStyles(
|
||||
theme => ({
|
||||
container: {
|
||||
zIndex: 1000,
|
||||
marginTop: theme.spacing(1),
|
||||
},
|
||||
removeButton: {
|
||||
"&:hover": {
|
||||
backgroundColor: theme.palette.error.main,
|
||||
|
@ -98,6 +80,16 @@ const messages = defineMessages({
|
|||
defaultMessage: "Invalid value",
|
||||
description: "value input helper text",
|
||||
},
|
||||
valueBiggerThatPrice: {
|
||||
defaultMessage: "Cannot be higher than the price",
|
||||
id: "VIdXPy",
|
||||
description: "value input helper text",
|
||||
},
|
||||
valueBiggerThat100: {
|
||||
defaultMessage: "Cannot be higher than 100%",
|
||||
id: "zHx85l",
|
||||
description: "value input helper text",
|
||||
},
|
||||
discountValueLabel: {
|
||||
id: "GAmGog",
|
||||
defaultMessage: "Discount value",
|
||||
|
@ -116,10 +108,7 @@ export interface OrderDiscountCommonModalProps {
|
|||
onClose: () => void;
|
||||
onRemove: () => void;
|
||||
modalType: OrderDiscountType;
|
||||
anchorRef: MutableRefObject<any>;
|
||||
existingDiscount: OrderDiscountCommonInput;
|
||||
dialogPlacement: PopperPlacementType;
|
||||
isOpen: boolean;
|
||||
confirmStatus: ConfirmButtonTransitionState;
|
||||
removeStatus: ConfirmButtonTransitionState;
|
||||
}
|
||||
|
@ -128,12 +117,9 @@ const OrderDiscountCommonModal: React.FC<OrderDiscountCommonModalProps> = ({
|
|||
maxPrice = { amount: null, currency: "" },
|
||||
onConfirm,
|
||||
modalType,
|
||||
anchorRef,
|
||||
onClose,
|
||||
onRemove,
|
||||
existingDiscount,
|
||||
dialogPlacement,
|
||||
isOpen,
|
||||
confirmStatus,
|
||||
removeStatus,
|
||||
}) => {
|
||||
|
@ -147,7 +133,7 @@ const OrderDiscountCommonModal: React.FC<OrderDiscountCommonModalProps> = ({
|
|||
const stringifiedValue = existingDiscount.value.toString();
|
||||
|
||||
if (calculationMode === DiscountValueTypeEnum.FIXED) {
|
||||
return parseFloat(stringifiedValue).toFixed(2);
|
||||
return parseFloat(stringifiedValue).toString();
|
||||
}
|
||||
|
||||
return stringifiedValue;
|
||||
|
@ -166,7 +152,7 @@ const OrderDiscountCommonModal: React.FC<OrderDiscountCommonModalProps> = ({
|
|||
|
||||
const initialData = getInitialData();
|
||||
|
||||
const [isValueError, setValueError] = useState<boolean>(false);
|
||||
const [valueErrorMsg, setValueErrorMsg] = useState<string | null>(null);
|
||||
const [reason, setReason] = useState<string>(initialData.reason);
|
||||
const [value, setValue] = useState<string>(initialData.value);
|
||||
const [calculationMode, setCalculationMode] = useState<DiscountValueTypeEnum>(
|
||||
|
@ -196,24 +182,36 @@ const OrderDiscountCommonModal: React.FC<OrderDiscountCommonModalProps> = ({
|
|||
) => {
|
||||
const value = event.target.value;
|
||||
|
||||
handleSetError(value);
|
||||
setValueErrorMsg(getErrorMessage(value));
|
||||
setValue(value);
|
||||
};
|
||||
|
||||
const getParsedDiscountValue = () => parseFloat(value) || 0;
|
||||
|
||||
const isAmountTooLarge = () => {
|
||||
const isAmountTooLarge = (value?: string) => {
|
||||
const topAmount = isDiscountTypePercentage ? 100 : maxAmount;
|
||||
|
||||
if (value) {
|
||||
return (parseFloat(value) || 0) > topAmount;
|
||||
}
|
||||
|
||||
return getParsedDiscountValue() > topAmount;
|
||||
};
|
||||
|
||||
const handleSetError = (value: string) => {
|
||||
const regexToCheck = isDiscountTypePercentage
|
||||
? fullNumbersRegex
|
||||
: numbersRegex;
|
||||
const getErrorMessage = (value: string): string | null => {
|
||||
if (isAmountTooLarge(value)) {
|
||||
if (calculationMode === DiscountValueTypeEnum.PERCENTAGE) {
|
||||
return intl.formatMessage(messages.valueBiggerThat100);
|
||||
}
|
||||
|
||||
setValueError(!regexToCheck.test(value));
|
||||
return intl.formatMessage(messages.valueBiggerThatPrice);
|
||||
}
|
||||
|
||||
if (!numbersRegex.test(value)) {
|
||||
return intl.formatMessage(messages.invalidValue);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const handleConfirm = () => {
|
||||
|
@ -228,7 +226,7 @@ const OrderDiscountCommonModal: React.FC<OrderDiscountCommonModalProps> = ({
|
|||
setReason(initialData.reason);
|
||||
setValue(initialData.value);
|
||||
setCalculationMode(initialData.calculationMode);
|
||||
setValueError(false);
|
||||
setValueErrorMsg(null);
|
||||
};
|
||||
|
||||
useEffect(setDefaultValues, [
|
||||
|
@ -246,21 +244,22 @@ const OrderDiscountCommonModal: React.FC<OrderDiscountCommonModalProps> = ({
|
|||
calculationMode === DiscountValueTypeEnum.FIXED;
|
||||
|
||||
const recalculatedValueFromPercentageToFixed = (
|
||||
getParsedDiscountValue() *
|
||||
PERMIL *
|
||||
maxPrice.amount
|
||||
).toFixed(2);
|
||||
(getParsedDiscountValue() * maxPrice.amount) /
|
||||
100
|
||||
).toString();
|
||||
|
||||
const recalculatedValueFromFixedToPercentage = Math.round(
|
||||
(getParsedDiscountValue() * (1 / PERMIL)) / maxPrice.amount,
|
||||
const recalculatedValueFromFixedToPercentage = (
|
||||
(getParsedDiscountValue() / maxPrice.amount) *
|
||||
100
|
||||
).toString();
|
||||
|
||||
const recalculatedValue = changedFromPercentageToFixed
|
||||
? recalculatedValueFromPercentageToFixed
|
||||
: recalculatedValueFromFixedToPercentage;
|
||||
|
||||
handleSetError(recalculatedValue);
|
||||
setValueErrorMsg(getErrorMessage(recalculatedValue));
|
||||
setValue(recalculatedValue);
|
||||
previousCalculationMode.current = calculationMode;
|
||||
};
|
||||
|
||||
useUpdateEffect(handleValueConversion, [calculationMode]);
|
||||
|
@ -274,15 +273,9 @@ const OrderDiscountCommonModal: React.FC<OrderDiscountCommonModalProps> = ({
|
|||
calculationMode === DiscountValueTypeEnum.FIXED ? currency : "%";
|
||||
|
||||
const isSubmitDisabled =
|
||||
!getParsedDiscountValue() || isValueError || isAmountTooLarge();
|
||||
!getParsedDiscountValue() || !!valueErrorMsg || isAmountTooLarge();
|
||||
|
||||
return (
|
||||
<Popper
|
||||
open={isOpen}
|
||||
anchorEl={anchorRef.current}
|
||||
className={classes.container}
|
||||
placement={dialogPlacement}
|
||||
>
|
||||
<Card>
|
||||
<ModalTitle title={intl.formatMessage(dialogTitle)} onClose={onClose} />
|
||||
<CardContent>
|
||||
|
@ -297,9 +290,9 @@ const OrderDiscountCommonModal: React.FC<OrderDiscountCommonModalProps> = ({
|
|||
<CardSpacer />
|
||||
<PriceField
|
||||
label={intl.formatMessage(messages.discountValueLabel)}
|
||||
error={isValueError}
|
||||
hint={isValueError && intl.formatMessage(messages.invalidValue)}
|
||||
value={value}
|
||||
error={!!valueErrorMsg}
|
||||
hint={valueErrorMsg}
|
||||
value={toFixed(value, 2)}
|
||||
onChange={handleSetDiscountValue}
|
||||
currencySymbol={valueFieldSymbol}
|
||||
/>
|
||||
|
@ -311,6 +304,7 @@ const OrderDiscountCommonModal: React.FC<OrderDiscountCommonModalProps> = ({
|
|||
className={classes.reasonInput}
|
||||
label={intl.formatMessage(messages.discountReasonLabel)}
|
||||
value={reason}
|
||||
data-test-id="discount-reason"
|
||||
onChange={(event: ChangeEvent<HTMLInputElement>) =>
|
||||
setReason(event.target.value)
|
||||
}
|
||||
|
@ -337,7 +331,6 @@ const OrderDiscountCommonModal: React.FC<OrderDiscountCommonModalProps> = ({
|
|||
)}
|
||||
</DialogButtons>
|
||||
</Card>
|
||||
</Popper>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -22,6 +22,7 @@ interface OrderDraftDetailsProps {
|
|||
order: OrderDetailsFragment;
|
||||
channelUsabilityData?: ChannelUsabilityDataQuery;
|
||||
errors: OrderErrorFragment[];
|
||||
loading: boolean;
|
||||
onOrderLineAdd: () => void;
|
||||
onOrderLineChange: (id: string, data: OrderLineInput) => void;
|
||||
onOrderLineRemove: (id: string) => void;
|
||||
|
@ -32,6 +33,7 @@ const OrderDraftDetails: React.FC<OrderDraftDetailsProps> = ({
|
|||
order,
|
||||
channelUsabilityData,
|
||||
errors,
|
||||
loading,
|
||||
onOrderLineAdd,
|
||||
onOrderLineChange,
|
||||
onOrderLineRemove,
|
||||
|
@ -70,6 +72,7 @@ const OrderDraftDetails: React.FC<OrderDraftDetailsProps> = ({
|
|||
<OrderDraftDetailsProducts
|
||||
order={order}
|
||||
errors={errors}
|
||||
loading={loading}
|
||||
onOrderLineChange={onOrderLineChange}
|
||||
onOrderLineRemove={onOrderLineRemove}
|
||||
/>
|
||||
|
|
|
@ -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 TableRowLink from "@dashboard/components/TableRowLink";
|
||||
import { OrderDetailsFragment, OrderErrorFragment } from "@dashboard/graphql";
|
||||
import {
|
||||
OrderLineDiscountConsumer,
|
||||
OrderLineDiscountContextConsumerProps,
|
||||
} from "@dashboard/products/components/OrderDiscountProviders/OrderLineDiscountProvider";
|
||||
import getOrderErrorMessage from "@dashboard/utils/errors/order";
|
||||
import { TableBody, TableCell, TableHead, Typography } from "@material-ui/core";
|
||||
import { makeStyles } from "@saleor/macaw-ui";
|
||||
import React from "react";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
|
||||
import { renderCollection } from "../../../misc";
|
||||
import TableLine from "./TableLine";
|
||||
|
||||
import { OrderDraftDetailsDatagrid } from "../OrderDraftDetailsDatagrid/OrderDraftDetailsDatagrid";
|
||||
export interface FormData {
|
||||
quantity: number;
|
||||
}
|
||||
|
||||
const useStyles = makeStyles(
|
||||
theme => ({
|
||||
colAction: {
|
||||
width: theme.spacing(10),
|
||||
},
|
||||
colName: {
|
||||
width: "auto",
|
||||
},
|
||||
colNameLabel: {},
|
||||
colPrice: {},
|
||||
colQuantity: {},
|
||||
colTotal: {},
|
||||
skeleton: {
|
||||
margin: theme.spacing(0, 4),
|
||||
},
|
||||
errorInfo: {
|
||||
color: theme.palette.error.main,
|
||||
marginLeft: theme.spacing(1.5),
|
||||
display: "inline",
|
||||
},
|
||||
quantityField: {
|
||||
"& input": {
|
||||
padding: "12px 12px 10px",
|
||||
},
|
||||
width: 60,
|
||||
},
|
||||
table: {
|
||||
[theme.breakpoints.up("md")]: {
|
||||
tableLayout: "auto",
|
||||
},
|
||||
tableLayout: "auto",
|
||||
},
|
||||
}),
|
||||
{ name: "OrderDraftDetailsProducts" },
|
||||
);
|
||||
|
@ -58,19 +20,21 @@ const useStyles = makeStyles(
|
|||
interface OrderDraftDetailsProductsProps {
|
||||
order?: OrderDetailsFragment;
|
||||
errors: OrderErrorFragment[];
|
||||
loading: boolean;
|
||||
onOrderLineChange: (id: string, data: FormData) => void;
|
||||
onOrderLineRemove: (id: string) => void;
|
||||
}
|
||||
|
||||
const OrderDraftDetailsProducts: React.FC<
|
||||
OrderDraftDetailsProductsProps
|
||||
> = props => {
|
||||
const { order, errors, onOrderLineChange, onOrderLineRemove } = props;
|
||||
const OrderDraftDetailsProducts: React.FC<OrderDraftDetailsProductsProps> = ({
|
||||
order,
|
||||
errors,
|
||||
loading,
|
||||
onOrderLineChange,
|
||||
onOrderLineRemove,
|
||||
}) => {
|
||||
const classes = useStyles();
|
||||
|
||||
const lines = order?.lines ?? [];
|
||||
|
||||
const intl = useIntl();
|
||||
const classes = useStyles(props);
|
||||
|
||||
const formErrors = errors.filter(error => error.field === "lines");
|
||||
|
||||
if (order === undefined) {
|
||||
|
@ -78,79 +42,15 @@ const OrderDraftDetailsProducts: React.FC<
|
|||
}
|
||||
|
||||
return (
|
||||
<ResponsiveTable className={classes.table}>
|
||||
{!!lines.length && (
|
||||
<TableHead>
|
||||
<TableRowLink>
|
||||
<TableCell className={classes.colName} colSpan={2}>
|
||||
<span className={classes.colNameLabel}>
|
||||
<FormattedMessage id="x/ZVlU" defaultMessage="Product" />
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell className={classes.colQuantity}>
|
||||
<FormattedMessage
|
||||
id="nEWp+k"
|
||||
defaultMessage="Quantity"
|
||||
description="quantity of ordered products"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className={classes.colPrice}>
|
||||
<FormattedMessage
|
||||
id="32dfzI"
|
||||
defaultMessage="Price"
|
||||
description="price or ordered products"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className={classes.colTotal}>
|
||||
<FormattedMessage
|
||||
id="lVwmf5"
|
||||
defaultMessage="Total"
|
||||
description="total price of ordered products"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className={classes.colAction} />
|
||||
</TableRowLink>
|
||||
</TableHead>
|
||||
)}
|
||||
<TableBody>
|
||||
{!!lines.length ? (
|
||||
renderCollection(lines, line => (
|
||||
<OrderLineDiscountConsumer key={line.id} orderLineId={line.id}>
|
||||
{(
|
||||
orderLineDiscountProps: OrderLineDiscountContextConsumerProps,
|
||||
) => (
|
||||
<TableLine
|
||||
{...orderLineDiscountProps}
|
||||
line={line}
|
||||
error={formErrors.find(error =>
|
||||
error.orderLines?.some(id => id === line.id),
|
||||
)}
|
||||
onOrderLineChange={onOrderLineChange}
|
||||
<OrderDraftDetailsDatagrid
|
||||
lines={lines}
|
||||
loading={loading}
|
||||
onOrderLineRemove={onOrderLineRemove}
|
||||
onOrderLineChange={onOrderLineChange}
|
||||
errors={formErrors}
|
||||
/>
|
||||
)}
|
||||
</OrderLineDiscountConsumer>
|
||||
))
|
||||
) : (
|
||||
<>
|
||||
<TableRowLink>
|
||||
<TableCell colSpan={5}>
|
||||
<FormattedMessage
|
||||
id="UD7/q8"
|
||||
defaultMessage="No Products added to Order"
|
||||
/>
|
||||
{!!formErrors.length && (
|
||||
<Typography variant="body2" className={classes.errorInfo}>
|
||||
{getOrderErrorMessage(formErrors[0], intl)}
|
||||
</Typography>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRowLink>
|
||||
</>
|
||||
)}
|
||||
</TableBody>
|
||||
</ResponsiveTable>
|
||||
);
|
||||
};
|
||||
|
||||
OrderDraftDetailsProducts.displayName = "OrderDraftDetailsProducts";
|
||||
export default OrderDraftDetailsProducts;
|
||||
|
|
|
@ -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({
|
||||
notPublished: {
|
||||
id: "Oad+ES",
|
||||
defaultMessage: "This product is not published in this channel.",
|
||||
id: "QL4a0Z",
|
||||
defaultMessage: "Not published in this channel",
|
||||
description: "alert message",
|
||||
},
|
||||
notAvailable: {
|
||||
id: "zO+l0L",
|
||||
defaultMessage: "This product is not available for sale in this channel.",
|
||||
id: "dDCLFW",
|
||||
defaultMessage: "Not available for sale this channel",
|
||||
description: "alert message",
|
||||
},
|
||||
notExists: {
|
||||
id: "pEMxyy",
|
||||
defaultMessage: "This product does no longer exist.",
|
||||
id: "ZJOX8n",
|
||||
defaultMessage: "Product no longer exists",
|
||||
description: "alert message",
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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 { Typography } from "@material-ui/core";
|
||||
import { makeStyles } from "@saleor/macaw-ui";
|
||||
import React, { useRef } from "react";
|
||||
import { Box, Button, Popover, sprinkles } from "@saleor/macaw-ui/next";
|
||||
import React from "react";
|
||||
import { useIntl } from "react-intl";
|
||||
|
||||
import OrderDiscountCommonModal from "../OrderDiscountCommonModal";
|
||||
|
@ -87,8 +88,6 @@ const OrderDraftDetailsSummary: React.FC<
|
|||
const intl = useIntl();
|
||||
const classes = useStyles(props);
|
||||
|
||||
const popperAnchorRef = useRef<HTMLTableRowElement | null>(null);
|
||||
|
||||
if (!order) {
|
||||
return null;
|
||||
}
|
||||
|
@ -179,36 +178,47 @@ const OrderDraftDetailsSummary: React.FC<
|
|||
return (
|
||||
<table className={classes.root}>
|
||||
<tbody>
|
||||
<tr className={classes.relativeRow} ref={popperAnchorRef}>
|
||||
<tr className={classes.relativeRow}>
|
||||
<td>
|
||||
<Link onClick={openDialog}>
|
||||
{intl.formatMessage(discountTitle)}
|
||||
</Link>
|
||||
<Popover open={isDialogOpen}>
|
||||
<Popover.Trigger>
|
||||
<Button
|
||||
variant="tertiary"
|
||||
onClick={isDialogOpen ? closeDialog : openDialog}
|
||||
>
|
||||
<Link>{intl.formatMessage(discountTitle)}</Link>
|
||||
</Button>
|
||||
</Popover.Trigger>
|
||||
<Popover.Content
|
||||
align="start"
|
||||
className={sprinkles({ zIndex: "3" })}
|
||||
>
|
||||
<Box boxShadow="overlay">
|
||||
<OrderDiscountCommonModal
|
||||
dialogPlacement="bottom-start"
|
||||
modalType={ORDER_DISCOUNT}
|
||||
anchorRef={popperAnchorRef}
|
||||
existingDiscount={orderDiscount}
|
||||
maxPrice={undiscountedPrice}
|
||||
isOpen={isDialogOpen}
|
||||
onConfirm={addOrderDiscount}
|
||||
onClose={closeDialog}
|
||||
onRemove={removeOrderDiscount}
|
||||
confirmStatus={orderDiscountAddStatus}
|
||||
removeStatus={orderDiscountRemoveStatus}
|
||||
/>
|
||||
</Box>
|
||||
</Popover.Content>
|
||||
</Popover>
|
||||
</td>
|
||||
<td className={classes.textRight}>
|
||||
{getOrderDiscountLabel(orderDiscount)}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<tr data-test-id="order-subtotal-price">
|
||||
<td>{intl.formatMessage(messages.subtotal)}</td>
|
||||
<td className={classes.textRight}>
|
||||
<Money money={subtotal.gross} />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<tr data-test-id="order-add-shipping-line">
|
||||
<td>
|
||||
{hasShippingMethods && getShippingMethodComponent()}
|
||||
|
||||
|
@ -230,13 +240,13 @@ const OrderDraftDetailsSummary: React.FC<
|
|||
)}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<tr data-test-id="order-taxes-price">
|
||||
<td>{intl.formatMessage(messages.taxes)}</td>
|
||||
<td className={classes.textRight}>
|
||||
<Money money={order.total.tax} />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<tr data-test-id="order-total-price">
|
||||
<td>{intl.formatMessage(messages.total)}</td>
|
||||
<td className={classes.textRight}>
|
||||
<Money money={total.gross} />
|
||||
|
|
|
@ -43,6 +43,7 @@ const order = draftOrder(placeholderImage);
|
|||
|
||||
const props: Omit<OrderDraftPageProps, "classes"> = {
|
||||
...fetchMoreProps,
|
||||
loading: false,
|
||||
disabled: false,
|
||||
fetchUsers: () => undefined,
|
||||
onBillingAddressEdit: undefined,
|
||||
|
|
|
@ -53,7 +53,7 @@ export interface OrderDraftPageProps extends FetchMoreProps {
|
|||
|
||||
const OrderDraftPage: React.FC<OrderDraftPageProps> = props => {
|
||||
const {
|
||||
disabled,
|
||||
loading,
|
||||
fetchUsers,
|
||||
hasMore,
|
||||
saveButtonBarState,
|
||||
|
@ -120,6 +120,7 @@ const OrderDraftPage: React.FC<OrderDraftPageProps> = props => {
|
|||
order={order as OrderDetailsFragment}
|
||||
channelUsabilityData={channelUsabilityData}
|
||||
errors={errors}
|
||||
loading={loading}
|
||||
onOrderLineAdd={onOrderLineAdd}
|
||||
onOrderLineChange={onOrderLineChange}
|
||||
onOrderLineRemove={onOrderLineRemove}
|
||||
|
@ -152,7 +153,7 @@ const OrderDraftPage: React.FC<OrderDraftPageProps> = props => {
|
|||
</DetailPageLayout.RightSidebar>
|
||||
<Savebar
|
||||
state={saveButtonBarState}
|
||||
disabled={disabled}
|
||||
disabled={loading}
|
||||
onCancel={() => navigate(orderDraftListUrl())}
|
||||
onSubmit={onDraftFinalize}
|
||||
labels={{
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import TableRowLink from "@dashboard/components/TableRowLink";
|
||||
import { FulfillmentStatus, OrderDetailsFragment } from "@dashboard/graphql";
|
||||
import { getStringOrPlaceholder } from "@dashboard/misc";
|
||||
import { TableCell, Typography } from "@material-ui/core";
|
||||
import { Typography } from "@material-ui/core";
|
||||
import { Box } from "@saleor/macaw-ui/next";
|
||||
import clsx from "clsx";
|
||||
import React from "react";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
|
@ -9,8 +9,6 @@ import { FormattedMessage, useIntl } from "react-intl";
|
|||
import { extraInfoMessages } from "./messages";
|
||||
import useStyles from "./styles";
|
||||
|
||||
const NUMBER_OF_COLUMNS = 5;
|
||||
|
||||
interface ExtraInfoLinesProps {
|
||||
fulfillment?: OrderDetailsFragment["fulfillments"][0];
|
||||
}
|
||||
|
@ -26,8 +24,12 @@ const ExtraInfoLines: React.FC<ExtraInfoLinesProps> = ({ fulfillment }) => {
|
|||
const { warehouse, trackingNumber, status } = fulfillment;
|
||||
|
||||
return (
|
||||
<TableRowLink>
|
||||
<TableCell className={classes.infoRow} colSpan={NUMBER_OF_COLUMNS}>
|
||||
<Box
|
||||
paddingY={7}
|
||||
borderColor="neutralHighlight"
|
||||
borderBottomStyle={"solid"}
|
||||
borderBottomWidth={1}
|
||||
>
|
||||
<Typography color="textSecondary" variant="body2">
|
||||
{warehouse && (
|
||||
<>
|
||||
|
@ -66,8 +68,7 @@ const ExtraInfoLines: React.FC<ExtraInfoLinesProps> = ({ fulfillment }) => {
|
|||
/>
|
||||
)}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
</TableRowLink>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -1,17 +1,14 @@
|
|||
import CardSpacer from "@dashboard/components/CardSpacer";
|
||||
import ResponsiveTable from "@dashboard/components/ResponsiveTable";
|
||||
import { FulfillmentStatus, OrderDetailsFragment } from "@dashboard/graphql";
|
||||
import TrashIcon from "@dashboard/icons/Trash";
|
||||
import { orderHasTransactions } from "@dashboard/orders/types";
|
||||
import { mergeRepeatedOrderLines } from "@dashboard/orders/utils/data";
|
||||
import { Card, CardContent, TableBody } from "@material-ui/core";
|
||||
import { Card, CardContent } from "@material-ui/core";
|
||||
import { IconButton } from "@saleor/macaw-ui";
|
||||
import React from "react";
|
||||
|
||||
import { renderCollection } from "../../../misc";
|
||||
import OrderCardTitle from "../OrderCardTitle";
|
||||
import TableHeader from "../OrderProductsCardElements/OrderProductsCardHeader";
|
||||
import TableLine from "../OrderProductsCardElements/OrderProductsTableRow";
|
||||
import { OrderDetailsDatagrid } from "../OrderDetailsDatagrid";
|
||||
import ActionButtons from "./ActionButtons";
|
||||
import ExtraInfoLines from "./ExtraInfoLines";
|
||||
import useStyles from "./styles";
|
||||
|
@ -55,10 +52,12 @@ const OrderFulfilledProductsCard: React.FC<
|
|||
|
||||
const getLines = () => {
|
||||
if (statusesToMergeLines.includes(fulfillment?.status)) {
|
||||
return mergeRepeatedOrderLines(fulfillment.lines);
|
||||
return mergeRepeatedOrderLines(fulfillment.lines).map(
|
||||
order => order.orderLine,
|
||||
);
|
||||
}
|
||||
|
||||
return fulfillment?.lines || [];
|
||||
return fulfillment?.lines.map(order => order.orderLine) || [];
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -85,15 +84,8 @@ const OrderFulfilledProductsCard: React.FC<
|
|||
}
|
||||
/>
|
||||
<CardContent>
|
||||
<ResponsiveTable className={classes.table}>
|
||||
<TableHeader />
|
||||
<TableBody>
|
||||
{renderCollection(getLines(), line => (
|
||||
<TableLine key={line.id} line={line} />
|
||||
))}
|
||||
</TableBody>
|
||||
<OrderDetailsDatagrid lines={getLines()} loading={false} />
|
||||
<ExtraInfoLines fulfillment={fulfillment} />
|
||||
</ResponsiveTable>
|
||||
<ActionButtons
|
||||
orderId={order?.id}
|
||||
status={fulfillment?.status}
|
||||
|
|
|
@ -27,9 +27,6 @@ const useStyles = makeStyles(
|
|||
infoLabelWithMargin: {
|
||||
marginBottom: theme.spacing(),
|
||||
},
|
||||
infoRow: {
|
||||
padding: theme.spacing(2, 3),
|
||||
},
|
||||
}),
|
||||
{ name: "OrderFulfilledProductsCard" },
|
||||
);
|
||||
|
|
|
@ -8,7 +8,6 @@ import { GetCellContentOpts } from "@dashboard/components/Datagrid/Datagrid";
|
|||
import { useEmptyColumn } from "@dashboard/components/Datagrid/hooks/useEmptyColumn";
|
||||
import { AvailableColumn } from "@dashboard/components/Datagrid/types";
|
||||
import { Locale } from "@dashboard/components/Locale";
|
||||
import { formatMoneyAmount } from "@dashboard/components/Money";
|
||||
import { OrderListQuery } from "@dashboard/graphql";
|
||||
import useLocale from "@dashboard/hooks/useLocale";
|
||||
import {
|
||||
|
@ -128,7 +127,7 @@ export const useGetCellContent = ({ columns, orders }: GetCellContentProps) => {
|
|||
case "status":
|
||||
return getStatusCellContent(intl, themeValues, currentTheme, rowData);
|
||||
case "total":
|
||||
return getTotalCellContent(locale, rowData);
|
||||
return getTotalCellContent(rowData);
|
||||
default:
|
||||
return textCell("");
|
||||
}
|
||||
|
@ -213,15 +212,12 @@ export function getStatusCellContent(
|
|||
}
|
||||
|
||||
export function getTotalCellContent(
|
||||
locale: Locale,
|
||||
rowData: RelayToFlat<OrderListQuery["orders"]>[number],
|
||||
) {
|
||||
if (rowData?.total?.gross) {
|
||||
return moneyCell(
|
||||
formatMoneyAmount(rowData.total.gross, locale),
|
||||
rowData.total.gross.currency,
|
||||
{ cursor: "pointer" },
|
||||
);
|
||||
return moneyCell(rowData.total.gross.amount, rowData.total.gross.currency, {
|
||||
cursor: "pointer",
|
||||
});
|
||||
}
|
||||
|
||||
return readonlyTextCell("-");
|
||||
|
|
|
@ -169,12 +169,12 @@ const OrderProductAddDialog: React.FC<OrderProductAddDialogProps> = props => {
|
|||
scrollableTarget={scrollableTargetId}
|
||||
>
|
||||
<ResponsiveTable key="table">
|
||||
<TableBody>
|
||||
<TableBody data-test-id="add-products-table">
|
||||
{renderCollection(
|
||||
productChoicesWithValidVariants,
|
||||
(product, productIndex) => (
|
||||
<React.Fragment key={product ? product.id : "skeleton"}>
|
||||
<TableRowLink>
|
||||
<TableRowLink data-test-id="product">
|
||||
<TableCell
|
||||
padding="checkbox"
|
||||
className={classes.productCheckboxCell}
|
||||
|
@ -199,14 +199,18 @@ const OrderProductAddDialog: React.FC<OrderProductAddDialogProps> = props => {
|
|||
className={classes.avatar}
|
||||
thumbnail={maybe(() => product.thumbnail.url)}
|
||||
/>
|
||||
<TableCell className={classes.colName} colSpan={2}>
|
||||
<TableCell
|
||||
className={classes.colName}
|
||||
colSpan={2}
|
||||
data-test-id="product-name"
|
||||
>
|
||||
{maybe(() => product.name)}
|
||||
</TableCell>
|
||||
</TableRowLink>
|
||||
{maybe(() => product.variants, [])
|
||||
.filter(isValidVariant)
|
||||
.map((variant, variantIndex) => (
|
||||
<TableRowLink key={variant.id}>
|
||||
<TableRowLink key={variant.id} data-test-id="variant">
|
||||
<TableCell />
|
||||
<TableCell className={classes.colVariantCheckbox}>
|
||||
<Checkbox
|
||||
|
@ -242,7 +246,10 @@ const OrderProductAddDialog: React.FC<OrderProductAddDialogProps> = props => {
|
|||
</div>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className={classes.textRight}>
|
||||
<TableCell
|
||||
className={classes.textRight}
|
||||
data-test-id="variant-price"
|
||||
>
|
||||
<OrderPriceLabel pricing={variant.pricing} />
|
||||
</TableCell>
|
||||
</TableRowLink>
|
||||
|
|
|
@ -1,22 +1,13 @@
|
|||
import { Button } from "@dashboard/components/Button";
|
||||
import CardSpacer from "@dashboard/components/CardSpacer";
|
||||
import ResponsiveTable from "@dashboard/components/ResponsiveTable";
|
||||
import { OrderLineFragment } from "@dashboard/graphql";
|
||||
import { commonMessages } from "@dashboard/intl";
|
||||
import { renderCollection } from "@dashboard/misc";
|
||||
import {
|
||||
Card,
|
||||
CardActions,
|
||||
CardContent,
|
||||
TableBody,
|
||||
Typography,
|
||||
} from "@material-ui/core";
|
||||
import { Card, CardActions, CardContent, Typography } from "@material-ui/core";
|
||||
import React from "react";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
|
||||
import OrderCardTitle from "../OrderCardTitle";
|
||||
import TableHeader from "../OrderProductsCardElements/OrderProductsCardHeader";
|
||||
import TableLine from "../OrderProductsCardElements/OrderProductsTableRow";
|
||||
import { OrderDetailsDatagrid } from "../OrderDetailsDatagrid";
|
||||
import { useStyles } from "./styles";
|
||||
|
||||
interface OrderUnfulfilledProductsCardProps {
|
||||
|
@ -24,13 +15,18 @@ interface OrderUnfulfilledProductsCardProps {
|
|||
notAllowedToFulfillUnpaid: boolean;
|
||||
lines: OrderLineFragment[];
|
||||
onFulfill: () => void;
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
const OrderUnfulfilledProductsCard: React.FC<
|
||||
OrderUnfulfilledProductsCardProps
|
||||
> = props => {
|
||||
const { showFulfillmentAction, notAllowedToFulfillUnpaid, lines, onFulfill } =
|
||||
props;
|
||||
> = ({
|
||||
showFulfillmentAction,
|
||||
notAllowedToFulfillUnpaid,
|
||||
lines,
|
||||
onFulfill,
|
||||
loading,
|
||||
}) => {
|
||||
const classes = useStyles();
|
||||
|
||||
if (!lines.length) {
|
||||
|
@ -47,14 +43,7 @@ const OrderUnfulfilledProductsCard: React.FC<
|
|||
className={classes.cardTitle}
|
||||
/>
|
||||
<CardContent>
|
||||
<ResponsiveTable className={classes.table}>
|
||||
<TableHeader />
|
||||
<TableBody>
|
||||
{renderCollection(lines, line => (
|
||||
<TableLine key={line.id} isOrderLine line={line} />
|
||||
))}
|
||||
</TableBody>
|
||||
</ResponsiveTable>
|
||||
<OrderDetailsDatagrid lines={lines} loading={loading} />
|
||||
{showFulfillmentAction && (
|
||||
<CardActions className={classes.actions}>
|
||||
<Button
|
||||
|
|
|
@ -210,6 +210,7 @@ export const OrderDraftDetails: React.FC<OrderDraftDetailsProps> = ({
|
|||
<OrderDiscountProvider order={order}>
|
||||
<OrderLineDiscountProvider order={order}>
|
||||
<OrderDraftPage
|
||||
loading={loading}
|
||||
disabled={loading}
|
||||
errors={errors}
|
||||
onNoteAdd={variables =>
|
||||
|
@ -224,7 +225,6 @@ export const OrderDraftDetails: React.FC<OrderDraftDetailsProps> = ({
|
|||
hasMore={users?.data?.search?.pageInfo?.hasNextPage || false}
|
||||
onFetchMore={loadMoreCustomers}
|
||||
fetchUsers={searchUsers}
|
||||
loading={users.loading}
|
||||
usersLoading={users.loading}
|
||||
onCustomerEdit={handleCustomerChange}
|
||||
onDraftFinalize={() => orderDraftFinalize.mutate({ id })}
|
||||
|
|
|
@ -186,7 +186,7 @@ export const OrderNormalDetails: React.FC<OrderNormalDetailsProps> = ({
|
|||
/>
|
||||
<OrderDetailsPage
|
||||
onOrderReturn={() => navigate(orderReturnUrl(id))}
|
||||
disabled={
|
||||
loading={
|
||||
updateMetadataOpts.loading || updatePrivateMetadataOpts.loading
|
||||
}
|
||||
errors={errors}
|
||||
|
|
|
@ -183,7 +183,7 @@ export const OrderUnconfirmedDetails: React.FC<
|
|||
<OrderLineDiscountProvider order={order}>
|
||||
<OrderDetailsPage
|
||||
onOrderReturn={() => navigate(orderReturnUrl(id))}
|
||||
disabled={
|
||||
loading={
|
||||
updateMetadataOpts.loading || updatePrivateMetadataOpts.loading
|
||||
}
|
||||
errors={errors}
|
||||
|
|
|
@ -9,7 +9,7 @@ import { getDefaultNotifierSuccessErrorData } from "@dashboard/hooks/useNotifier
|
|||
import { getById } from "@dashboard/misc";
|
||||
import { OrderDiscountCommonInput } from "@dashboard/orders/components/OrderDiscountCommonModal/types";
|
||||
import { ConfirmButtonTransitionState } from "@saleor/macaw-ui";
|
||||
import React, { createContext, useState } from "react";
|
||||
import React, { createContext, useContext, useState } from "react";
|
||||
import { useIntl } from "react-intl";
|
||||
|
||||
import {
|
||||
|
@ -44,6 +44,16 @@ interface DiscountProviderProps {
|
|||
export const OrderLineDiscountContext =
|
||||
createContext<GetOrderLineDiscountContextConsumerProps>(null);
|
||||
|
||||
export const useOrderLineDiscountContext = () => {
|
||||
const context = useContext(OrderLineDiscountContext);
|
||||
|
||||
if (context === null) {
|
||||
throw new Error("You are outside order line discount context");
|
||||
}
|
||||
|
||||
return context;
|
||||
};
|
||||
|
||||
export const OrderLineDiscountProvider: React.FC<DiscountProviderProps> = ({
|
||||
children,
|
||||
order,
|
||||
|
|
|
@ -78,7 +78,7 @@ export const ProductVariants: React.FC<ProductVariantsProps> = ({
|
|||
getColumnData(c, channels, warehouses, variantAttributes, intl),
|
||||
)
|
||||
: [],
|
||||
[variantAttributes, warehouses, channels],
|
||||
[variantAttributes, warehouses, channels, intl],
|
||||
);
|
||||
|
||||
const {
|
||||
|
@ -103,7 +103,7 @@ export const ProductVariants: React.FC<ProductVariantsProps> = ({
|
|||
searchAttributeValues: onAttributeValuesSearch,
|
||||
...opts,
|
||||
}),
|
||||
[columns, variants],
|
||||
[channels, columns, onAttributeValuesSearch, variants],
|
||||
);
|
||||
|
||||
const getCellError = React.useCallback(
|
||||
|
@ -117,7 +117,7 @@ export const ProductVariants: React.FC<ProductVariantsProps> = ({
|
|||
searchAttributeValues: onAttributeValuesSearch,
|
||||
...opts,
|
||||
}),
|
||||
[columns, variants, errors],
|
||||
[errors, columns, channels, variants, onAttributeValuesSearch],
|
||||
);
|
||||
|
||||
return (
|
||||
|
|
|
@ -70,6 +70,10 @@ const messages = defineMessages({
|
|||
defaultMessage: "Shipping method is required for this order",
|
||||
description: "error message",
|
||||
},
|
||||
noZeroValue: {
|
||||
defaultMessage: "Ensure this value is greater than 0.",
|
||||
id: "YzLUXA",
|
||||
},
|
||||
});
|
||||
|
||||
function getOrderErrorMessage(
|
||||
|
@ -104,6 +108,8 @@ function getOrderErrorMessage(
|
|||
return intl.formatMessage(messages.shippingRequired);
|
||||
case OrderErrorCode.VOID_INACTIVE_PAYMENT:
|
||||
return intl.formatMessage(messages.cannotVoid);
|
||||
case OrderErrorCode.ZERO_QUANTITY:
|
||||
return intl.formatMessage(messages.noZeroValue);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
14
src/utils/toFixed.test.ts
Normal file
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