Order drafts list datagrid (#3765)

This commit is contained in:
Paweł Chyła 2023-07-04 09:27:17 +02:00 committed by GitHub
parent b4f11eff66
commit 09c9024e0d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 712 additions and 397 deletions

View file

@ -0,0 +1,5 @@
---
"saleor-dashboard": minor
---
Introduce datagrid on order draft list

View file

@ -1330,10 +1330,6 @@
"context": "staff member status", "context": "staff member status",
"string": "Inactive" "string": "Inactive"
}, },
"7a1S4K": {
"context": "tab name",
"string": "All Drafts"
},
"7dhhzL": { "7dhhzL": {
"context": "bulk issue gift cards dialog title", "context": "bulk issue gift cards dialog title",
"string": "Bulk Issue Gift Cards" "string": "Bulk Issue Gift Cards"
@ -2929,6 +2925,9 @@
"context": "all authorized amount from transactions in order", "context": "all authorized amount from transactions in order",
"string": "Authorized" "string": "Authorized"
}, },
"IzECoP": {
"string": "Search draft orders..."
},
"IzEVek": { "IzEVek": {
"context": "bulk disable label", "context": "bulk disable label",
"string": "Deactivate" "string": "Deactivate"
@ -3138,9 +3137,6 @@
"KHZlmi": { "KHZlmi": {
"string": "Discount Type" "string": "Discount Type"
}, },
"KIh25E": {
"string": "No draft orders found"
},
"KKQUMK": { "KKQUMK": {
"context": "edit menu item, header", "context": "edit menu item, header",
"string": "Edit Item" "string": "Edit Item"
@ -3486,9 +3482,6 @@
"NGc9kE": { "NGc9kE": {
"string": "Page type deleted" "string": "Page type deleted"
}, },
"NJEe12": {
"string": "Search Draft"
},
"NJbzcP": { "NJbzcP": {
"context": "dialog header", "context": "dialog header",
"string": "Cancel Orders" "string": "Cancel Orders"
@ -5103,6 +5096,9 @@
"context": "no address is set in draft order", "context": "no address is set in draft order",
"string": "Not set" "string": "Not set"
}, },
"YJ2uRR": {
"string": "Bulk delete draft orders"
},
"YJ4TXc": { "YJ4TXc": {
"context": "tab name", "context": "tab name",
"string": "All Staff Members" "string": "All Staff Members"
@ -7061,6 +7057,10 @@
"context": "section header", "context": "section header",
"string": "Ongoing Installations" "string": "Ongoing Installations"
}, },
"nJ0tek": {
"context": "tab name",
"string": "All draft orders"
},
"nKjLjT": { "nKjLjT": {
"context": "error message", "context": "error message",
"string": "Slug must be unique for each warehouse" "string": "Slug must be unique for each warehouse"

View file

@ -26,7 +26,7 @@ export const Root: React.FC<PropsWithChildren<TopNavProps>> = ({
return ( return (
<TopNavWrapper withoutBorder={withoutBorder}> <TopNavWrapper withoutBorder={withoutBorder}>
{href && <TopNavLink to={href} />} {href && <TopNavLink to={href} />}
<Box __flex={isAlignToRight ? 1 : 0}> <Box __flex={isAlignToRight ? 1 : 0} __minWidth="max-content">
<Text variant="title" size="small"> <Text variant="title" size="small">
{title} {title}
</Text> </Text>

View file

@ -10,6 +10,7 @@ import { PaginatorContextValues } from "./hooks/usePaginator";
import { import {
FetchMoreProps, FetchMoreProps,
FilterPageProps, FilterPageProps,
FilterPresetsProps,
ListActions, ListActions,
SearchPageProps, SearchPageProps,
SortPage, SortPage,
@ -305,6 +306,16 @@ export const tabPageProps: TabPageProps = {
tabs: ["Tab X"], tabs: ["Tab X"],
}; };
export const filterPresetsProps: FilterPresetsProps = {
selectedFilterPreset: 0,
onFilterPresetsAll: () => undefined,
onFilterPresetChange: () => undefined,
onFilterPresetDelete: () => undefined,
onFilterPresetPresetSave: () => undefined,
onFilterPresetUpdate: () => undefined,
filterPresets: ["Tab X"],
};
export const paginatorContextValues: PaginatorContextValues = { export const paginatorContextValues: PaginatorContextValues = {
endCursor: "", endCursor: "",
startCursor: "", startCursor: "",

View file

@ -1,237 +0,0 @@
// @ts-strict-ignore
import Checkbox from "@dashboard/components/Checkbox";
import { DateTime } from "@dashboard/components/Date";
import Money from "@dashboard/components/Money";
import ResponsiveTable from "@dashboard/components/ResponsiveTable";
import Skeleton from "@dashboard/components/Skeleton";
import TableCellHeader from "@dashboard/components/TableCellHeader";
import TableHead from "@dashboard/components/TableHead";
import { TablePaginationWithContext } from "@dashboard/components/TablePagination";
import TableRowLink from "@dashboard/components/TableRowLink";
import { OrderDraftListQuery } from "@dashboard/graphql";
import {
maybe,
renderCollection,
transformOrderStatus,
transformPaymentStatus,
} from "@dashboard/misc";
import { OrderDraftListUrlSortField, orderUrl } from "@dashboard/orders/urls";
import {
ListActions,
ListProps,
RelayToFlat,
SortPage,
} from "@dashboard/types";
import { getArrowDirection } from "@dashboard/utils/sort";
import { TableBody, TableCell, TableFooter } from "@material-ui/core";
import { makeStyles } from "@saleor/macaw-ui";
import React from "react";
import { FormattedMessage, useIntl } from "react-intl";
const useStyles = makeStyles(
theme => ({
[theme.breakpoints.up("lg")]: {
colCustomer: {
width: 300,
},
colDate: {
width: 300,
},
colNumber: {
width: 160,
},
colTotal: {},
},
colCustomer: {},
colDate: {},
colNumber: {
paddingLeft: 0,
},
colTotal: {
textAlign: "right",
},
link: {
cursor: "pointer",
},
}),
{ name: "OrderDraftList" },
);
interface OrderDraftListProps
extends ListProps,
ListActions,
SortPage<OrderDraftListUrlSortField> {
orders: RelayToFlat<OrderDraftListQuery["draftOrders"]>;
}
export const OrderDraftList: React.FC<OrderDraftListProps> = props => {
const {
disabled,
settings,
orders,
onUpdateListSettings,
onSort,
isChecked,
selected,
sort,
toggle,
toggleAll,
toolbar,
} = props;
const classes = useStyles(props);
const intl = useIntl();
const orderDraftList = orders
? orders.map(order => ({
...order,
paymentStatus: transformPaymentStatus(order.paymentStatus, intl),
status: transformOrderStatus(order.status, intl),
}))
: undefined;
const numberOfColumns = orderDraftList?.length === 0 ? 4 : 5;
return (
<ResponsiveTable>
<TableHead
colSpan={numberOfColumns}
selected={selected}
disabled={disabled}
items={orders}
toggleAll={toggleAll}
toolbar={toolbar}
>
<TableCellHeader
direction={
sort.sort === OrderDraftListUrlSortField.number
? getArrowDirection(sort.asc)
: undefined
}
arrowPosition="right"
onClick={() => onSort(OrderDraftListUrlSortField.number)}
className={classes.colNumber}
>
<FormattedMessage id="kFkPWB" defaultMessage="Number" />
</TableCellHeader>
<TableCellHeader
direction={
sort.sort === OrderDraftListUrlSortField.date
? getArrowDirection(sort.asc)
: undefined
}
onClick={() => onSort(OrderDraftListUrlSortField.date)}
className={classes.colDate}
>
<FormattedMessage
id="mCP0UD"
defaultMessage="Date"
description="order draft creation date"
/>
</TableCellHeader>
<TableCellHeader
direction={
sort.sort === OrderDraftListUrlSortField.customer
? getArrowDirection(sort.asc)
: undefined
}
onClick={() => onSort(OrderDraftListUrlSortField.customer)}
className={classes.colCustomer}
>
<FormattedMessage id="hkENym" defaultMessage="Customer" />
</TableCellHeader>
<TableCellHeader textAlign="right" className={classes.colTotal}>
<FormattedMessage
id="1Uj0Wd"
defaultMessage="Total"
description="order draft total price"
/>
</TableCellHeader>
</TableHead>
<TableFooter>
<TableRowLink>
<TablePaginationWithContext
colSpan={numberOfColumns}
settings={settings}
onUpdateListSettings={onUpdateListSettings}
/>
</TableRowLink>
</TableFooter>
<TableBody>
{renderCollection(
orderDraftList,
order => {
const isSelected = order ? isChecked(order.id) : false;
return (
<TableRowLink
data-test-id="draft-order-table-row"
hover={!!order}
className={!!order ? classes.link : undefined}
href={order && orderUrl(order.id)}
key={order ? order.id : "skeleton"}
selected={isSelected}
>
<TableCell padding="checkbox">
<Checkbox
checked={isSelected}
disabled={disabled}
disableClickPropagation
onChange={() => toggle(order.id)}
/>
</TableCell>
<TableCell className={classes.colNumber}>
{maybe(() => order.number) ? (
"#" + order.number
) : (
<Skeleton />
)}
</TableCell>
<TableCell className={classes.colDate}>
{maybe(() => order.created) ? (
<DateTime date={order.created} plain />
) : (
<Skeleton />
)}
</TableCell>
<TableCell className={classes.colCustomer}>
{maybe(() => order.billingAddress) ? (
<>
{order.billingAddress.firstName}
&nbsp;
{order.billingAddress.lastName}
</>
) : maybe(() => order.userEmail) !== undefined ? (
order.userEmail
) : (
<Skeleton />
)}
</TableCell>
<TableCell className={classes.colTotal} align="right">
{maybe(() => order.total.gross) ? (
<Money money={order.total.gross} />
) : (
<Skeleton />
)}
</TableCell>
</TableRowLink>
);
},
() => (
<TableRowLink>
<TableCell colSpan={numberOfColumns}>
<FormattedMessage
id="KIh25E"
defaultMessage="No draft orders found"
/>
</TableCell>
</TableRowLink>
),
)}
</TableBody>
</ResponsiveTable>
);
};
OrderDraftList.displayName = "OrderDraftList";
export default OrderDraftList;

View file

@ -1,2 +0,0 @@
export { default } from "./OrderDraftList";
export * from "./OrderDraftList";

View file

@ -0,0 +1,150 @@
// @ts-strict-ignore
import ColumnPicker from "@dashboard/components/ColumnPicker/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 { TablePaginationWithContext } from "@dashboard/components/TablePagination";
import { OrderDraftListQuery } from "@dashboard/graphql";
import useLocale from "@dashboard/hooks/useLocale";
import { OrderDraftListUrlSortField, orderUrl } from "@dashboard/orders/urls";
import { ListProps, RelayToFlat, SortPage } from "@dashboard/types";
import { Item } from "@glideapps/glide-data-grid";
import { Box } from "@saleor/macaw-ui/next";
import React, { useCallback, useMemo } from "react";
import { useIntl } from "react-intl";
import { createGetCellContent, getColumns } from "./datagrid";
import { messages } from "./messages";
import { canBeSorted } from "./utils";
interface OrderDraftListDatagridProps
extends ListProps,
SortPage<OrderDraftListUrlSortField> {
orders: RelayToFlat<OrderDraftListQuery["draftOrders"]>;
hasRowHover?: boolean;
onRowClick?: (id: string) => void;
onSelectOrderDraftIds;
}
export const OrderDraftListDatagrid = ({
disabled,
orders,
sort,
onSort,
hasRowHover,
onRowClick,
settings,
onUpdateListSettings,
onSelectOrderDraftIds,
}: OrderDraftListDatagridProps) => {
const intl = useIntl();
const { locale } = useLocale();
const datagridState = useDatagridChangeState();
const availableColumns = useMemo(() => getColumns(intl, sort), [intl, sort]);
const {
columns,
availableColumnsChoices,
columnChoices,
defaultColumns,
onColumnMoved,
onColumnResize,
onColumnsChange,
picker,
} = useColumnsDefault(availableColumns);
// eslint-disable-next-line react-hooks/exhaustive-deps
const getCellContent = useCallback(
createGetCellContent({ orders, columns, locale }),
[columns, locale, orders],
);
const handleHeaderClick = useCallback(
(col: number) => {
const columnName = columns[col].id as OrderDraftListUrlSortField;
if (canBeSorted(columnName)) {
onSort(columnName);
}
},
[columns, onSort],
);
const handleRowClick = useCallback(
([_, row]: Item) => {
if (!onRowClick) {
return;
}
const rowData = orders[row];
onRowClick(rowData.id);
},
[onRowClick, orders],
);
const handleRowAnchor = useCallback(
([, row]: Item) => {
const rowData = orders[row];
return orderUrl(rowData.id);
},
[orders],
);
return (
<DatagridChangeStateContext.Provider value={datagridState}>
<Datagrid
readonly
rowMarkers="checkbox"
columnSelect="single"
freezeColumns={1}
hasRowHover={hasRowHover}
loading={disabled}
availableColumns={columns}
verticalBorder={col => col > 0}
getCellContent={getCellContent}
getCellError={() => false}
menuItems={() => []}
emptyText={intl.formatMessage(messages.emptyText)}
rows={orders?.length ?? 0}
selectionActions={() => null}
onRowSelectionChange={onSelectOrderDraftIds}
onColumnMoved={onColumnMoved}
onColumnResize={onColumnResize}
onHeaderClicked={handleHeaderClick}
onRowClick={handleRowClick}
rowAnchor={handleRowAnchor}
renderColumnPicker={defaultProps => (
<ColumnPicker
{...defaultProps}
IconButtonProps={{
...defaultProps.IconButtonProps,
disabled: orders.length === 0,
}}
availableColumns={availableColumnsChoices}
initialColumns={columnChoices}
defaultColumns={defaultColumns}
onSave={onColumnsChange}
hasMore={false}
loading={false}
onFetchMore={() => undefined}
onQueryChange={picker.setQuery}
query={picker.query}
/>
)}
/>
<Box paddingX={6}>
<TablePaginationWithContext
component="div"
colSpan={1}
settings={settings}
disabled={disabled}
onUpdateListSettings={onUpdateListSettings}
/>
</Box>
</DatagridChangeStateContext.Provider>
);
};

View file

@ -0,0 +1,51 @@
import { OrderDraftListQuery } from "@dashboard/graphql";
import { RelayToFlat } from "@dashboard/types";
import { getCustomerName } from "./datagrid";
describe("getCustomerName", () => {
it("should return billing address first name and last name when exists", () => {
// Arrange
const data = {
billingAddress: {
firstName: "John",
lastName: "Doe",
},
} as RelayToFlat<NonNullable<OrderDraftListQuery["draftOrders"]>>[number];
// Act
const result = getCustomerName(data);
// Assert
expect(result).toEqual("John Doe");
});
it("should return user email when exists", () => {
// Arrange
const data = {
billingAddress: {
city: "New York",
},
userEmail: "john@doe.com",
} as RelayToFlat<NonNullable<OrderDraftListQuery["draftOrders"]>>[number];
// Act
const result = getCustomerName(data);
// Assert
expect(result).toEqual("john@doe.com");
});
it("should return - when no user email and billing address", () => {
// Arrange
const data = {} as RelayToFlat<
NonNullable<OrderDraftListQuery["draftOrders"]>
>[number];
// Act
const result = getCustomerName(data);
// Assert
expect(result).toEqual("-");
});
});

View file

@ -0,0 +1,94 @@
// @ts-strict-ignore
import {
moneyCell,
readonlyTextCell,
} from "@dashboard/components/Datagrid/customCells/cells";
import { AvailableColumn } from "@dashboard/components/Datagrid/types";
import { Locale } from "@dashboard/components/Locale";
import { OrderDraftListQuery } from "@dashboard/graphql";
import { RelayToFlat, Sort } from "@dashboard/types";
import { getColumnSortDirectionIcon } from "@dashboard/utils/columns/getColumnSortDirectionIcon";
import { GridCell, Item } from "@glideapps/glide-data-grid";
import moment from "moment";
import { IntlShape } from "react-intl";
import { columnsMessages } from "./messages";
export const getColumns = (intl: IntlShape, sort: Sort): AvailableColumn[] => [
{
id: "number",
title: intl.formatMessage(columnsMessages.number),
width: 100,
icon: getColumnSortDirectionIcon(sort, "number"),
},
{
id: "date",
title: intl.formatMessage(columnsMessages.date),
width: 200,
icon: getColumnSortDirectionIcon(sort, "date"),
},
{
id: "customer",
title: intl.formatMessage(columnsMessages.customer),
width: 200,
icon: getColumnSortDirectionIcon(sort, "customer"),
},
{
id: "total",
title: intl.formatMessage(columnsMessages.total),
width: 200,
icon: getColumnSortDirectionIcon(sort, "total"),
},
];
export const createGetCellContent =
({
orders,
locale,
columns,
}: {
orders: RelayToFlat<OrderDraftListQuery["draftOrders"]>;
columns: AvailableColumn[];
locale: Locale;
}) =>
([column, row]: Item): GridCell => {
const rowData = orders[row];
const columnId = columns[column]?.id;
if (!columnId) {
return readonlyTextCell("");
}
switch (columnId) {
case "number":
return readonlyTextCell(`#${rowData.number}`);
case "date":
return readonlyTextCell(
moment(rowData.created).locale(locale).format("lll"),
);
case "customer":
return readonlyTextCell(getCustomerName(rowData));
case "total":
return moneyCell(
rowData.total?.gross?.amount ?? 0,
rowData.total?.gross?.currency ?? "",
{
readonly: true,
},
);
}
};
export function getCustomerName(
rowData: RelayToFlat<OrderDraftListQuery["draftOrders"]>[number],
) {
if (rowData?.billingAddress?.firstName && rowData?.billingAddress?.lastName) {
return `${rowData.billingAddress.firstName} ${rowData.billingAddress.lastName}`;
}
if (rowData.userEmail) {
return rowData.userEmail;
}
return "-";
}

View file

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

View file

@ -0,0 +1,29 @@
import { defineMessages } from "react-intl";
export const messages = defineMessages({
emptyText: {
defaultMessage: "No orders found",
id: "RlfqSV",
},
});
export const columnsMessages = defineMessages({
number: {
defaultMessage: "Number",
id: "kFkPWB",
},
date: {
id: "mCP0UD",
defaultMessage: "Date",
description: "order draft creation date",
},
customer: {
id: "hkENym",
defaultMessage: "Customer",
},
total: {
id: "1Uj0Wd",
defaultMessage: "Total",
description: "order draft total price",
},
});

View file

@ -0,0 +1,12 @@
import { OrderDraftListUrlSortField } from "@dashboard/orders/urls";
export function canBeSorted(sort: OrderDraftListUrlSortField) {
switch (sort) {
case OrderDraftListUrlSortField.number:
case OrderDraftListUrlSortField.date:
case OrderDraftListUrlSortField.customer:
return true;
default:
return false;
}
}

View file

@ -0,0 +1,38 @@
import { Button, Tooltip, TrashBinIcon } from "@saleor/macaw-ui/next";
import React, { forwardRef, ReactNode, useState } from "react";
interface OrderDraftListDeleteButtonProps {
onClick: () => void;
children: ReactNode;
}
export const OrderDraftListDeleteButton = forwardRef<
HTMLButtonElement,
OrderDraftListDeleteButtonProps
>(({ onClick, children }, ref) => {
const [isTooltipOpen, setIsTooltipOpen] = useState(false);
return (
<Tooltip open={isTooltipOpen}>
<Tooltip.Trigger>
<Button
ref={ref}
onMouseOver={() => {
setIsTooltipOpen(true);
}}
onMouseLeave={() => {
setIsTooltipOpen(false);
}}
onClick={onClick}
icon={<TrashBinIcon />}
variant="secondary"
data-test-id="delete-categories-button"
/>
</Tooltip.Trigger>
<Tooltip.Content side="bottom">
<Tooltip.Arrow />
{children}
</Tooltip.Content>
</Tooltip>
);
});

View file

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

View file

@ -0,0 +1,108 @@
import { TopNav } from "@dashboard/components/AppLayout";
import { LimitsInfo } from "@dashboard/components/AppLayout/LimitsInfo";
import { FilterPresetsSelect } from "@dashboard/components/FilterPresetsSelect";
import { RefreshLimitsQuery } from "@dashboard/graphql";
import { sectionNames } from "@dashboard/intl";
import { FilterPresetsProps } from "@dashboard/types";
import { hasLimits, isLimitReached } from "@dashboard/utils/limits";
import { Box, Button, ChevronRightIcon } from "@saleor/macaw-ui/next";
import React from "react";
import { FormattedMessage, useIntl } from "react-intl";
export interface OrderDraftListHeaderProps
extends Omit<FilterPresetsProps, "onTabDelete"> {
limits: RefreshLimitsQuery["shop"]["limits"];
hasPresetsChanged: boolean;
isFilterPresetOpen: boolean;
disabled: boolean;
onAdd: () => void;
setFilterPresetOpen: (open: boolean) => void;
}
export const OrderDraftListHeader = ({
hasPresetsChanged,
onFilterPresetChange,
onFilterPresetDelete,
onFilterPresetUpdate,
onFilterPresetPresetSave,
filterPresets,
selectedFilterPreset,
onFilterPresetsAll,
isFilterPresetOpen,
setFilterPresetOpen,
disabled,
limits,
onAdd,
}: OrderDraftListHeaderProps) => {
const intl = useIntl();
const limitsReached = isLimitReached(limits, "orders");
return (
<TopNav
title={intl.formatMessage(sectionNames.draftOrders)}
withoutBorder
isAlignToRight={false}
>
<Box
__flex={1}
display="flex"
justifyContent="space-between"
alignItems="center"
>
<Box display="flex">
<Box marginX={3} display="flex" alignItems="center">
<ChevronRightIcon />
</Box>
<FilterPresetsSelect
presetsChanged={hasPresetsChanged}
onSelect={onFilterPresetChange}
onRemove={onFilterPresetDelete}
onUpdate={onFilterPresetUpdate}
savedPresets={filterPresets}
activePreset={selectedFilterPreset}
onSelectAll={onFilterPresetsAll}
onSave={onFilterPresetPresetSave}
isOpen={isFilterPresetOpen}
onOpenChange={setFilterPresetOpen}
selectAllLabel={intl.formatMessage({
id: "nJ0tek",
defaultMessage: "All draft orders",
description: "tab name",
})}
/>
</Box>
<Box display="flex" alignItems="center" gap={2}>
<Button
variant="primary"
disabled={disabled || limitsReached}
onClick={onAdd}
data-test-id="create-draft-order-button"
>
<FormattedMessage
id="LshEVn"
defaultMessage="Create order"
description="button"
/>
</Button>
{hasLimits(limits, "orders") && (
<LimitsInfo
text={intl.formatMessage(
{
id: "w2eTzO",
defaultMessage: "{count}/{max} orders",
description: "placed orders counter",
},
{
count: limits.currentUsage.orders,
max: limits.allowedUsage.orders,
},
)}
/>
)}
</Box>
</Box>
</TopNav>
);
};

View file

@ -1,6 +1,7 @@
// @ts-strict-ignore // @ts-strict-ignore
import { import {
filterPageProps, filterPageProps,
filterPresetsProps,
limits, limits,
limitsReached, limitsReached,
listActionsProps, listActionsProps,
@ -22,6 +23,7 @@ const props: OrderDraftListPageProps = {
...listActionsProps, ...listActionsProps,
...pageListProps.default, ...pageListProps.default,
...searchPageProps, ...searchPageProps,
...filterPresetsProps,
...sortPageProps, ...sortPageProps,
...tabPageProps, ...tabPageProps,
...filterPageProps, ...filterPageProps,
@ -45,6 +47,10 @@ const props: OrderDraftListPageProps = {
...sortPageProps.sort, ...sortPageProps.sort,
sort: OrderDraftListUrlSortField.number, sort: OrderDraftListUrlSortField.number,
}, },
onDraftOrdersDelete: () => undefined,
onSelectOrderDraftIds: () => undefined,
selectedOrderDraftIds: [],
hasPresetsChanged: () => false,
}; };
const meta: Meta<typeof OrderDraftListPage> = { const meta: Meta<typeof OrderDraftListPage> = {

View file

@ -1,25 +1,22 @@
// @ts-strict-ignore // @ts-strict-ignore
import { LimitsInfo } from "@dashboard/components/AppLayout/LimitsInfo"; import { ListFilters } from "@dashboard/components/AppLayout/ListFilters";
import { TopNav } from "@dashboard/components/AppLayout/TopNav";
import { Button } from "@dashboard/components/Button";
import FilterBar from "@dashboard/components/FilterBar";
import { OrderDraftListQuery, RefreshLimitsQuery } from "@dashboard/graphql"; import { OrderDraftListQuery, RefreshLimitsQuery } from "@dashboard/graphql";
import { sectionNames } from "@dashboard/intl";
import { OrderDraftListUrlSortField } from "@dashboard/orders/urls"; import { OrderDraftListUrlSortField } from "@dashboard/orders/urls";
import { import {
FilterPageProps, FilterPagePropsWithPresets,
ListActions,
PageListProps, PageListProps,
RelayToFlat, RelayToFlat,
SortPage, SortPage,
TabPageProps,
} from "@dashboard/types"; } from "@dashboard/types";
import { hasLimits, isLimitReached } from "@dashboard/utils/limits"; import { isLimitReached } from "@dashboard/utils/limits";
import { Card } from "@material-ui/core"; import { Card } from "@material-ui/core";
import React from "react"; import { Box } from "@saleor/macaw-ui/next";
import { FormattedMessage, useIntl } from "react-intl"; import React, { useState } from "react";
import { useIntl } from "react-intl";
import OrderDraftList from "../OrderDraftList"; import { OrderDraftListDatagrid } from "../OrderDraftListDatagrid";
import { OrderDraftListDeleteButton } from "../OrderDraftListDeleteButton";
import { OrderDraftListHeader } from "../OrderDraftListHeader/OrderDraftListHeader";
import OrderLimitReached from "../OrderLimitReached"; import OrderLimitReached from "../OrderLimitReached";
import { import {
createFilterStructure, createFilterStructure,
@ -29,90 +26,103 @@ import {
export interface OrderDraftListPageProps export interface OrderDraftListPageProps
extends PageListProps, extends PageListProps,
ListActions, FilterPagePropsWithPresets<OrderDraftFilterKeys, OrderDraftListFilterOpts>,
FilterPageProps<OrderDraftFilterKeys, OrderDraftListFilterOpts>, SortPage<OrderDraftListUrlSortField> {
SortPage<OrderDraftListUrlSortField>,
TabPageProps {
limits: RefreshLimitsQuery["shop"]["limits"]; limits: RefreshLimitsQuery["shop"]["limits"];
orders: RelayToFlat<OrderDraftListQuery["draftOrders"]>; orders: RelayToFlat<OrderDraftListQuery["draftOrders"]>;
selectedOrderDraftIds: string[];
hasPresetsChanged: () => boolean;
onAdd: () => void; onAdd: () => void;
onDraftOrdersDelete: () => void;
onSelectOrderDraftIds: (ids: number[], clearSelection: () => void) => void;
} }
const OrderDraftListPage: React.FC<OrderDraftListPageProps> = ({ const OrderDraftListPage: React.FC<OrderDraftListPageProps> = ({
currentTab, selectedFilterPreset,
disabled, disabled,
filterOpts, filterOpts,
initialSearch, initialSearch,
limits, limits,
onAdd, onAdd,
onAll, onFilterPresetsAll,
onFilterChange, onFilterChange,
onSearchChange, onSearchChange,
onTabChange, onFilterPresetChange,
onTabDelete, onFilterPresetDelete,
onTabSave, onFilterPresetUpdate,
tabs, onFilterPresetPresetSave,
filterPresets,
hasPresetsChanged,
onDraftOrdersDelete,
onFilterAttributeFocus,
currencySymbol,
selectedOrderDraftIds,
...listProps ...listProps
}) => { }) => {
const intl = useIntl(); const intl = useIntl();
const structure = createFilterStructure(intl, filterOpts); const [isFilterPresetOpen, setFilterPresetOpen] = useState(false);
const filterStructure = createFilterStructure(intl, filterOpts);
const limitsReached = isLimitReached(limits, "orders"); const limitsReached = isLimitReached(limits, "orders");
return ( return (
<> <>
<TopNav title={intl.formatMessage(sectionNames.draftOrders)}> <OrderDraftListHeader
<Button disabled={disabled}
variant="primary" selectedFilterPreset={selectedFilterPreset}
disabled={disabled || limitsReached} hasPresetsChanged={hasPresetsChanged()}
onClick={onAdd} isFilterPresetOpen={isFilterPresetOpen}
data-test-id="create-draft-order-button" setFilterPresetOpen={setFilterPresetOpen}
> limits={limits}
<FormattedMessage onAdd={onAdd}
id="LshEVn" onFilterPresetsAll={onFilterPresetsAll}
defaultMessage="Create order" onFilterPresetDelete={onFilterPresetDelete}
description="button" onFilterPresetChange={onFilterPresetChange}
/> onFilterPresetPresetSave={onFilterPresetPresetSave}
</Button> onFilterPresetUpdate={onFilterPresetUpdate}
{hasLimits(limits, "orders") && ( filterPresets={filterPresets}
<LimitsInfo />
text={intl.formatMessage(
{
id: "w2eTzO",
defaultMessage: "{count}/{max} orders",
description: "placed orders counter",
},
{
count: limits.currentUsage.orders,
max: limits.allowedUsage.orders,
},
)}
/>
)}
</TopNav>
{limitsReached && <OrderLimitReached />} {limitsReached && <OrderLimitReached />}
<Card> <Card>
<FilterBar <Box
allTabLabel={intl.formatMessage({ display="flex"
id: "7a1S4K", flexDirection="column"
defaultMessage: "All Drafts", width="100%"
description: "tab name", alignItems="stretch"
})} justifyContent="space-between"
currentTab={currentTab} >
filterStructure={structure} <ListFilters
initialSearch={initialSearch} currencySymbol={currencySymbol}
searchPlaceholder={intl.formatMessage({ initialSearch={initialSearch}
id: "NJEe12", onFilterChange={onFilterChange}
defaultMessage: "Search Draft", onFilterAttributeFocus={onFilterAttributeFocus}
})} onSearchChange={onSearchChange}
tabs={tabs} filterStructure={filterStructure}
onAll={onAll} searchPlaceholder={intl.formatMessage({
onFilterChange={onFilterChange} id: "IzECoP",
onSearchChange={onSearchChange} defaultMessage: "Search draft orders...",
onTabChange={onTabChange} })}
onTabDelete={onTabDelete} actions={
onTabSave={onTabSave} <Box display="flex" gap={4}>
{selectedOrderDraftIds.length > 0 && (
<OrderDraftListDeleteButton onClick={onDraftOrdersDelete}>
{intl.formatMessage({
id: "YJ2uRR",
defaultMessage: "Bulk delete draft orders",
})}
</OrderDraftListDeleteButton>
)}
</Box>
}
/>
</Box>
<OrderDraftListDatagrid
disabled={disabled}
hasRowHover={!isFilterPresetOpen}
{...listProps}
/> />
<OrderDraftList disabled={disabled} {...listProps} />
</Card> </Card>
</> </>
); );

View file

@ -3,16 +3,14 @@ import ChannelPickerDialog from "@dashboard/channels/components/ChannelPickerDia
import ActionDialog from "@dashboard/components/ActionDialog"; import ActionDialog from "@dashboard/components/ActionDialog";
import useAppChannel from "@dashboard/components/AppLayout/AppChannelContext"; import useAppChannel from "@dashboard/components/AppLayout/AppChannelContext";
import DeleteFilterTabDialog from "@dashboard/components/DeleteFilterTabDialog"; import DeleteFilterTabDialog from "@dashboard/components/DeleteFilterTabDialog";
import SaveFilterTabDialog, { import SaveFilterTabDialog from "@dashboard/components/SaveFilterTabDialog";
SaveFilterTabDialogFormData,
} from "@dashboard/components/SaveFilterTabDialog";
import { useShopLimitsQuery } from "@dashboard/components/Shop/queries"; import { useShopLimitsQuery } from "@dashboard/components/Shop/queries";
import { import {
useOrderDraftBulkCancelMutation, useOrderDraftBulkCancelMutation,
useOrderDraftCreateMutation, useOrderDraftCreateMutation,
useOrderDraftListQuery, useOrderDraftListQuery,
} from "@dashboard/graphql"; } from "@dashboard/graphql";
import useBulkActions from "@dashboard/hooks/useBulkActions"; import { useFilterPresets } from "@dashboard/hooks/useFilterPresets";
import useListSettings from "@dashboard/hooks/useListSettings"; import useListSettings from "@dashboard/hooks/useListSettings";
import useNavigator from "@dashboard/hooks/useNavigator"; import useNavigator from "@dashboard/hooks/useNavigator";
import useNotifier from "@dashboard/hooks/useNotifier"; import useNotifier from "@dashboard/hooks/useNotifier";
@ -21,6 +19,7 @@ import usePaginator, {
createPaginationState, createPaginationState,
PaginatorContext, PaginatorContext,
} from "@dashboard/hooks/usePaginator"; } from "@dashboard/hooks/usePaginator";
import { useRowSelection } from "@dashboard/hooks/useRowSelection";
import { maybe } from "@dashboard/misc"; import { maybe } from "@dashboard/misc";
import { ListViews } from "@dashboard/types"; import { ListViews } from "@dashboard/types";
import createDialogActionHandlers from "@dashboard/utils/handlers/dialogActionHandlers"; import createDialogActionHandlers from "@dashboard/utils/handlers/dialogActionHandlers";
@ -29,8 +28,8 @@ import createSortHandler from "@dashboard/utils/handlers/sortHandler";
import { mapEdgesToItems, mapNodeToChoice } from "@dashboard/utils/maps"; import { mapEdgesToItems, mapNodeToChoice } from "@dashboard/utils/maps";
import { getSortParams } from "@dashboard/utils/sort"; import { getSortParams } from "@dashboard/utils/sort";
import { DialogContentText } from "@material-ui/core"; import { DialogContentText } from "@material-ui/core";
import { DeleteIcon, IconButton } from "@saleor/macaw-ui"; import isEqual from "lodash/isEqual";
import React from "react"; import React, { useCallback } from "react";
import { FormattedMessage, useIntl } from "react-intl"; import { FormattedMessage, useIntl } from "react-intl";
import OrderDraftListPage from "../../components/OrderDraftListPage"; import OrderDraftListPage from "../../components/OrderDraftListPage";
@ -41,14 +40,10 @@ import {
orderUrl, orderUrl,
} from "../../urls"; } from "../../urls";
import { import {
deleteFilterTab,
getActiveFilters,
getFilterOpts, getFilterOpts,
getFilterQueryParam, getFilterQueryParam,
getFiltersCurrentTab,
getFilterTabs,
getFilterVariables, getFilterVariables,
saveFilterTab, storageUtils,
} from "./filters"; } from "./filters";
import { getSortQueryVariables } from "./sort"; import { getSortQueryVariables } from "./sort";
@ -59,16 +54,20 @@ interface OrderDraftListProps {
export const OrderDraftList: React.FC<OrderDraftListProps> = ({ params }) => { export const OrderDraftList: React.FC<OrderDraftListProps> = ({ params }) => {
const navigate = useNavigator(); const navigate = useNavigator();
const notify = useNotifier(); const notify = useNotifier();
const { isSelected, listElements, reset, toggle, toggleAll } = useBulkActions( const intl = useIntl();
params.ids,
);
const { updateListSettings, settings } = useListSettings( const { updateListSettings, settings } = useListSettings(
ListViews.DRAFT_LIST, ListViews.DRAFT_LIST,
); );
usePaginationReset(orderDraftListUrl, params, settings.rowNumber); usePaginationReset(orderDraftListUrl, params, settings.rowNumber);
const intl = useIntl(); const {
clearRowSelection,
selectedRowIds,
setClearDatagridRowSelectionCallback,
setSelectedRowIds,
} = useRowSelection(params);
const [orderDraftBulkDelete, orderDraftBulkDeleteOpts] = const [orderDraftBulkDelete, orderDraftBulkDeleteOpts] =
useOrderDraftBulkCancelMutation({ useOrderDraftBulkCancelMutation({
@ -82,7 +81,7 @@ export const OrderDraftList: React.FC<OrderDraftListProps> = ({ params }) => {
}), }),
}); });
refetch(); refetch();
reset(); clearRowSelection();
closeModal(); closeModal();
} }
}, },
@ -108,17 +107,14 @@ export const OrderDraftList: React.FC<OrderDraftListProps> = ({ params }) => {
}, },
}); });
const tabs = getFilterTabs();
const currentTab = getFiltersCurrentTab(params, tabs);
const [changeFilters, resetFilters, handleSearchChange] = const [changeFilters, resetFilters, handleSearchChange] =
createFilterHandlers({ createFilterHandlers({
cleanupFn: reset, cleanupFn: clearRowSelection,
createUrl: orderDraftListUrl, createUrl: orderDraftListUrl,
getFilterQueryParam, getFilterQueryParam,
navigate, navigate,
params, params,
keepActiveTab: true,
}); });
const [openModal, closeModal] = createDialogActionHandlers< const [openModal, closeModal] = createDialogActionHandlers<
@ -126,41 +122,40 @@ export const OrderDraftList: React.FC<OrderDraftListProps> = ({ params }) => {
OrderDraftListUrlQueryParams OrderDraftListUrlQueryParams
>(navigate, orderDraftListUrl, params); >(navigate, orderDraftListUrl, params);
const handleTabChange = (tab: number) => { const {
reset(); selectedPreset,
navigate( presets,
orderDraftListUrl({ hasPresetsChange,
activeTab: tab.toString(), onPresetChange,
...getFilterTabs()[tab - 1].data, onPresetDelete,
}), onPresetSave,
); onPresetUpdate,
}; setPresetIdToDelete,
presetIdToDelete,
const handleTabDelete = () => { } = useFilterPresets({
deleteFilterTab(currentTab); params,
reset(); reset: clearRowSelection,
navigate(orderDraftListUrl()); getUrl: orderDraftListUrl,
}; storageUtils,
});
const handleTabSave = (data: SaveFilterTabDialogFormData) => {
saveFilterTab(data.name, getActiveFilters(params));
handleTabChange(tabs.length + 1);
};
const paginationState = createPaginationState(settings.rowNumber, params); const paginationState = createPaginationState(settings.rowNumber, params);
const queryVariables = React.useMemo( const queryVariables = React.useMemo(
() => ({ () => ({
...paginationState, ...paginationState,
filter: getFilterVariables(params), filter: getFilterVariables(params),
sort: getSortQueryVariables(params), sort: getSortQueryVariables(params),
}), }),
[params, settings.rowNumber], [paginationState, params],
); );
const { data, loading, refetch } = useOrderDraftListQuery({ const { data, loading, refetch } = useOrderDraftListQuery({
displayLoader: true, displayLoader: true,
variables: queryVariables, variables: queryVariables,
}); });
const orderDrafts = mapEdgesToItems(data?.draftOrders);
const paginationValues = usePaginator({ const paginationValues = usePaginator({
pageInfo: maybe(() => data.draftOrders.pageInfo), pageInfo: maybe(() => data.draftOrders.pageInfo),
paginationState, paginationState,
@ -172,48 +167,70 @@ export const OrderDraftList: React.FC<OrderDraftListProps> = ({ params }) => {
const onOrderDraftBulkDelete = () => const onOrderDraftBulkDelete = () =>
orderDraftBulkDelete({ orderDraftBulkDelete({
variables: { variables: {
ids: params.ids, ids: selectedRowIds,
}, },
}); });
const handleSetSelectedOrderDraftIds = useCallback(
(rows: number[], clearSelection: () => void) => {
if (!orderDrafts) {
return;
}
const rowsIds = rows.map(row => orderDrafts[row].id);
const haveSaveValues = isEqual(rowsIds, selectedRowIds);
if (!haveSaveValues) {
setSelectedRowIds(rowsIds);
}
setClearDatagridRowSelectionCallback(clearSelection);
},
[
orderDrafts,
selectedRowIds,
setClearDatagridRowSelectionCallback,
setSelectedRowIds,
],
);
return ( return (
<PaginatorContext.Provider value={paginationValues}> <PaginatorContext.Provider value={paginationValues}>
<OrderDraftListPage <OrderDraftListPage
currentTab={currentTab} selectedFilterPreset={selectedPreset}
filterOpts={getFilterOpts(params)} filterOpts={getFilterOpts(params)}
limits={limitOpts.data?.shop.limits} limits={limitOpts.data?.shop.limits}
initialSearch={params.query || ""} initialSearch={params.query || ""}
onSearchChange={handleSearchChange} onSearchChange={handleSearchChange}
onFilterChange={changeFilters} onFilterChange={changeFilters}
onAll={resetFilters} onFilterPresetsAll={resetFilters}
onTabChange={handleTabChange} onFilterPresetChange={onPresetChange}
onTabDelete={() => openModal("delete-search")} onFilterPresetDelete={(id: number) => {
onTabSave={() => openModal("save-search")} setPresetIdToDelete(id);
tabs={tabs.map(tab => tab.name)} openModal("delete-search");
}}
onFilterPresetUpdate={onPresetUpdate}
onFilterPresetPresetSave={() => openModal("save-search")}
filterPresets={presets.map(tab => tab.name)}
disabled={loading} disabled={loading}
settings={settings} settings={settings}
orders={mapEdgesToItems(data?.draftOrders)} orders={orderDrafts}
onAdd={() => openModal("create-order")} onAdd={() => openModal("create-order")}
onSort={handleSort} onSort={handleSort}
onUpdateListSettings={updateListSettings}
isChecked={isSelected}
selected={listElements.length}
sort={getSortParams(params)} sort={getSortParams(params)}
toggle={toggle} currencySymbol={channel?.currencyCode}
toggleAll={toggleAll} hasPresetsChanged={hasPresetsChange}
toolbar={ onDraftOrdersDelete={() =>
<IconButton openModal("remove", {
variant="secondary" ids: selectedRowIds,
color="primary" })
onClick={() =>
openModal("remove", {
ids: listElements,
})
}
>
<DeleteIcon />
</IconButton>
} }
onUpdateListSettings={(...props) => {
clearRowSelection();
updateListSettings(...props);
}}
selectedOrderDraftIds={selectedRowIds}
onSelectOrderDraftIds={handleSetSelectedOrderDraftIds}
/> />
<ActionDialog <ActionDialog
confirmButtonState={orderDraftBulkDeleteOpts.status} confirmButtonState={orderDraftBulkDeleteOpts.status}
@ -233,9 +250,9 @@ export const OrderDraftList: React.FC<OrderDraftListProps> = ({ params }) => {
defaultMessage="{counter,plural,one{Are you sure you want to delete this order draft?} other{Are you sure you want to delete {displayQuantity} order drafts?}}" defaultMessage="{counter,plural,one{Are you sure you want to delete this order draft?} other{Are you sure you want to delete {displayQuantity} order drafts?}}"
description="dialog content" description="dialog content"
values={{ values={{
counter: maybe(() => params.ids.length), counter: maybe(() => selectedRowIds.length),
displayQuantity: ( displayQuantity: (
<strong>{maybe(() => params.ids.length)}</strong> <strong>{maybe(() => selectedRowIds.length)}</strong>
), ),
}} }}
/> />
@ -245,14 +262,14 @@ export const OrderDraftList: React.FC<OrderDraftListProps> = ({ params }) => {
open={params.action === "save-search"} open={params.action === "save-search"}
confirmButtonState="default" confirmButtonState="default"
onClose={closeModal} onClose={closeModal}
onSubmit={handleTabSave} onSubmit={onPresetSave}
/> />
<DeleteFilterTabDialog <DeleteFilterTabDialog
open={params.action === "delete-search"} open={params.action === "delete-search"}
confirmButtonState="default" confirmButtonState="default"
onClose={closeModal} onClose={closeModal}
onSubmit={handleTabDelete} onSubmit={onPresetDelete}
tabName={maybe(() => tabs[currentTab - 1].name, "...")} tabName={presets[presetIdToDelete - 1]?.name ?? "..."}
/> />
<ChannelPickerDialog <ChannelPickerDialog
channelsChoices={mapNodeToChoice(availableChannels)} channelsChoices={mapNodeToChoice(availableChannels)}

View file

@ -80,8 +80,9 @@ export function getFilterQueryParam(
} }
} }
export const { deleteFilterTab, getFilterTabs, saveFilterTab } = export const storageUtils = createFilterTabUtils<string>(
createFilterTabUtils<OrderDraftListUrlFilters>(ORDER_DRAFT_FILTERS_KEY); ORDER_DRAFT_FILTERS_KEY,
);
export const { areFiltersApplied, getActiveFilters, getFiltersCurrentTab } = export const { areFiltersApplied, getActiveFilters, getFiltersCurrentTab } =
createFilterUtils<OrderDraftListUrlQueryParams, OrderDraftListUrlFilters>( createFilterUtils<OrderDraftListUrlQueryParams, OrderDraftListUrlFilters>(

View file

@ -105,6 +105,7 @@ export const OrderList: React.FC<OrderListProps> = ({ params }) => {
params, params,
defaultSortField: DEFAULT_SORT_KEY, defaultSortField: DEFAULT_SORT_KEY,
hasSortWithRank: true, hasSortWithRank: true,
keepActiveTab: true,
}); });
const [openModal, closeModal] = createDialogActionHandlers< const [openModal, closeModal] = createDialogActionHandlers<

View file

@ -107,12 +107,31 @@ export interface FilterPageProps<TKeys extends string, TOpts extends {}>
filterOpts: TOpts; filterOpts: TOpts;
} }
export interface FilterPagePropsWithPresets<
TKeys extends string,
TOpts extends {},
> extends FilterProps<TKeys>,
SearchPageProps,
FilterPresetsProps {
filterOpts: TOpts;
}
export interface FilterProps<TKeys extends string> { export interface FilterProps<TKeys extends string> {
currencySymbol?: string; currencySymbol?: string;
onFilterChange: (filter: IFilter<TKeys>) => void; onFilterChange: (filter: IFilter<TKeys>) => void;
onFilterAttributeFocus?: (id?: string) => void; onFilterAttributeFocus?: (id?: string) => void;
} }
export interface FilterPresetsProps {
selectedFilterPreset: number;
filterPresets: string[];
onFilterPresetsAll: () => void;
onFilterPresetChange: (id: number) => void;
onFilterPresetUpdate: (name: string) => void;
onFilterPresetDelete: (id: number) => void;
onFilterPresetPresetSave: () => void;
}
export interface TabPageProps { export interface TabPageProps {
currentTab: number; currentTab: number;
tabs: string[]; tabs: string[];