Introduce datagrid on Discounts list (#3939)

Co-authored-by: wojteknowacki <wojciech.nowacki@saleor.io>
This commit is contained in:
Paweł Chyła 2023-07-19 15:31:13 +02:00 committed by GitHub
parent 33b4199cec
commit 52f58eb00a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 622 additions and 462 deletions

View file

@ -0,0 +1,5 @@
---
"saleor-dashboard": minor
---
Introduct datagrid on discounts list page

View file

@ -166,9 +166,7 @@ describe("As an admin I want to create sale for products", () => {
cy.clearSessionData()
.loginUserViaRequest("auth", ONE_PERMISSION_USERS.discount)
*/
cy.visit(urlList.sales)
.expectSkeletonIsVisible()
.waitForProgressBarToNotExist();
cy.visit(urlList.sales);
createSale({
saleName,
channelName: channel.name,

View file

@ -24,7 +24,6 @@ export function createSale({
cy.get(SALES_SELECTORS.createSaleButton)
.click()
.waitForProgressBarToNotBeVisible()
.get(SALES_SELECTORS.nameInput)
.type(saleName)
.get(discountOption)
@ -37,7 +36,6 @@ export function createSale({
.addAliasToGraphRequest("SaleCreate")
.get(SALES_SELECTORS.saveButton)
.click()
.confirmationMessageShouldDisappear()
.waitForRequestAndCheckIfNoErrors("@SaleCreate");
}
@ -102,9 +100,7 @@ export function createSaleWithNewProduct({
cy.clearSessionData()
.loginUserViaRequest("auth", ONE_PERMISSION_USERS.discount)
*/
cy.visit(urlList.sales)
.expectSkeletonIsVisible()
.waitForProgressBarToNotExist();
cy.visit(urlList.sales);
createSale({
saleName: name,
channelName: channel.name,
@ -141,9 +137,7 @@ export function createSaleWithNewVariant({
cy.clearSessionData()
.loginUserViaRequest("auth", ONE_PERMISSION_USERS.discount)
*/
cy.visit(urlList.sales)
.expectSkeletonIsVisible()
.waitForProgressBarToNotExist();
cy.visit(urlList.sales);
createSale({
saleName: name,
channelName: channel.name,

View file

@ -96,6 +96,9 @@
"context": "button",
"string": "Activate"
},
"+bhokL": {
"string": "Search discounts..."
},
"+do3gl": {
"context": "input helper text",
"string": "This number defines quantity of items in checkout line that can be bought. You can override this setting per variant. Leaving this setting empty mean that there is no limits."
@ -2758,6 +2761,9 @@
"context": "create gift card product alert message",
"string": "Create a gift card product"
},
"Hswqx2": {
"string": "Delete discounts"
},
"HvJPcU": {
"string": "Category deleted"
},
@ -5317,6 +5323,10 @@
"a5msuh": {
"string": "Yes"
},
"a6GDem": {
"context": "tab name",
"string": "All discounts"
},
"a9S9Je": {
"context": "page types section name",
"string": "Page Types"
@ -5621,10 +5631,6 @@
"c8nvms": {
"string": "Sales"
},
"c8zJID": {
"context": "tab name",
"string": "All Discounts"
},
"cBHRxx": {
"context": "button",
"string": "Assign Warehouse"
@ -6828,9 +6834,6 @@
"context": "export items as csv file",
"string": "Plain CSV file"
},
"lit2zF": {
"string": "Search Discounts"
},
"ll2dE6": {
"context": "PageTypeDeleteWarningDialog multiple assigned items description",
"string": "Are you sure you want to delete selected page types? If you remove them you wont be able to assign them to created pages."

View file

@ -17,7 +17,7 @@ export interface ListFiltersProps<TKeys extends string = string>
actions?: ReactNode;
}
export const ListFilters = ({
export const ListFilters = <TFilterKeys extends string = string>({
currencySymbol,
filterStructure,
initialSearch,
@ -27,7 +27,7 @@ export const ListFilters = ({
onFilterAttributeFocus,
errorMessages,
actions,
}: ListFiltersProps) => {
}: ListFiltersProps<TFilterKeys>) => {
const isProductPage = window.location.pathname.includes("/products");
const productListingPageFiltersFlag = useFlag("product_filters");
const filtersEnabled = isProductPage && productListingPageFiltersFlag.enabled;
@ -45,7 +45,7 @@ export const ListFilters = ({
{filtersEnabled ? (
<ExpressionFilters />
) : (
<FiltersSelect
<FiltersSelect<TFilterKeys>
errorMessages={errorMessages}
menu={filterStructure}
currencySymbol={currencySymbol}

View file

@ -19,17 +19,17 @@ export interface FilterProps<TFilterKeys extends string = string> {
currencySymbol?: string;
errorMessages?: FilterErrorMessages<TFilterKeys>;
menu: IFilter<TFilterKeys>;
onFilterAdd: (filter: Array<FilterElement<string>>) => void;
onFilterAdd: (filter: Array<FilterElement<TFilterKeys>>) => void;
onFilterAttributeFocus?: (id?: string) => void;
}
export const FiltersSelect = ({
export const FiltersSelect = <TFilterKeys extends string = string>({
currencySymbol,
menu,
onFilterAdd,
onFilterAttributeFocus,
errorMessages,
}: FilterProps) => {
}: FilterProps<TFilterKeys>) => {
const anchor = React.useRef<HTMLDivElement>();
const [isFilterMenuOpened, setFilterMenuOpened] = useState(false);
const [filterErrors, setFilterErrors] = useState<InvalidFilters<string>>({});

View file

@ -0,0 +1 @@
export const PLACEHOLDER = "-";

View file

@ -1,6 +1,7 @@
import React from "react";
import { LocaleConsumer } from "../Locale";
import { formatPercantage } from "./utils";
interface PercentProps {
amount: number;
@ -8,14 +9,7 @@ interface PercentProps {
const Percent: React.FC<PercentProps> = ({ amount }) => (
<LocaleConsumer>
{({ locale }) =>
amount
? (amount / 100).toLocaleString(locale, {
maximumFractionDigits: 2,
style: "percent",
})
: "-"
}
{({ locale }) => formatPercantage(amount, locale)}
</LocaleConsumer>
);
Percent.displayName = "Percent";

View file

@ -0,0 +1,17 @@
import { Locale } from "../Locale";
import { formatPercantage } from "./utils";
describe("formatPercantage", () => {
it('should return "-" when amount is 0', () => {
expect(formatPercantage(0, Locale.EN)).toBe("-");
});
it('should return "-" when amount is undefined', () => {
expect(formatPercantage(undefined, Locale.EN)).toBe("-");
});
it("should return percantage when amount is provided", () => {
expect(formatPercantage(33, Locale.EN)).toBe("33%");
expect(formatPercantage(33.1233, Locale.EN)).toBe("33.12%");
});
});

View file

@ -0,0 +1,13 @@
import { Locale } from "../Locale";
export const formatPercantage = (
amount: number | undefined,
locale: Locale,
) => {
return amount
? (amount / 100).toLocaleString(locale, {
maximumFractionDigits: 2,
style: "percent",
})
: "-";
};

View file

@ -111,6 +111,7 @@ export const defaultListSettings: AppListViewSettings = {
},
[ListViews.SALES_LIST]: {
rowNumber: PAGINATE_BY,
columns: ["name", "startDate", "endDate", "value"],
},
[ListViews.SHIPPING_METHODS_LIST]: {
rowNumber: PAGINATE_BY,

View file

@ -1,268 +0,0 @@
// @ts-strict-ignore
import Checkbox from "@dashboard/components/Checkbox";
import Date from "@dashboard/components/Date";
import Money from "@dashboard/components/Money";
import Percent from "@dashboard/components/Percent";
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 TooltipTableCellHeader from "@dashboard/components/TooltipTableCellHeader";
import { commonTooltipMessages } from "@dashboard/components/TooltipTableCellHeader/messages";
import { SaleListUrlSortField, saleUrl } from "@dashboard/discounts/urls";
import { canBeSorted } from "@dashboard/discounts/views/SaleList/sort";
import { SaleFragment, SaleType } from "@dashboard/graphql";
import { maybe, renderCollection } from "@dashboard/misc";
import {
ChannelProps,
ListActions,
ListProps,
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 clsx from "clsx";
import React from "react";
import { FormattedMessage, useIntl } from "react-intl";
export interface SaleListProps
extends ListProps,
ListActions,
SortPage<SaleListUrlSortField>,
ChannelProps {
sales: SaleFragment[];
}
const useStyles = makeStyles(
theme => ({
[theme.breakpoints.up("lg")]: {
colEnd: {
width: 250,
},
colName: {},
colStart: {
width: 250,
},
colValue: {
width: 200,
},
},
colEnd: {
textAlign: "right",
},
colName: {
paddingLeft: 0,
},
colStart: {
textAlign: "right",
},
colValue: {
textAlign: "right",
},
tableRow: {
cursor: "pointer",
},
textOverflow: {
textOverflow: "ellipsis",
overflow: "hidden",
},
}),
{ name: "SaleList" },
);
const SaleList: React.FC<SaleListProps> = props => {
const {
settings,
disabled,
onUpdateListSettings,
onSort,
sales,
selectedChannelId,
isChecked,
selected,
sort,
toggle,
toggleAll,
toolbar,
filterDependency,
} = props;
const classes = useStyles(props);
const intl = useIntl();
const numberOfColumns = sales?.length === 0 ? 4 : 5;
return (
<ResponsiveTable>
<TableHead
colSpan={numberOfColumns}
selected={selected}
disabled={disabled}
items={sales}
toggleAll={toggleAll}
toolbar={toolbar}
>
<TableCellHeader
direction={
sort.sort === SaleListUrlSortField.name
? getArrowDirection(sort.asc)
: undefined
}
arrowPosition="right"
onClick={() => onSort(SaleListUrlSortField.name)}
className={classes.colName}
>
<FormattedMessage
id="F56hOz"
defaultMessage="Name"
description="sale name"
/>
</TableCellHeader>
<TableCellHeader
direction={
sort.sort === SaleListUrlSortField.startDate
? getArrowDirection(sort.asc)
: undefined
}
textAlign="right"
onClick={() => onSort(SaleListUrlSortField.startDate)}
className={classes.colStart}
>
<FormattedMessage
id="iBSq6l"
defaultMessage="Starts"
description="sale start date"
/>
</TableCellHeader>
<TableCellHeader
direction={
sort.sort === SaleListUrlSortField.endDate
? getArrowDirection(sort.asc)
: undefined
}
textAlign="right"
onClick={() => onSort(SaleListUrlSortField.endDate)}
className={classes.colEnd}
>
<FormattedMessage
id="giF5UV"
defaultMessage="Ends"
description="sale end date"
/>
</TableCellHeader>
<TooltipTableCellHeader
direction={
sort.sort === SaleListUrlSortField.value
? getArrowDirection(sort.asc)
: undefined
}
textAlign="right"
onClick={() => onSort(SaleListUrlSortField.value)}
disabled={
!canBeSorted(SaleListUrlSortField.value, !!selectedChannelId)
}
tooltip={intl.formatMessage(commonTooltipMessages.noFilterSelected, {
filterName: filterDependency.label,
})}
className={classes.colValue}
>
<FormattedMessage
id="XZR590"
defaultMessage="Value"
description="sale value"
/>
</TooltipTableCellHeader>
</TableHead>
<TableFooter>
<TableRowLink>
<TablePaginationWithContext
colSpan={numberOfColumns}
settings={settings}
onUpdateListSettings={onUpdateListSettings}
/>
</TableRowLink>
</TableFooter>
<TableBody>
{renderCollection(
sales,
sale => {
const isSelected = sale ? isChecked(sale.id) : false;
const channel = sale?.channelListings?.find(
lisiting => lisiting.channel.id === selectedChannelId,
);
return (
<TableRowLink
className={!!sale ? classes.tableRow : undefined}
hover={!!sale}
key={sale ? sale.id : "skeleton"}
href={sale && saleUrl(sale.id)}
selected={isSelected}
>
<TableCell padding="checkbox">
<Checkbox
checked={isSelected}
disabled={disabled}
disableClickPropagation
onChange={() => toggle(sale.id)}
/>
</TableCell>
<TableCell
className={clsx(classes.colName, classes.textOverflow)}
>
{maybe<React.ReactNode>(() => sale.name, <Skeleton />)}
</TableCell>
<TableCell className={classes.colStart}>
{sale && sale.startDate ? (
<Date date={sale.startDate} plain />
) : (
<Skeleton />
)}
</TableCell>
<TableCell className={classes.colEnd}>
{sale && sale.endDate ? (
<Date date={sale.endDate} plain />
) : sale && sale.endDate === null ? (
"-"
) : (
<Skeleton />
)}
</TableCell>
<TableCell className={classes.colValue}>
{sale?.type && channel?.discountValue ? (
sale.type === SaleType.FIXED ? (
<Money
money={{
amount: channel.discountValue,
currency: channel.currency,
}}
/>
) : channel?.discountValue ? (
<Percent amount={channel.discountValue} />
) : (
"-"
)
) : sale && !channel ? (
"_"
) : (
<Skeleton />
)}
</TableCell>
</TableRowLink>
);
},
() => (
<TableRowLink>
<TableCell colSpan={numberOfColumns}>
<FormattedMessage id="51HE+Q" defaultMessage="No sales found" />
</TableCell>
</TableRowLink>
),
)}
</TableBody>
</ResponsiveTable>
);
};
SaleList.displayName = "SaleList";
export default SaleList;

View file

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

View file

@ -0,0 +1,175 @@
import { ColumnPicker } from "@dashboard/components/Datagrid/ColumnPicker/ColumnPicker";
import { useColumns } from "@dashboard/components/Datagrid/ColumnPicker/useColumns";
import Datagrid from "@dashboard/components/Datagrid/Datagrid";
import {
DatagridChangeStateContext,
useDatagridChangeState,
} from "@dashboard/components/Datagrid/hooks/useDatagridChange";
import { TablePaginationWithContext } from "@dashboard/components/TablePagination";
import { commonTooltipMessages } from "@dashboard/components/TooltipTableCellHeader/messages";
import { SaleListUrlSortField, saleUrl } from "@dashboard/discounts/urls";
import { SaleFragment } from "@dashboard/graphql";
import useLocale from "@dashboard/hooks/useLocale";
import { ChannelProps, ListProps, 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 { canBeSorted } from "../../views/SaleList/sort";
import {
createGetCellContent,
salesListStaticColumnsAdapter,
} from "./datagrid";
import { messages } from "./messages";
interface SaleListDatagridProps
extends ListProps,
SortPage<SaleListUrlSortField>,
ChannelProps {
sales: SaleFragment[];
onSelectSaleIds: (ids: number[], clearSelection: () => void) => void;
onRowClick: (id: string) => void;
hasRowHover?: boolean;
}
export const SaleListDatagrid = ({
disabled,
onSort,
sales,
selectedChannelId,
sort,
filterDependency,
onUpdateListSettings,
onSelectSaleIds,
onRowClick,
hasRowHover = true,
settings,
}: SaleListDatagridProps) => {
const intl = useIntl();
const { locale } = useLocale();
const datagrid = useDatagridChangeState();
const collectionListStaticColumns = useMemo(
() => salesListStaticColumnsAdapter(intl, sort),
[intl, sort],
);
const onColumnChange = useCallback(
(picked: string[]) => {
if (onUpdateListSettings) {
onUpdateListSettings("columns", picked.filter(Boolean));
}
},
[onUpdateListSettings],
);
const {
handlers,
visibleColumns,
staticColumns,
selectedColumns,
recentlyAddedColumn,
} = useColumns({
staticColumns: collectionListStaticColumns,
selectedColumns: settings?.columns ?? [],
onSave: onColumnChange,
});
const getCellContent = useCallback(
createGetCellContent({
sales,
columns: visibleColumns,
locale,
selectedChannelId,
}),
[sales, selectedChannelId, locale, visibleColumns],
);
const handleRowClick = useCallback(
([_, row]: Item) => {
if (!onRowClick) {
return;
}
const rowData: SaleFragment = sales[row];
onRowClick(rowData.id);
},
[onRowClick, sales],
);
const handleRowAnchor = useCallback(
([, row]: Item) => saleUrl(sales[row].id),
[sales],
);
const handleGetColumnTooltipContent = useCallback(
(col: number): string => {
const columnName = visibleColumns[col].id as SaleListUrlSortField;
if (canBeSorted(columnName, !!selectedChannelId)) {
return "";
}
// Sortable but requrie selected channel
return intl.formatMessage(commonTooltipMessages.noFilterSelected, {
filterName: filterDependency?.label ?? "",
});
},
[filterDependency, intl, selectedChannelId, visibleColumns],
);
const handleHeaderClick = useCallback(
(col: number) => {
const columnName = visibleColumns[col].id as SaleListUrlSortField;
if (canBeSorted(columnName, !!selectedChannelId)) {
onSort(columnName);
}
},
[visibleColumns, onSort],
);
return (
<DatagridChangeStateContext.Provider value={datagrid}>
<Datagrid
readonly
loading={disabled}
rowMarkers="checkbox"
columnSelect="single"
hasRowHover={hasRowHover}
onColumnMoved={handlers.onMove}
onColumnResize={handlers.onResize}
verticalBorder={col => col > 0}
rows={sales?.length ?? 0}
availableColumns={visibleColumns}
emptyText={intl.formatMessage(messages.empty)}
onRowSelectionChange={onSelectSaleIds}
getCellContent={getCellContent}
getCellError={() => false}
selectionActions={() => null}
menuItems={() => []}
onRowClick={handleRowClick}
onHeaderClicked={handleHeaderClick}
rowAnchor={handleRowAnchor}
getColumnTooltipContent={handleGetColumnTooltipContent}
recentlyAddedColumn={recentlyAddedColumn}
renderColumnPicker={() => (
<ColumnPicker
staticColumns={staticColumns}
selectedColumns={selectedColumns}
onToggle={handlers.onToggle}
/>
)}
/>
<Box paddingX={6}>
<TablePaginationWithContext
component="div"
settings={settings}
disabled={disabled}
onUpdateListSettings={onUpdateListSettings}
/>
</Box>
</DatagridChangeStateContext.Provider>
);
};

View file

@ -0,0 +1,114 @@
import { PLACEHOLDER } from "@dashboard/components/Datagrid/const";
import { readonlyTextCell } from "@dashboard/components/Datagrid/customCells/cells";
import { AvailableColumn } from "@dashboard/components/Datagrid/types";
import { Locale } from "@dashboard/components/Locale";
import { formatMoney } from "@dashboard/components/Money";
import { formatPercantage } from "@dashboard/components/Percent/utils";
import { SaleListUrlSortField } from "@dashboard/discounts/urls";
import { SaleFragment } from "@dashboard/graphql";
import { 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 salesListStaticColumnsAdapter = (
intl: IntlShape,
sort: Sort<SaleListUrlSortField>,
) =>
[
{
id: "name",
title: intl.formatMessage(columnsMessages.name),
width: 350,
},
{
id: "startDate",
title: intl.formatMessage(columnsMessages.starts),
width: 200,
},
{
id: "endDate",
title: intl.formatMessage(columnsMessages.ends),
width: 200,
},
{
id: "value",
title: intl.formatMessage(columnsMessages.value),
width: 200,
},
].map(column => ({
...column,
icon: getColumnSortDirectionIcon(sort, column.id),
}));
export const createGetCellContent =
({
sales,
columns,
locale,
selectedChannelId,
}: {
sales: SaleFragment[];
columns: AvailableColumn[];
locale: Locale;
selectedChannelId?: string;
}) =>
([column, row]: Item): GridCell => {
const rowData = sales[row];
const columnId = columns[column]?.id;
const channel = rowData?.channelListings?.find(
lisiting => lisiting.channel.id === selectedChannelId,
);
if (!columnId || !rowData) {
return readonlyTextCell("");
}
switch (columnId) {
case "name":
return readonlyTextCell(rowData.name);
case "startDate":
return readonlyTextCell(
rowData.startDate
? moment(rowData.startDate).locale(locale).format("lll")
: PLACEHOLDER,
);
case "endDate":
return readonlyTextCell(
rowData.endDate
? moment(rowData.endDate).locale(locale).format("lll")
: PLACEHOLDER,
);
case "value":
if (!channel) {
return readonlyTextCell(PLACEHOLDER);
}
if (rowData?.type && channel?.discountValue) {
if (rowData.type === "FIXED") {
return readonlyTextCell(
formatMoney(
{
amount: channel.discountValue,
currency: channel.channel.currencyCode,
},
locale,
),
);
}
return readonlyTextCell(
formatPercantage(channel.discountValue, locale),
);
}
return readonlyTextCell(PLACEHOLDER);
default:
return readonlyTextCell("");
}
};

View file

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

View file

@ -0,0 +1,31 @@
import { defineMessages } from "react-intl";
export const columnsMessages = defineMessages({
name: {
id: "F56hOz",
defaultMessage: "Name",
description: "sale name",
},
starts: {
id: "iBSq6l",
defaultMessage: "Starts",
description: "sale start date",
},
ends: {
id: "giF5UV",
defaultMessage: "Ends",
description: "sale end date",
},
value: {
id: "XZR590",
defaultMessage: "Value",
description: "sale value",
},
});
export const messages = defineMessages({
empty: {
id: "51HE+Q",
defaultMessage: "No sales found",
},
});

View file

@ -1,12 +1,10 @@
// @ts-strict-ignore
import { saleList } from "@dashboard/discounts/fixtures";
import { SaleListUrlSortField } from "@dashboard/discounts/urls";
import {
filterPageProps,
listActionsProps,
filterPresetsProps,
pageListProps,
searchPageProps,
sortPageProps,
tabPageProps,
} from "@dashboard/fixtures";
import { DiscountStatusEnum, DiscountValueTypeEnum } from "@dashboard/graphql";
import { Meta, StoryObj } from "@storybook/react";
@ -15,11 +13,18 @@ import { PaginatorContextDecorator } from "../../../../.storybook/decorators";
import SaleListPage, { SaleListPageProps } from "./SaleListPage";
const props: SaleListPageProps = {
...listActionsProps,
...pageListProps.default,
...filterPageProps,
...searchPageProps,
...sortPageProps,
...tabPageProps,
...filterPresetsProps,
onFilterChange: () => undefined,
selectedSaleIds: [],
onSelectSaleIds: () => {},
onSalesDelete: () => {},
settings: {
...pageListProps.default.settings,
columns: ["name", "startDate", "endDate", "value"],
},
filterOpts: {
channel: {
active: false,
@ -38,8 +43,8 @@ const props: SaleListPageProps = {
started: {
active: false,
value: {
max: undefined,
min: undefined,
max: "",
min: "",
},
},
status: {
@ -76,6 +81,7 @@ export const Loading: Story = {
args: {
...props,
sales: undefined,
disabled: true,
},
parameters: {
chromatic: { diffThreshold: 0.85 },

View file

@ -1,25 +1,29 @@
// @ts-strict-ignore
import { ListFilters } from "@dashboard/components/AppLayout/ListFilters";
import { TopNav } from "@dashboard/components/AppLayout/TopNav";
import { Button } from "@dashboard/components/Button";
import { BulkDeleteButton } from "@dashboard/components/BulkDeleteButton";
import { getByName } from "@dashboard/components/Filter/utils";
import FilterBar from "@dashboard/components/FilterBar";
import { FilterPresetsSelect } from "@dashboard/components/FilterPresetsSelect";
import { ListPageLayout } from "@dashboard/components/Layouts";
import { saleAddUrl, SaleListUrlSortField } from "@dashboard/discounts/urls";
import {
saleAddUrl,
SaleListUrlSortField,
saleUrl,
} from "@dashboard/discounts/urls";
import { SaleFragment } from "@dashboard/graphql";
import useNavigator from "@dashboard/hooks/useNavigator";
import { commonMessages } from "@dashboard/intl";
import {
ChannelProps,
FilterPageProps,
ListActions,
FilterPagePropsWithPresets,
PageListProps,
SortPage,
TabPageProps,
} from "@dashboard/types";
import { Card } from "@material-ui/core";
import React from "react";
import { Box, Button, ChevronRightIcon } from "@saleor/macaw-ui/next";
import React, { useState } from "react";
import { FormattedMessage, useIntl } from "react-intl";
import SaleList from "../SaleList";
import { SaleListDatagrid } from "../SaleListDatagrid";
import {
createFilterStructure,
SaleFilterKeys,
@ -28,70 +32,126 @@ import {
export interface SaleListPageProps
extends PageListProps,
ListActions,
FilterPageProps<SaleFilterKeys, SaleListFilterOpts>,
FilterPagePropsWithPresets<SaleFilterKeys, SaleListFilterOpts>,
SortPage<SaleListUrlSortField>,
TabPageProps,
ChannelProps {
sales: SaleFragment[];
selectedSaleIds: string[];
onSalesDelete: () => void;
onSelectSaleIds: (rows: number[], clearSelection: () => void) => void;
}
const SaleListPage: React.FC<SaleListPageProps> = ({
currentTab,
filterOpts,
initialSearch,
onAll,
onFilterChange,
onSearchChange,
onTabChange,
onTabDelete,
onTabSave,
tabs,
onFilterPresetChange,
onFilterPresetDelete,
onFilterPresetPresetSave,
onFilterPresetUpdate,
onFilterPresetsAll,
hasPresetsChanged,
onSalesDelete,
filterPresets,
selectedSaleIds,
selectedFilterPreset,
currencySymbol,
...listProps
}) => {
const intl = useIntl();
const navigation = useNavigator();
const structure = createFilterStructure(intl, filterOpts);
const [isFilterPresetOpen, setFilterPresetOpen] = useState(false);
const filterDependency = structure.find(getByName("channel"));
const handleRowClick = (id: string) => {
navigation(saleUrl(id));
};
return (
<ListPageLayout>
<TopNav title={intl.formatMessage(commonMessages.discounts)}>
<Button
href={saleAddUrl()}
variant="primary"
data-test-id="create-sale"
<TopNav
isAlignToRight={false}
withoutBorder
title={intl.formatMessage(commonMessages.discounts)}
>
<Box
__flex={1}
display="flex"
justifyContent="space-between"
alignItems="center"
>
<FormattedMessage
id="+MJW+8"
defaultMessage="Create Discount"
description="button"
/>
</Button>
<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: "a6GDem",
defaultMessage: "All discounts",
description: "tab name",
})}
/>
</Box>
<Box>
<Button
onClick={() => navigation(saleAddUrl())}
variant="primary"
data-test-id="create-sale"
>
<FormattedMessage
id="+MJW+8"
defaultMessage="Create Discount"
description="button"
/>
</Button>
</Box>
</Box>
</TopNav>
<Card>
<FilterBar
allTabLabel={intl.formatMessage({
id: "c8zJID",
defaultMessage: "All Discounts",
description: "tab name",
})}
currentTab={currentTab}
filterStructure={structure}
<ListFilters<SaleFilterKeys>
currencySymbol={currencySymbol}
initialSearch={initialSearch}
searchPlaceholder={intl.formatMessage({
id: "lit2zF",
defaultMessage: "Search Discounts",
})}
tabs={tabs}
onAll={onAll}
onFilterChange={onFilterChange}
onSearchChange={onSearchChange}
onTabChange={onTabChange}
onTabDelete={onTabDelete}
onTabSave={onTabSave}
filterStructure={structure}
searchPlaceholder={intl.formatMessage({
id: "+bhokL",
defaultMessage: "Search discounts...",
})}
actions={
<Box display="flex" gap={4}>
{selectedSaleIds.length > 0 && (
<BulkDeleteButton onClick={onSalesDelete}>
<FormattedMessage
defaultMessage="Delete discounts"
id="Hswqx2"
/>
</BulkDeleteButton>
)}
</Box>
}
/>
<SaleListDatagrid
{...listProps}
hasRowHover={!isFilterPresetOpen}
filterDependency={filterDependency}
onRowClick={handleRowClick}
/>
<SaleList filterDependency={filterDependency} {...listProps} />
</Card>
</ListPageLayout>
);

View file

@ -32,8 +32,8 @@ export type SaleListUrlFilters = Filters<SaleListUrlFiltersEnum> &
export type SaleListUrlDialog = "remove" | TabActionDialog;
export enum SaleListUrlSortField {
name = "name",
endDate = "end-date",
startDate = "start-date",
endDate = "endDate",
startDate = "startDate",
type = "type",
value = "value",
}

View file

@ -1,16 +1,14 @@
// @ts-strict-ignore
import ActionDialog from "@dashboard/components/ActionDialog";
import useAppChannel from "@dashboard/components/AppLayout/AppChannelContext";
import DeleteFilterTabDialog from "@dashboard/components/DeleteFilterTabDialog";
import SaveFilterTabDialog, {
SaveFilterTabDialogFormData,
} from "@dashboard/components/SaveFilterTabDialog";
import SaveFilterTabDialog from "@dashboard/components/SaveFilterTabDialog";
import { WindowTitle } from "@dashboard/components/WindowTitle";
import {
SaleFragment,
useSaleBulkDeleteMutation,
useSaleListQuery,
} from "@dashboard/graphql";
import useBulkActions from "@dashboard/hooks/useBulkActions";
import { useFilterPresets } from "@dashboard/hooks/useFilterPresets";
import useListSettings from "@dashboard/hooks/useListSettings";
import useNavigator from "@dashboard/hooks/useNavigator";
import useNotifier from "@dashboard/hooks/useNotifier";
@ -19,8 +17,8 @@ import usePaginator, {
createPaginationState,
PaginatorContext,
} from "@dashboard/hooks/usePaginator";
import { useRowSelection } from "@dashboard/hooks/useRowSelection";
import { commonMessages } from "@dashboard/intl";
import { maybe } from "@dashboard/misc";
import { ListViews } from "@dashboard/types";
import createDialogActionHandlers from "@dashboard/utils/handlers/dialogActionHandlers";
import createFilterHandlers from "@dashboard/utils/handlers/filterHandlers";
@ -28,8 +26,8 @@ import createSortHandler from "@dashboard/utils/handlers/sortHandler";
import { mapEdgesToItems, mapNodeToChoice } from "@dashboard/utils/maps";
import { getSortParams } from "@dashboard/utils/sort";
import { DialogContentText } from "@material-ui/core";
import { DeleteIcon, IconButton } from "@saleor/macaw-ui";
import React, { useEffect } from "react";
import isEqual from "lodash/isEqual";
import React, { useCallback, useEffect } from "react";
import { FormattedMessage, useIntl } from "react-intl";
import SaleListPage from "../../components/SaleListPage";
@ -39,14 +37,10 @@ import {
SaleListUrlQueryParams,
} from "../../urls";
import {
deleteFilterTab,
getActiveFilters,
getFilterOpts,
getFilterQueryParam,
getFiltersCurrentTab,
getFilterTabs,
getFilterVariables,
saveFilterTab,
storageUtils,
} from "./filters";
import { canBeSorted, DEFAULT_SORT_KEY, getSortQueryVariables } from "./sort";
@ -57,9 +51,6 @@ interface SaleListProps {
export const SaleList: React.FC<SaleListProps> = ({ params }) => {
const navigate = useNavigator();
const notify = useNotifier();
const { isSelected, listElements, reset, toggle, toggleAll } = useBulkActions(
params.ids,
);
const { updateListSettings, settings } = useListSettings(
ListViews.SALES_LIST,
);
@ -73,7 +64,7 @@ export const SaleList: React.FC<SaleListProps> = ({ params }) => {
);
const channelOpts = availableChannels
? mapNodeToChoice(availableChannels, channel => channel.slug)
: null;
: [];
const [openModal, closeModal] = createDialogActionHandlers<
SaleListUrlDialog,
@ -95,21 +86,44 @@ export const SaleList: React.FC<SaleListProps> = ({ params }) => {
variables: queryVariables,
});
const tabs = getFilterTabs();
const sales: SaleFragment[] = mapEdgesToItems(data?.sales) ?? [];
const currentTab = getFiltersCurrentTab(params, tabs);
const {
clearRowSelection,
selectedRowIds,
setSelectedRowIds,
setClearDatagridRowSelectionCallback,
} = useRowSelection(params);
const {
hasPresetsChange,
onPresetChange,
onPresetDelete,
onPresetSave,
onPresetUpdate,
presetIdToDelete,
selectedPreset,
presets,
setPresetIdToDelete,
} = useFilterPresets({
getUrl: saleListUrl,
params,
storageUtils,
reset: clearRowSelection,
});
const [changeFilters, resetFilters, handleSearchChange] =
createFilterHandlers({
cleanupFn: reset,
cleanupFn: clearRowSelection,
createUrl: saleListUrl,
getFilterQueryParam,
navigate,
params,
keepActiveTab: true,
});
useEffect(() => {
if (!canBeSorted(params.sort, !!selectedChannel)) {
if (!canBeSorted(params?.sort, !!selectedChannel)) {
navigate(
saleListUrl({
...params,
@ -119,43 +133,20 @@ export const SaleList: React.FC<SaleListProps> = ({ params }) => {
}
}, [params]);
const handleTabChange = (tab: number) => {
reset();
navigate(
saleListUrl({
activeTab: tab.toString(),
...getFilterTabs()[tab - 1].data,
}),
);
};
const handleTabDelete = () => {
deleteFilterTab(currentTab);
reset();
navigate(saleListUrl());
};
const handleTabSave = (data: SaveFilterTabDialogFormData) => {
saveFilterTab(data.name, getActiveFilters(params));
handleTabChange(tabs.length + 1);
};
const canOpenBulkActionDialog = maybe(() => params.ids.length > 0);
const paginationValues = usePaginator({
pageInfo: maybe(() => data.sales.pageInfo),
pageInfo: data?.sales?.pageInfo,
paginationState,
queryString: params,
});
const [saleBulkDelete, saleBulkDeleteOpts] = useSaleBulkDeleteMutation({
onCompleted: data => {
if (data.saleBulkDelete.errors.length === 0) {
if (data?.saleBulkDelete?.errors?.length === 0) {
notify({
status: "success",
text: intl.formatMessage(commonMessages.savedChanges),
});
reset();
clearRowSelection();
closeModal();
refetch();
}
@ -164,57 +155,82 @@ export const SaleList: React.FC<SaleListProps> = ({ params }) => {
const handleSort = createSortHandler(navigate, saleListUrl, params);
const onSaleBulkDelete = () =>
saleBulkDelete({
const handleSelectSaleIds = useCallback(
(rows: number[], clearSelection: () => void) => {
if (!sales) {
return;
}
const rowsIds = rows.map(row => sales[row].id);
const haveSaveValues = isEqual(rowsIds, selectedRowIds);
if (!haveSaveValues) {
setSelectedRowIds(rowsIds);
}
setClearDatagridRowSelectionCallback(clearSelection);
},
[
sales,
selectedRowIds,
setClearDatagridRowSelectionCallback,
setSelectedRowIds,
],
);
const getFilterPresetDeleteName = () => {
if (!presetIdToDelete || !presets[presetIdToDelete - 1]) {
return "...";
}
return presets[presetIdToDelete - 1].name;
};
const onSaleBulkDelete = async () => {
await saleBulkDelete({
variables: {
ids: params.ids,
ids: selectedRowIds,
},
});
clearRowSelection();
};
return (
<PaginatorContext.Provider value={paginationValues}>
<WindowTitle title={intl.formatMessage(commonMessages.discounts)} />
<SaleListPage
currentTab={currentTab}
currencySymbol={selectedChannel?.currencyCode}
onSelectSaleIds={handleSelectSaleIds}
filterOpts={getFilterOpts(params, channelOpts)}
initialSearch={params.query || ""}
onSearchChange={handleSearchChange}
onFilterChange={filter => changeFilters(filter)}
onAll={resetFilters}
onTabChange={handleTabChange}
onTabDelete={() => openModal("delete-search")}
onTabSave={() => openModal("save-search")}
tabs={tabs.map(tab => tab.name)}
sales={mapEdgesToItems(data?.sales)}
onFilterPresetDelete={(id: number) => {
setPresetIdToDelete(id);
openModal("delete-search");
}}
onFilterPresetPresetSave={() => openModal("save-search")}
onFilterPresetChange={onPresetChange}
onFilterPresetUpdate={onPresetUpdate}
onFilterPresetsAll={resetFilters}
filterPresets={presets.map(preset => preset.name)}
selectedFilterPreset={selectedPreset}
hasPresetsChanged={hasPresetsChange}
onSalesDelete={() => openModal("remove")}
selectedSaleIds={selectedRowIds}
sales={sales}
settings={settings}
disabled={loading}
onSort={handleSort}
onUpdateListSettings={updateListSettings}
isChecked={isSelected}
selected={listElements.length}
sort={getSortParams(params)}
toggle={toggle}
toggleAll={toggleAll}
toolbar={
<IconButton
variant="secondary"
color="primary"
onClick={() =>
openModal("remove", {
ids: listElements,
})
}
>
<DeleteIcon />
</IconButton>
}
selectedChannelId={selectedChannel?.id}
selectedChannelId={selectedChannel?.id ?? ""}
/>
<ActionDialog
confirmButtonState={saleBulkDeleteOpts.status}
onClose={closeModal}
onConfirm={onSaleBulkDelete}
open={params.action === "remove" && canOpenBulkActionDialog}
open={params.action === "remove" && selectedRowIds.length > 0}
title={intl.formatMessage({
id: "ZWIjvr",
defaultMessage: "Delete Sales",
@ -222,32 +238,30 @@ export const SaleList: React.FC<SaleListProps> = ({ params }) => {
})}
variant="delete"
>
{canOpenBulkActionDialog && (
<DialogContentText>
<FormattedMessage
id="FPzzh7"
defaultMessage="{counter,plural,one{Are you sure you want to delete this sale?} other{Are you sure you want to delete {displayQuantity} sales?}}"
description="dialog content"
values={{
counter: params.ids.length,
displayQuantity: <strong>{params.ids.length}</strong>,
}}
/>
</DialogContentText>
)}
<DialogContentText>
<FormattedMessage
id="FPzzh7"
defaultMessage="{counter,plural,one{Are you sure you want to delete this sale?} other{Are you sure you want to delete {displayQuantity} sales?}}"
description="dialog content"
values={{
counter: selectedRowIds.length,
displayQuantity: <strong>{selectedRowIds.length}</strong>,
}}
/>
</DialogContentText>
</ActionDialog>
<SaveFilterTabDialog
open={params.action === "save-search"}
confirmButtonState="default"
onClose={closeModal}
onSubmit={handleTabSave}
onSubmit={onPresetSave}
/>
<DeleteFilterTabDialog
open={params.action === "delete-search"}
confirmButtonState="default"
onClose={closeModal}
onSubmit={handleTabDelete}
tabName={maybe(() => tabs[currentTab - 1].name, "...")}
onSubmit={onPresetDelete}
tabName={getFilterPresetDeleteName()}
/>
</PaginatorContext.Provider>
);

View file

@ -121,8 +121,7 @@ export function getFilterQueryParam(
}
}
export const { deleteFilterTab, getFilterTabs, saveFilterTab } =
createFilterTabUtils<SaleListUrlFilters>(SALE_FILTERS_KEY);
export const storageUtils = createFilterTabUtils<string>(SALE_FILTERS_KEY);
export const { areFiltersApplied, getActiveFilters, getFiltersCurrentTab } =
createFilterUtils<SaleListUrlQueryParams, SaleListUrlFilters>({

View file

@ -6,9 +6,13 @@ import { createGetSortQueryVariables } from "@dashboard/utils/sort";
export const DEFAULT_SORT_KEY = SaleListUrlSortField.name;
export function canBeSorted(
sort: SaleListUrlSortField,
sort: SaleListUrlSortField | undefined,
isChannelSelected: boolean,
) {
if (sort === undefined) {
return false;
}
switch (sort) {
case SaleListUrlSortField.name:
case SaleListUrlSortField.startDate:

View file

@ -314,6 +314,7 @@ export const filterPresetsProps: FilterPresetsProps = {
onFilterPresetPresetSave: () => undefined,
onFilterPresetUpdate: () => undefined,
filterPresets: ["Tab X"],
hasPresetsChanged: () => false,
};
export const paginatorContextValues: PaginatorContextValues = {

View file

@ -9,10 +9,8 @@ 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"> {
export interface OrderDraftListHeaderProps extends FilterPresetsProps {
limits: RefreshLimitsQuery["shop"]["limits"];
hasPresetsChanged: boolean;
isFilterPresetOpen: boolean;
disabled: boolean;
onAdd: () => void;
@ -55,7 +53,7 @@ export const OrderDraftListHeader = ({
</Box>
<FilterPresetsSelect
presetsChanged={hasPresetsChanged}
presetsChanged={hasPresetsChanged()}
onSelect={onFilterPresetChange}
onRemove={onFilterPresetDelete}
onUpdate={onFilterPresetUpdate}

View file

@ -69,7 +69,7 @@ const OrderDraftListPage: React.FC<OrderDraftListPageProps> = ({
<OrderDraftListHeader
disabled={disabled}
selectedFilterPreset={selectedFilterPreset}
hasPresetsChanged={hasPresetsChanged()}
hasPresetsChanged={hasPresetsChanged}
isFilterPresetOpen={isFilterPresetOpen}
setFilterPresetOpen={setFilterPresetOpen}
limits={limits}

View file

@ -123,13 +123,14 @@ export interface FilterProps<TKeys extends string> {
}
export interface FilterPresetsProps {
selectedFilterPreset: number;
selectedFilterPreset: number | undefined;
filterPresets: string[];
onFilterPresetsAll: () => void;
onFilterPresetChange: (id: number) => void;
onFilterPresetUpdate: (name: string) => void;
onFilterPresetDelete: (id: number) => void;
onFilterPresetPresetSave: () => void;
hasPresetsChanged: () => boolean;
}
export interface TabPageProps {