diff --git a/.changeset/sixty-kangaroos-cheer.md b/.changeset/sixty-kangaroos-cheer.md new file mode 100644 index 00000000000..dcc83bdc3c1 --- /dev/null +++ b/.changeset/sixty-kangaroos-cheer.md @@ -0,0 +1,5 @@ +--- +"saleor-dashboard": minor +--- + +Implement refunds datagrid on order details diff --git a/.featureFlags/generated.tsx b/.featureFlags/generated.tsx index 4089ebdef61..1d72dcddd74 100644 --- a/.featureFlags/generated.tsx +++ b/.featureFlags/generated.tsx @@ -1,13 +1,16 @@ // @ts-nocheck -import L10838 from "./images/discounts-list.png" -import W33914 from "./images/filters.png" +import E60240 from "./images/discounts-list.png" +import T97919 from "./images/filters.png" -const discounts_rules = () => (<>

Discount rules

+const discounts_rules = () => (<>

Discount rules

Apply the new discounts rules to narrow your promotions audience. Set up conditions and channels that must be fulfilled to apply defined reward.

) -const product_filters = () => (<>

new filters

+const improved_refunds = () => (<>

Experience new refund flow supporting multiple transactions.

+ +) +const product_filters = () => (<>

new filters

Experience the new look and enhanced abilities of new fitering mechanism. Easily combine any criteria you want, and quickly browse their values.

) @@ -21,6 +24,15 @@ export const AVAILABLE_FLAGS = [{ enabled: false, payload: "default", } +},{ + name: "improved_refunds", + displayName: "Improved refunds", + component: improved_refunds, + visible: false, + content: { + enabled: false, + payload: "default", + } },{ name: "product_filters", displayName: "Products filtering", diff --git a/.featureFlags/improved-refunds.md b/.featureFlags/improved-refunds.md new file mode 100644 index 00000000000..c27b8610149 --- /dev/null +++ b/.featureFlags/improved-refunds.md @@ -0,0 +1,11 @@ +--- +name: improved_refunds +displayName: Improved refunds +enabled: false +payload: "default" +visible: false +--- + +Experience new refund flow supporting multiple transactions. + + diff --git a/locale/defaultMessages.json b/locale/defaultMessages.json index 19ee5b30adf..0082f778f61 100644 --- a/locale/defaultMessages.json +++ b/locale/defaultMessages.json @@ -741,6 +741,10 @@ "context": "delete app", "string": "Deleting this app, you will remove installation of the app. If you are paying for app subscription, remember to unsubscribe from the app in Saleor Marketplace." }, + "2v3tZ2": { + "context": "refund datagrid reason column", + "string": "Reason" + }, "2x9ZgK": { "context": "card title", "string": "Refund balance" @@ -983,6 +987,10 @@ "context": "modal button", "string": "Upload URL" }, + "4XRIV5": { + "context": "add new refund button", + "string": "Add new refund" + }, "4XhJY+": { "context": "dialog header", "string": "Add product from {channelName}" @@ -1473,6 +1481,10 @@ "context": "default tax class name for new tax classes", "string": "New tax class" }, + "8DyXiQ": { + "context": "refund datagrid status column", + "string": "Status" + }, "8EGagh": { "context": "search box label", "string": "Filter Countries" @@ -3258,6 +3270,10 @@ "context": "select product informations to be exported", "string": "Information exported:" }, + "JxWKvr": { + "context": "refund datagrid account column", + "string": "Account" + }, "JyQoES": { "context": "delete attribute value", "string": "Are you sure you want to delete \"{name}\" value?" @@ -4977,6 +4993,10 @@ "context": "warehouse input", "string": "Warehouse" }, + "W6VS7F": { + "context": "refund datagrid amount column", + "string": "Amount" + }, "W6nwjo": { "string": "Draft" }, @@ -7778,6 +7798,10 @@ "context": "order status", "string": "Fulfilled" }, + "pnPj0b": { + "context": "refund datagrid date column", + "string": "Date" + }, "ppLwx3": { "context": "number of categories", "string": "Categories ({quantity})" @@ -8292,6 +8316,10 @@ "context": "balance curreny missing error message", "string": "Balance currency is missing" }, + "tZmVfa": { + "context": "refund section header", + "string": "Refund" + }, "tZnV8L": { "context": "Header row stock label", "string": "Stock" diff --git a/src/config.ts b/src/config.ts index cef1d9e944d..f180c96464c 100644 --- a/src/config.ts +++ b/src/config.ts @@ -68,7 +68,10 @@ export interface AppListViewSettings { [ListViews.ORDER_DRAFT_DETAILS_LIST]: ListSettings; [ListViews.PRODUCT_DETAILS]: ListSettings; [ListViews.VOUCHER_CODES]: ListSettings; + [ListViews.ORDER_REFUNDS]: ListSettings; } +// TODO: replace with +// type AppListViewSettings = Record; export const defaultListSettings: AppListViewSettings = { [ListViews.APPS_LIST]: { @@ -194,6 +197,10 @@ export const defaultListSettings: AppListViewSettings = { [ListViews.VOUCHER_CODES]: { rowNumber: PAGINATE_BY, }, + [ListViews.ORDER_REFUNDS]: { + rowNumber: PAGINATE_BY, + columns: ["status", "amount", "reason", "date", "account"], + }, }; export const APP_VERSION = diff --git a/src/orders/components/OrderPaymentOrTransaction/OrderPaymentOrTransaction.test.tsx b/src/orders/components/OrderPaymentOrTransaction/OrderPaymentOrTransaction.test.tsx index b15d6bd0222..3d7fd514df7 100644 --- a/src/orders/components/OrderPaymentOrTransaction/OrderPaymentOrTransaction.test.tsx +++ b/src/orders/components/OrderPaymentOrTransaction/OrderPaymentOrTransaction.test.tsx @@ -52,6 +52,10 @@ jest.mock("react-router-dom", () => ({ Link: jest.fn(({ to, ...props }) => ), })); +jest.mock("@dashboard/featureFlags", () => ({ + useFlag: jest.fn(() => ({ enabled: false })), +})); + describe("OrderPaymentOrTransaction", () => { const order = orderFixture(undefined); const sharedProps = { diff --git a/src/orders/components/OrderPaymentOrTransaction/OrderTransactionsWrapper.tsx b/src/orders/components/OrderPaymentOrTransaction/OrderTransactionsWrapper.tsx index 02da93e21f3..f9da7fff908 100644 --- a/src/orders/components/OrderPaymentOrTransaction/OrderTransactionsWrapper.tsx +++ b/src/orders/components/OrderPaymentOrTransaction/OrderTransactionsWrapper.tsx @@ -1,5 +1,6 @@ // @ts-strict-ignore import CardSpacer from "@dashboard/components/CardSpacer"; +import { useFlag } from "@dashboard/featureFlags"; import { OrderDetailsFragment, OrderDetailsQuery, @@ -11,6 +12,7 @@ import OrderAddTransaction from "../OrderAddTransaction"; import { useStyles } from "../OrderDetailsPage/styles"; import OrderGrantedRefunds from "../OrderGrantedRefunds"; import OrderPaymentSummaryCard from "../OrderPaymentSummaryCard"; +import { OrderRefundDatagrid } from "../OrderRefundDatagrid"; import OrderSummaryCard from "../OrderSummaryCard"; import OrderTransaction from "../OrderTransaction"; import OrderTransactionGiftCard from "../OrderTransactionGiftCard"; @@ -45,6 +47,9 @@ export const OrderTransactionsWrapper: React.FC = ({ () => getFilteredPayments(order), [order], ); + + const { enabled } = useFlag("improved_refunds"); + return ( <>
@@ -52,12 +57,23 @@ export const OrderTransactionsWrapper: React.FC = ({
- {order?.grantedRefunds?.length !== 0 ? ( - <> - - - - ) : null} + <> + {enabled && ( + <> + + + + )} + {order?.grantedRefunds?.length !== 0 && !enabled && ( + <> + + + + )} +
{order?.transactions?.map(transaction => ( = ({ + grantedRefunds, + orderId, +}) => { + const { datagrid, currentTheme, settings, handleColumnChange } = + useDatagridOpts(ListViews.ORDER_REFUNDS); + + const orderDraftDetailsStaticColumns = useOrderRefundStaticColumns(); + + const { + handlers, + visibleColumns, + staticColumns, + selectedColumns, + recentlyAddedColumn, + } = useColumns({ + staticColumns: orderDraftDetailsStaticColumns, + selectedColumns: settings?.columns ?? [], + onSave: handleColumnChange, + }); + + const getCellContent = createGetCellContent({ + columns: visibleColumns, + refunds: grantedRefunds, + currentTheme, + }); + + const getMenuItems = React.useCallback( + index => [ + { + label: "", + Icon: ( + + + + ), + onSelect: () => false, + }, + ], + [], + ); + + return ( + + + + + + {/** TODO: Add modal */} + + + + col > 1} + availableColumns={visibleColumns} + emptyText={""} + getCellContent={getCellContent} + getCellError={() => false} + rows={grantedRefunds.length} + selectionActions={() => null} + onColumnResize={handlers.onResize} + onColumnMoved={handlers.onMove} + recentlyAddedColumn={recentlyAddedColumn} + renderColumnPicker={() => ( + + )} + /> + + + ); +}; diff --git a/src/orders/components/OrderRefundDatagrid/datagrid.test.ts b/src/orders/components/OrderRefundDatagrid/datagrid.test.ts new file mode 100644 index 00000000000..27194e4f3f9 --- /dev/null +++ b/src/orders/components/OrderRefundDatagrid/datagrid.test.ts @@ -0,0 +1,149 @@ +import { grantedRefunds } from "@dashboard/orders/fixtures"; +import { GridCellKind } from "@glideapps/glide-data-grid"; + +import { createGetCellContent, useOrderRefundStaticColumns } from "./datagrid"; + +const currentTheme = "defaultLight"; + +jest.mock("react-intl", () => ({ + useIntl: () => ({ + formatMessage: ({ defaultMessage }: { defaultMessage: string }) => + defaultMessage, + }), + defineMessages: jest.fn(x => x), +})); + +jest.mock("@dashboard/components/Datagrid/hooks/useEmptyColumn", () => ({ + useEmptyColumn: () => ({ + id: "empty", + title: "", + width: 20, + }), +})); + +describe("Order refund datagrid", () => { + it("presents grant refund with status draft created by user", () => { + // Arrange + const refunds = grantedRefunds; + const columns = useOrderRefundStaticColumns(); + + // Act + const getCellContent = createGetCellContent({ + refunds, + columns, + currentTheme, + }); + + // Assert + + // Status column + expect(getCellContent([1, 1])).toEqual( + expect.objectContaining({ + data: { + kind: "tags-cell", + possibleTags: [{ tag: "DRAFT", color: "#ffe6c8" }], + tags: ["DRAFT"], + }, + }), + ); + + // Amount column + expect(getCellContent([2, 1])).toEqual( + expect.objectContaining({ + data: { + kind: "money-cell", + value: 234.93, + currency: "USD", + }, + }), + ); + + // Reason column + expect(getCellContent([3, 1])).toEqual( + expect.objectContaining({ + data: "Products arrived damaged", + kind: GridCellKind.Text, + }), + ); + + // Date column + expect(getCellContent([4, 1])).toEqual( + expect.objectContaining({ + data: { + kind: "date-cell", + value: "2022-08-22T10:40:22.226875+00:00", + }, + }), + ); + + // Account column + expect(getCellContent([5, 1])).toEqual( + expect.objectContaining({ + data: "john.doe@example.com", + kind: GridCellKind.Text, + }), + ); + }); + it("presents grant refund with status draft created by app", () => { + // Arrange + const refunds = grantedRefunds; + const columns = useOrderRefundStaticColumns(); + + // Act + const getCellContent = createGetCellContent({ + refunds, + columns, + currentTheme, + }); + + // Assert + + // Status column + expect(getCellContent([1, 0])).toEqual( + expect.objectContaining({ + data: { + kind: "tags-cell", + possibleTags: [{ tag: "DRAFT", color: "#ffe6c8" }], + tags: ["DRAFT"], + }, + }), + ); + + // Amount column + expect(getCellContent([2, 0])).toEqual( + expect.objectContaining({ + data: { + kind: "money-cell", + value: 234.93, + currency: "USD", + }, + }), + ); + + // Reason column + expect(getCellContent([3, 0])).toEqual( + expect.objectContaining({ + data: "Products returned", + kind: GridCellKind.Text, + }), + ); + + // Date column + expect(getCellContent([4, 0])).toEqual( + expect.objectContaining({ + data: { + kind: "date-cell", + value: "2022-08-22T10:40:22.226875+00:00", + }, + }), + ); + + // Account column + expect(getCellContent([5, 0])).toEqual( + expect.objectContaining({ + data: "", + kind: GridCellKind.Text, + }), + ); + }); +}); diff --git a/src/orders/components/OrderRefundDatagrid/datagrid.ts b/src/orders/components/OrderRefundDatagrid/datagrid.ts new file mode 100644 index 00000000000..0cccbccd7f1 --- /dev/null +++ b/src/orders/components/OrderRefundDatagrid/datagrid.ts @@ -0,0 +1,137 @@ +import { + dateCell, + moneyCell, + readonlyTextCell, + tagsCell, +} from "@dashboard/components/Datagrid/customCells/cells"; +import { + UseDatagridChangeState, + useDatagridChangeState, +} from "@dashboard/components/Datagrid/hooks/useDatagridChange"; +import { useEmptyColumn } from "@dashboard/components/Datagrid/hooks/useEmptyColumn"; +import { AvailableColumn } from "@dashboard/components/Datagrid/types"; +import { OrderDetailsFragment } from "@dashboard/graphql"; +import useListSettings from "@dashboard/hooks/useListSettings"; +import { getStatusColor } from "@dashboard/misc"; +import { ListSettings, ListViews } from "@dashboard/types"; +import { GridCell, Item } from "@glideapps/glide-data-grid"; +import { DefaultTheme, useTheme } from "@saleor/macaw-ui-next"; +import React from "react"; +import { useIntl } from "react-intl"; + +import { refundGridMessages } from "./messages"; + +const useOrderRefundConstantColumns = () => { + const intl = useIntl(); + return [ + { + id: "status", + title: intl.formatMessage(refundGridMessages.statusCell), + width: 80, + }, + { + id: "amount", + title: intl.formatMessage(refundGridMessages.amountCell), + width: 150, + }, + { + id: "reason", + title: intl.formatMessage(refundGridMessages.reasonCell), + width: 300, + }, + { + id: "date", + title: intl.formatMessage(refundGridMessages.dateCell), + width: 300, + }, + { + id: "account", + title: intl.formatMessage(refundGridMessages.accountCell), + width: 300, + }, + ]; +}; + +export const useOrderRefundStaticColumns = () => { + const emptyColumn = useEmptyColumn(); + const constantColumns = useOrderRefundConstantColumns(); + return [emptyColumn, ...constantColumns]; +}; + +export const createGetCellContent = + ({ + refunds, + columns, + currentTheme, + }: { + refunds: OrderDetailsFragment["grantedRefunds"] | undefined; + columns: AvailableColumn[]; + currentTheme: DefaultTheme; + }) => + ([column, row]: Item): GridCell => { + const rowData = refunds?.[row]; + const columnId = columns[column]?.id; + + if (!columnId || !rowData) { + return readonlyTextCell(""); + } + + // TODO: replace with actual status when API available + const color = getStatusColor({ + status: "generic", + currentTheme, + }); + + switch (columnId) { + case "status": + return tagsCell( + [ + { + tag: "DRAFT", + color: color.base, + }, + ], + ["DRAFT"], + ); + case "amount": + return moneyCell(rowData.amount.amount, rowData.amount.currency ?? "", { + readonly: true, + }); + case "reason": + return readonlyTextCell(rowData.reason ?? "", false); + case "date": + return dateCell(rowData.createdAt); + case "account": + return readonlyTextCell(rowData.user?.email ?? "", false); + default: + return readonlyTextCell(""); + } + }; + +export const useDatagridOpts = ( + view: ListViews, +): { + datagrid: UseDatagridChangeState; + currentTheme: DefaultTheme; + settings: ListSettings; + handleColumnChange: (picked: string[]) => void; +} => { + const datagrid = useDatagridChangeState(); + const { theme: currentTheme } = useTheme(); + const { updateListSettings, settings } = useListSettings(view); + + const handleColumnChange = React.useCallback( + picked => { + if (updateListSettings) { + updateListSettings("columns", picked.filter(Boolean)); + } + }, + [updateListSettings], + ); + return { + datagrid, + currentTheme, + settings, + handleColumnChange, + }; +}; diff --git a/src/orders/components/OrderRefundDatagrid/index.ts b/src/orders/components/OrderRefundDatagrid/index.ts new file mode 100644 index 00000000000..d9b51044520 --- /dev/null +++ b/src/orders/components/OrderRefundDatagrid/index.ts @@ -0,0 +1 @@ +export * from "./OrderRefundDatagrid"; diff --git a/src/orders/components/OrderRefundDatagrid/messages.ts b/src/orders/components/OrderRefundDatagrid/messages.ts new file mode 100644 index 00000000000..6b70b0787eb --- /dev/null +++ b/src/orders/components/OrderRefundDatagrid/messages.ts @@ -0,0 +1,39 @@ +import { defineMessages } from "react-intl"; + +export const refundGridMessages = defineMessages({ + statusCell: { + defaultMessage: "Status", + id: "8DyXiQ", + description: "refund datagrid status column", + }, + amountCell: { + defaultMessage: "Amount", + id: "W6VS7F", + description: "refund datagrid amount column", + }, + reasonCell: { + defaultMessage: "Reason", + id: "2v3tZ2", + description: "refund datagrid reason column", + }, + dateCell: { + defaultMessage: "Date", + id: "pnPj0b", + description: "refund datagrid date column", + }, + accountCell: { + defaultMessage: "Account", + id: "JxWKvr", + description: "refund datagrid account column", + }, + addNewRefund: { + defaultMessage: "Add new refund", + id: "4XRIV5", + description: "add new refund button", + }, + refundSection: { + defaultMessage: "Refund", + id: "tZmVfa", + description: "refund section header", + }, +}); diff --git a/src/types.ts b/src/types.ts index 891c5086121..48e326f0382 100644 --- a/src/types.ts +++ b/src/types.ts @@ -50,6 +50,7 @@ export enum ListViews { // Not strictly a list view, but there's a list of variants PRODUCT_DETAILS = "PRODUCT_DETAILS", VOUCHER_CODES = "VOUCHER_CODES", + ORDER_REFUNDS = "ORDER_REFUNDS", } export interface ListProps {