diff --git a/.changeset/polite-trainers-fix.md b/.changeset/polite-trainers-fix.md new file mode 100644 index 000000000..ad51dc5e6 --- /dev/null +++ b/.changeset/polite-trainers-fix.md @@ -0,0 +1,5 @@ +--- +"saleor-dashboard": minor +--- + +Introduct datagrid on discounts list page diff --git a/cypress/e2e/discounts/sales/createSalesForProducts.js b/cypress/e2e/discounts/sales/createSalesForProducts.js index b2f54f744..392612407 100644 --- a/cypress/e2e/discounts/sales/createSalesForProducts.js +++ b/cypress/e2e/discounts/sales/createSalesForProducts.js @@ -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, diff --git a/cypress/support/pages/discounts/salesPage.js b/cypress/support/pages/discounts/salesPage.js index 208a778b2..632ac2411 100644 --- a/cypress/support/pages/discounts/salesPage.js +++ b/cypress/support/pages/discounts/salesPage.js @@ -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, diff --git a/locale/defaultMessages.json b/locale/defaultMessages.json index 6330ef401..f0f506965 100644 --- a/locale/defaultMessages.json +++ b/locale/defaultMessages.json @@ -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 won’t be able to assign them to created pages." diff --git a/src/components/AppLayout/ListFilters/ListFilters.tsx b/src/components/AppLayout/ListFilters/ListFilters.tsx index 759f27cf7..cf5111f9c 100644 --- a/src/components/AppLayout/ListFilters/ListFilters.tsx +++ b/src/components/AppLayout/ListFilters/ListFilters.tsx @@ -17,7 +17,7 @@ export interface ListFiltersProps actions?: ReactNode; } -export const ListFilters = ({ +export const ListFilters = ({ currencySymbol, filterStructure, initialSearch, @@ -27,7 +27,7 @@ export const ListFilters = ({ onFilterAttributeFocus, errorMessages, actions, -}: ListFiltersProps) => { +}: ListFiltersProps) => { 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 ? ( ) : ( - errorMessages={errorMessages} menu={filterStructure} currencySymbol={currencySymbol} diff --git a/src/components/AppLayout/ListFilters/components/FiltersSelect.tsx b/src/components/AppLayout/ListFilters/components/FiltersSelect.tsx index 568b973c3..92c9c28d4 100644 --- a/src/components/AppLayout/ListFilters/components/FiltersSelect.tsx +++ b/src/components/AppLayout/ListFilters/components/FiltersSelect.tsx @@ -19,17 +19,17 @@ export interface FilterProps { currencySymbol?: string; errorMessages?: FilterErrorMessages; menu: IFilter; - onFilterAdd: (filter: Array>) => void; + onFilterAdd: (filter: Array>) => void; onFilterAttributeFocus?: (id?: string) => void; } -export const FiltersSelect = ({ +export const FiltersSelect = ({ currencySymbol, menu, onFilterAdd, onFilterAttributeFocus, errorMessages, -}: FilterProps) => { +}: FilterProps) => { const anchor = React.useRef(); const [isFilterMenuOpened, setFilterMenuOpened] = useState(false); const [filterErrors, setFilterErrors] = useState>({}); diff --git a/src/components/Datagrid/const.ts b/src/components/Datagrid/const.ts new file mode 100644 index 000000000..2f5f117bb --- /dev/null +++ b/src/components/Datagrid/const.ts @@ -0,0 +1 @@ +export const PLACEHOLDER = "-"; diff --git a/src/components/Percent/Percent.tsx b/src/components/Percent/Percent.tsx index b1ff6e982..1070a87a3 100644 --- a/src/components/Percent/Percent.tsx +++ b/src/components/Percent/Percent.tsx @@ -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 = ({ amount }) => ( - {({ locale }) => - amount - ? (amount / 100).toLocaleString(locale, { - maximumFractionDigits: 2, - style: "percent", - }) - : "-" - } + {({ locale }) => formatPercantage(amount, locale)} ); Percent.displayName = "Percent"; diff --git a/src/components/Percent/utils.test.ts b/src/components/Percent/utils.test.ts new file mode 100644 index 000000000..24ecd4bb7 --- /dev/null +++ b/src/components/Percent/utils.test.ts @@ -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%"); + }); +}); diff --git a/src/components/Percent/utils.ts b/src/components/Percent/utils.ts new file mode 100644 index 000000000..af88bd90a --- /dev/null +++ b/src/components/Percent/utils.ts @@ -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", + }) + : "-"; +}; diff --git a/src/config.ts b/src/config.ts index ea02ad4b7..f811f9026 100644 --- a/src/config.ts +++ b/src/config.ts @@ -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, diff --git a/src/discounts/components/SaleList/SaleList.tsx b/src/discounts/components/SaleList/SaleList.tsx deleted file mode 100644 index b6ab9faa5..000000000 --- a/src/discounts/components/SaleList/SaleList.tsx +++ /dev/null @@ -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, - 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 = 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 ( - - - onSort(SaleListUrlSortField.name)} - className={classes.colName} - > - - - onSort(SaleListUrlSortField.startDate)} - className={classes.colStart} - > - - - onSort(SaleListUrlSortField.endDate)} - className={classes.colEnd} - > - - - onSort(SaleListUrlSortField.value)} - disabled={ - !canBeSorted(SaleListUrlSortField.value, !!selectedChannelId) - } - tooltip={intl.formatMessage(commonTooltipMessages.noFilterSelected, { - filterName: filterDependency.label, - })} - className={classes.colValue} - > - - - - - - - - - - {renderCollection( - sales, - sale => { - const isSelected = sale ? isChecked(sale.id) : false; - const channel = sale?.channelListings?.find( - lisiting => lisiting.channel.id === selectedChannelId, - ); - return ( - - - toggle(sale.id)} - /> - - - {maybe(() => sale.name, )} - - - {sale && sale.startDate ? ( - - ) : ( - - )} - - - {sale && sale.endDate ? ( - - ) : sale && sale.endDate === null ? ( - "-" - ) : ( - - )} - - - {sale?.type && channel?.discountValue ? ( - sale.type === SaleType.FIXED ? ( - - ) : channel?.discountValue ? ( - - ) : ( - "-" - ) - ) : sale && !channel ? ( - "_" - ) : ( - - )} - - - ); - }, - () => ( - - - - - - ), - )} - - - ); -}; -SaleList.displayName = "SaleList"; -export default SaleList; diff --git a/src/discounts/components/SaleList/index.ts b/src/discounts/components/SaleList/index.ts deleted file mode 100644 index 20d8fef01..000000000 --- a/src/discounts/components/SaleList/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from "./SaleList"; -export * from "./SaleList"; diff --git a/src/discounts/components/SaleListDatagrid/SaleListDatagrid.tsx b/src/discounts/components/SaleListDatagrid/SaleListDatagrid.tsx new file mode 100644 index 000000000..264182baf --- /dev/null +++ b/src/discounts/components/SaleListDatagrid/SaleListDatagrid.tsx @@ -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, + 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 ( + + 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={() => ( + + )} + /> + + + + + + ); +}; diff --git a/src/discounts/components/SaleListDatagrid/datagrid.ts b/src/discounts/components/SaleListDatagrid/datagrid.ts new file mode 100644 index 000000000..101763717 --- /dev/null +++ b/src/discounts/components/SaleListDatagrid/datagrid.ts @@ -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, +) => + [ + { + 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(""); + } + }; diff --git a/src/discounts/components/SaleListDatagrid/index.tsx b/src/discounts/components/SaleListDatagrid/index.tsx new file mode 100644 index 000000000..788a5e5c9 --- /dev/null +++ b/src/discounts/components/SaleListDatagrid/index.tsx @@ -0,0 +1 @@ +export * from "./SaleListDatagrid"; diff --git a/src/discounts/components/SaleListDatagrid/messages.ts b/src/discounts/components/SaleListDatagrid/messages.ts new file mode 100644 index 000000000..0d7f6567e --- /dev/null +++ b/src/discounts/components/SaleListDatagrid/messages.ts @@ -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", + }, +}); diff --git a/src/discounts/components/SaleListPage/SaleListPage.stories.tsx b/src/discounts/components/SaleListPage/SaleListPage.stories.tsx index 1a386057c..738631fca 100644 --- a/src/discounts/components/SaleListPage/SaleListPage.stories.tsx +++ b/src/discounts/components/SaleListPage/SaleListPage.stories.tsx @@ -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 }, diff --git a/src/discounts/components/SaleListPage/SaleListPage.tsx b/src/discounts/components/SaleListPage/SaleListPage.tsx index 2cfeb37cf..8ad9c6932 100644 --- a/src/discounts/components/SaleListPage/SaleListPage.tsx +++ b/src/discounts/components/SaleListPage/SaleListPage.tsx @@ -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, + FilterPagePropsWithPresets, SortPage, - TabPageProps, ChannelProps { sales: SaleFragment[]; + selectedSaleIds: string[]; + onSalesDelete: () => void; + onSelectSaleIds: (rows: number[], clearSelection: () => void) => void; } const SaleListPage: React.FC = ({ - 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 ( - - + + + + + + + + + + + + - + 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={ + + {selectedSaleIds.length > 0 && ( + + + + )} + + } + /> + + - ); diff --git a/src/discounts/urls.ts b/src/discounts/urls.ts index 3ec335936..87dc61754 100644 --- a/src/discounts/urls.ts +++ b/src/discounts/urls.ts @@ -32,8 +32,8 @@ export type SaleListUrlFilters = Filters & export type SaleListUrlDialog = "remove" | TabActionDialog; export enum SaleListUrlSortField { name = "name", - endDate = "end-date", - startDate = "start-date", + endDate = "endDate", + startDate = "startDate", type = "type", value = "value", } diff --git a/src/discounts/views/SaleList/SaleList.tsx b/src/discounts/views/SaleList/SaleList.tsx index d14bfb1b2..daff6e7fb 100644 --- a/src/discounts/views/SaleList/SaleList.tsx +++ b/src/discounts/views/SaleList/SaleList.tsx @@ -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 = ({ 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 = ({ params }) => { ); const channelOpts = availableChannels ? mapNodeToChoice(availableChannels, channel => channel.slug) - : null; + : []; const [openModal, closeModal] = createDialogActionHandlers< SaleListUrlDialog, @@ -95,21 +86,44 @@ export const SaleList: React.FC = ({ 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 = ({ 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 = ({ 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 ( 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={ - - openModal("remove", { - ids: listElements, - }) - } - > - - - } - selectedChannelId={selectedChannel?.id} + selectedChannelId={selectedChannel?.id ?? ""} /> 0} title={intl.formatMessage({ id: "ZWIjvr", defaultMessage: "Delete Sales", @@ -222,32 +238,30 @@ export const SaleList: React.FC = ({ params }) => { })} variant="delete" > - {canOpenBulkActionDialog && ( - - {params.ids.length}, - }} - /> - - )} + + {selectedRowIds.length}, + }} + /> + tabs[currentTab - 1].name, "...")} + onSubmit={onPresetDelete} + tabName={getFilterPresetDeleteName()} /> ); diff --git a/src/discounts/views/SaleList/filters.ts b/src/discounts/views/SaleList/filters.ts index 92766fc82..fa185ef91 100644 --- a/src/discounts/views/SaleList/filters.ts +++ b/src/discounts/views/SaleList/filters.ts @@ -121,8 +121,7 @@ export function getFilterQueryParam( } } -export const { deleteFilterTab, getFilterTabs, saveFilterTab } = - createFilterTabUtils(SALE_FILTERS_KEY); +export const storageUtils = createFilterTabUtils(SALE_FILTERS_KEY); export const { areFiltersApplied, getActiveFilters, getFiltersCurrentTab } = createFilterUtils({ diff --git a/src/discounts/views/SaleList/sort.ts b/src/discounts/views/SaleList/sort.ts index ed40356d8..9c3cc365f 100644 --- a/src/discounts/views/SaleList/sort.ts +++ b/src/discounts/views/SaleList/sort.ts @@ -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: diff --git a/src/fixtures.ts b/src/fixtures.ts index c89f3323b..2658a77ee 100644 --- a/src/fixtures.ts +++ b/src/fixtures.ts @@ -314,6 +314,7 @@ export const filterPresetsProps: FilterPresetsProps = { onFilterPresetPresetSave: () => undefined, onFilterPresetUpdate: () => undefined, filterPresets: ["Tab X"], + hasPresetsChanged: () => false, }; export const paginatorContextValues: PaginatorContextValues = { diff --git a/src/orders/components/OrderDraftListHeader/OrderDraftListHeader.tsx b/src/orders/components/OrderDraftListHeader/OrderDraftListHeader.tsx index 2afbe1707..b748b68c0 100644 --- a/src/orders/components/OrderDraftListHeader/OrderDraftListHeader.tsx +++ b/src/orders/components/OrderDraftListHeader/OrderDraftListHeader.tsx @@ -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 { +export interface OrderDraftListHeaderProps extends FilterPresetsProps { limits: RefreshLimitsQuery["shop"]["limits"]; - hasPresetsChanged: boolean; isFilterPresetOpen: boolean; disabled: boolean; onAdd: () => void; @@ -55,7 +53,7 @@ export const OrderDraftListHeader = ({ = ({ { } 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 {