diff --git a/.changeset/gold-starfishes-trade.md b/.changeset/gold-starfishes-trade.md new file mode 100644 index 000000000..71708e27d --- /dev/null +++ b/.changeset/gold-starfishes-trade.md @@ -0,0 +1,5 @@ +--- +"saleor-dashboard": minor +--- + +Introduce datagrid on customer list view diff --git a/locale/defaultMessages.json b/locale/defaultMessages.json index feb33a7ab..6869284cd 100644 --- a/locale/defaultMessages.json +++ b/locale/defaultMessages.json @@ -691,9 +691,6 @@ "context": "section header button", "string": "Manage" }, - "2mRLis": { - "string": "Search Customer" - }, "2ob30/": { "string": "Success! In a few minutes you’ll receive a message with instructions on how to reset your password." }, @@ -1474,6 +1471,10 @@ "context": "button", "string": "Back to homepage" }, + "945a4a": { + "context": "column header", + "string": "Customer e-mail" + }, "94oZR0": { "context": "deactivate app billing info", "string": "You will be still billed for the app." @@ -1482,9 +1483,6 @@ "context": "button", "string": "Go back to dashboard" }, - "97l2MO": { - "string": "Customer Email" - }, "98Nw4g": { "context": "card subtitle", "string": "Rendered prices" @@ -2053,6 +2051,10 @@ "context": "no warehouses info", "string": "There are no warehouses set up for your store. To add stock quantity to the variant please configure a warehouse" }, + "D95l71": { + "context": "tab name", + "string": "All customers" + }, "D9Rg+F": { "context": "window title", "string": "Channel details" @@ -2173,9 +2175,6 @@ "E8T3e+": { "string": "Cannot add and remove group the same time" }, - "E8VDeH": { - "string": "No. of Orders" - }, "E9Dz18": { "context": "Order summary refunds header", "string": "Refunds" @@ -2609,9 +2608,6 @@ "context": "shipping method description", "string": "Description" }, - "Gr1SAu": { - "string": "Customer Name" - }, "GsBRWL": { "string": "Languages" }, @@ -3371,6 +3367,10 @@ "MSItJD": { "string": "You are about to leave the Dashboard. Do you want to continue?" }, + "MTGT8E": { + "context": "column header", + "string": "No. of orders" + }, "MTl5o6": { "context": "new discount label", "string": "New discount value" @@ -6631,6 +6631,9 @@ "kFkPWB": { "string": "Number" }, + "kFsTMN": { + "string": "Delete customers" + }, "kIvvax": { "string": "Search Products..." }, @@ -6709,6 +6712,9 @@ "context": "balance amound missing error message", "string": "Balance amount is missing" }, + "kdRcqU": { + "string": "Search customers..." + }, "kgVqk1": { "string": "Category name" }, @@ -7094,6 +7100,10 @@ "context": "window title", "string": "Create customer" }, + "nZDQbr": { + "context": "column header", + "string": "Customer name" + }, "nayZY0": { "context": "returned event title", "string": "Products were returned by" @@ -8392,10 +8402,6 @@ "context": "title", "string": "Details" }, - "xQK2EC": { - "context": "tab name", - "string": "All Customers" - }, "xRbqcg": { "context": "option", "string": "Regular product type" diff --git a/src/categories/views/CategoryList/CategoryList.tsx b/src/categories/views/CategoryList/CategoryList.tsx index edef04895..abf3330ee 100644 --- a/src/categories/views/CategoryList/CategoryList.tsx +++ b/src/categories/views/CategoryList/CategoryList.tsx @@ -59,7 +59,7 @@ export const CategoryList: React.FC = ({ params }) => { } = useRowSelection(params); const { - hasPresetsChange, + hasPresetsChanged, onPresetChange, onPresetDelete, onPresetSave, @@ -164,7 +164,7 @@ export const CategoryList: React.FC = ({ params }) => { return ( = ({ params }) => { const { selectedPreset, presets, - hasPresetsChange, + hasPresetsChanged, onPresetChange, onPresetDelete, onPresetSave, @@ -219,7 +219,7 @@ export const CollectionList: React.FC = ({ params }) => { onFilterChange={changeFilters} selectedCollectionIds={selectedRowIds} onSelectCollectionIds={handleSetSelectedCollectionIds} - hasPresetsChanged={hasPresetsChange} + hasPresetsChanged={hasPresetsChanged} onCollectionsDelete={() => openModal("remove", { ids: selectedRowIds, diff --git a/src/components/TableFilter/FilterTabs.tsx b/src/components/TableFilter/FilterTabs.tsx index 363bf4165..5464265c4 100644 --- a/src/components/TableFilter/FilterTabs.tsx +++ b/src/components/TableFilter/FilterTabs.tsx @@ -14,7 +14,7 @@ const useStyles = makeStyles( interface FilterTabsProps { children?: React.ReactNode; - currentTab: number; + currentTab: number | undefined; } export const FilterTabs: React.FC = props => { diff --git a/src/config.ts b/src/config.ts index f3df67507..77a313745 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,4 +1,3 @@ -// @ts-strict-ignore import packageInfo from "../package.json"; import { SearchVariables } from "./hooks/makeSearch"; import { ListSettings, ListViews, Pagination } from "./types"; @@ -7,7 +6,7 @@ export const getAppDefaultUri = () => "/"; export const getAppMountUri = () => window?.__SALEOR_CONFIG__?.APP_MOUNT_URI || getAppDefaultUri(); export const getApiUrl = () => window.__SALEOR_CONFIG__.API_URL; -export const SW_INTERVAL = parseInt(process.env.SW_INTERVAL, 10) || 300; +export const SW_INTERVAL = parseInt(process.env.SW_INTERVAL ?? "300", 10); export const IS_CLOUD_INSTANCE = window.__SALEOR_CONFIG__.IS_CLOUD_INSTANCE === "true"; @@ -80,6 +79,7 @@ export const defaultListSettings: AppListViewSettings = { }, [ListViews.CUSTOMER_LIST]: { rowNumber: PAGINATE_BY, + columns: ["name", "email", "orders"], }, [ListViews.DRAFT_LIST]: { rowNumber: PAGINATE_BY, diff --git a/src/customers/components/CustomerList/CustomerList.tsx b/src/customers/components/CustomerList/CustomerList.tsx deleted file mode 100644 index 2d069eeec..000000000 --- a/src/customers/components/CustomerList/CustomerList.tsx +++ /dev/null @@ -1,199 +0,0 @@ -// @ts-strict-ignore -import { useUserPermissions } from "@dashboard/auth/hooks/useUserPermissions"; -import Checkbox from "@dashboard/components/Checkbox"; -import RequirePermissions, { - hasPermissions, -} from "@dashboard/components/RequirePermissions"; -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 { - CustomerListUrlSortField, - customerUrl, -} from "@dashboard/customers/urls"; -import { ListCustomersQuery, PermissionEnum } from "@dashboard/graphql"; -import { getUserName, renderCollection } from "@dashboard/misc"; -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 } from "react-intl"; - -const useStyles = makeStyles( - theme => ({ - [theme.breakpoints.up("lg")]: { - colEmail: {}, - colName: {}, - colOrders: { - width: 200, - }, - }, - colEmail: {}, - colName: { - paddingLeft: 0, - }, - colOrders: { - textAlign: "center", - }, - tableRow: { - cursor: "pointer", - }, - }), - { name: "CustomerList" }, -); - -export interface CustomerListProps - extends ListProps, - ListActions, - SortPage { - customers: RelayToFlat; -} - -const CustomerList: React.FC = props => { - const { - settings, - disabled, - customers, - onUpdateListSettings, - onSort, - toolbar, - toggle, - toggleAll, - selected, - sort, - isChecked, - } = props; - - const userPermissions = useUserPermissions(); - - const numberOfColumns = hasPermissions(userPermissions, [ - PermissionEnum.MANAGE_ORDERS, - ]) - ? 4 - : 3; - - const classes = useStyles(props); - - return ( - - - onSort(CustomerListUrlSortField.name)} - className={classes.colName} - > - - - onSort(CustomerListUrlSortField.email)} - className={classes.colEmail} - > - - - - onSort(CustomerListUrlSortField.orders)} - className={classes.colOrders} - > - - - - - - - - - - - {renderCollection( - customers, - customer => { - const isSelected = customer ? isChecked(customer.id) : false; - - return ( - - - toggle(customer.id)} - /> - - - {getUserName(customer)} - - - {customer?.email ?? } - - - - {customer?.orders?.totalCount ?? } - - - - ); - }, - () => ( - - - - - - ), - )} - - - ); -}; -CustomerList.displayName = "CustomerList"; -export default CustomerList; diff --git a/src/customers/components/CustomerList/index.ts b/src/customers/components/CustomerList/index.ts deleted file mode 100644 index c5517efd0..000000000 --- a/src/customers/components/CustomerList/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from "./CustomerList"; -export * from "./CustomerList"; diff --git a/src/customers/components/CustomerListDatagrid/CustomerListDatagrid.tsx b/src/customers/components/CustomerListDatagrid/CustomerListDatagrid.tsx new file mode 100644 index 000000000..8fd7a4555 --- /dev/null +++ b/src/customers/components/CustomerListDatagrid/CustomerListDatagrid.tsx @@ -0,0 +1,168 @@ +import { useUserPermissions } from "@dashboard/auth/hooks/useUserPermissions"; +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 { Customer, Customers } from "@dashboard/customers/types"; +import { CustomerListUrlSortField } from "@dashboard/customers/urls"; +import { PermissionEnum } from "@dashboard/graphql"; +import { 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 { + createGetCellContent, + customerListStaticColumnsAdapter, +} from "./datagrid"; +import { messages } from "./messages"; + +interface CustomerListDatagridProps + extends ListProps, + SortPage { + customers: Customers | undefined; + loading: boolean; + hasRowHover?: boolean; + onSelectCustomerIds: ( + rowsIndex: number[], + clearSelection: () => void, + ) => void; + onRowClick: (id: string) => void; + rowAnchor?: (id: string) => string; +} + +export const CustomerListDatagrid = ({ + customers, + sort, + loading, + settings, + onUpdateListSettings, + hasRowHover, + onRowClick, + rowAnchor, + disabled, + onSelectCustomerIds, + onSort, +}: CustomerListDatagridProps) => { + const intl = useIntl(); + const datagrid = useDatagridChangeState(); + + const userPermissions = useUserPermissions(); + const hasManageOrdersPermission = + userPermissions?.some(perm => perm.code === PermissionEnum.MANAGE_ORDERS) ?? + false; + + const customerListStaticColumns = useMemo( + () => + customerListStaticColumnsAdapter(intl, sort, hasManageOrdersPermission), + [intl, sort, hasManageOrdersPermission], + ); + + const onColumnChange = useCallback( + (picked: string[]) => { + if (onUpdateListSettings) { + onUpdateListSettings("columns", picked.filter(Boolean)); + } + }, + [onUpdateListSettings], + ); + + const { + handlers, + visibleColumns, + staticColumns, + selectedColumns, + recentlyAddedColumn, + } = useColumns({ + staticColumns: customerListStaticColumns, + selectedColumns: settings?.columns ?? [], + onSave: onColumnChange, + }); + + const getCellContent = useCallback( + createGetCellContent({ + customers, + columns: visibleColumns, + }), + [customers, visibleColumns], + ); + + const handleRowClick = useCallback( + ([_, row]: Item) => { + if (!onRowClick || !customers) { + return; + } + const rowData: Customer = customers[row]; + onRowClick(rowData.id); + }, + [onRowClick, customers], + ); + + const handleRowAnchor = useCallback( + ([, row]: Item) => { + if (!rowAnchor || !customers) { + return ""; + } + const rowData: Customer = customers[row]; + return rowAnchor(rowData.id); + }, + [rowAnchor, customers], + ); + + const handleHeaderClick = useCallback( + (col: number) => { + const columnName = visibleColumns[col].id as CustomerListUrlSortField; + + onSort(columnName); + }, + [visibleColumns, onSort], + ); + + return ( + + col > 0} + rows={customers?.length ?? 0} + availableColumns={visibleColumns} + emptyText={intl.formatMessage(messages.empty)} + onRowSelectionChange={onSelectCustomerIds} + getCellContent={getCellContent} + getCellError={() => false} + selectionActions={() => null} + menuItems={() => []} + onRowClick={handleRowClick} + onHeaderClicked={handleHeaderClick} + rowAnchor={handleRowAnchor} + recentlyAddedColumn={recentlyAddedColumn} + renderColumnPicker={() => ( + + )} + /> + + + + + + ); +}; diff --git a/src/customers/components/CustomerListDatagrid/datagrid.ts b/src/customers/components/CustomerListDatagrid/datagrid.ts new file mode 100644 index 000000000..088370fa2 --- /dev/null +++ b/src/customers/components/CustomerListDatagrid/datagrid.ts @@ -0,0 +1,69 @@ +import { readonlyTextCell } from "@dashboard/components/Datagrid/customCells/cells"; +import { AvailableColumn } from "@dashboard/components/Datagrid/types"; +import { Customers } from "@dashboard/customers/types"; +import { CustomerListUrlSortField } from "@dashboard/customers/urls"; +import { getUserName } from "@dashboard/misc"; +import { Sort } from "@dashboard/types"; +import { getColumnSortDirectionIcon } from "@dashboard/utils/columns/getColumnSortDirectionIcon"; +import { GridCell, Item } from "@glideapps/glide-data-grid"; +import { IntlShape } from "react-intl"; + +import { columnsMessages } from "./messages"; + +export const customerListStaticColumnsAdapter = ( + intl: IntlShape, + sort: Sort, + includeOrders: boolean, +): AvailableColumn[] => + [ + { + id: "name", + title: intl.formatMessage(columnsMessages.name), + width: 450, + }, + { + id: "email", + title: intl.formatMessage(columnsMessages.email), + width: 450, + }, + ...(includeOrders + ? [ + { + id: "orders", + title: intl.formatMessage(columnsMessages.orders), + width: 200, + }, + ] + : []), + ].map(column => ({ + ...column, + icon: getColumnSortDirectionIcon(sort, column.id), + })); + +export const createGetCellContent = + ({ + customers, + columns, + }: { + customers: Customers | undefined; + columns: AvailableColumn[]; + }) => + ([column, row]: Item): GridCell => { + const rowData = customers?.[row]; + const columnId = columns[column]?.id; + + if (!columnId || !rowData) { + return readonlyTextCell(""); + } + + switch (columnId) { + case "name": + return readonlyTextCell(getUserName(rowData) ?? ""); + case "email": + return readonlyTextCell(rowData?.email ?? ""); + case "orders": + return readonlyTextCell(rowData?.orders?.totalCount?.toString() ?? ""); + default: + return readonlyTextCell(""); + } + }; diff --git a/src/customers/components/CustomerListDatagrid/messages.ts b/src/customers/components/CustomerListDatagrid/messages.ts new file mode 100644 index 000000000..0ea543a71 --- /dev/null +++ b/src/customers/components/CustomerListDatagrid/messages.ts @@ -0,0 +1,26 @@ +import { defineMessages } from "react-intl"; + +export const messages = defineMessages({ + empty: { + id: "FpIcp9", + defaultMessage: "No customers found", + }, +}); + +export const columnsMessages = defineMessages({ + name: { + id: "nZDQbr", + defaultMessage: "Customer name", + description: "column header", + }, + email: { + id: "945a4a", + defaultMessage: "Customer e-mail", + description: "column header", + }, + orders: { + id: "MTGT8E", + defaultMessage: "No. of orders", + description: "column header", + }, +}); diff --git a/src/customers/components/CustomerListPage/CustomerListPage.stories.tsx b/src/customers/components/CustomerListPage/CustomerListPage.stories.tsx index dcc654e15..0b14ef381 100644 --- a/src/customers/components/CustomerListPage/CustomerListPage.stories.tsx +++ b/src/customers/components/CustomerListPage/CustomerListPage.stories.tsx @@ -1,11 +1,11 @@ // @ts-strict-ignore import { filterPageProps, + filterPresetsProps, listActionsProps, pageListProps, searchPageProps, sortPageProps, - tabPageProps, } from "@dashboard/fixtures"; import { Meta, StoryObj } from "@storybook/react"; import React from "react"; @@ -24,7 +24,7 @@ const props: CustomerListPageProps = { ...pageListProps.default, ...searchPageProps, ...sortPageProps, - ...tabPageProps, + ...filterPresetsProps, customers: customerList, selectedCustomerIds: ["123"], filterOpts: { @@ -47,9 +47,13 @@ const props: CustomerListPageProps = { ...sortPageProps.sort, sort: CustomerListUrlSortField.name, }, + loading: false, + hasPresetsChanged: () => false, + onSelectCustomerIds: () => undefined, + onCustomersDelete: () => undefined, }; -const CustomerListPage = props => ( +const CustomerListPage = (props: CustomerListPageProps) => ( diff --git a/src/customers/components/CustomerListPage/CustomerListPage.tsx b/src/customers/components/CustomerListPage/CustomerListPage.tsx index e3758dd08..83c5992e2 100644 --- a/src/customers/components/CustomerListPage/CustomerListPage.tsx +++ b/src/customers/components/CustomerListPage/CustomerListPage.tsx @@ -6,76 +6,69 @@ import { useExtensions, } from "@dashboard/apps/hooks/useExtensions"; import { useUserPermissions } from "@dashboard/auth/hooks/useUserPermissions"; +import { ListFilters } from "@dashboard/components/AppLayout/ListFilters"; import { TopNav } from "@dashboard/components/AppLayout/TopNav"; -import ButtonWithSelect from "@dashboard/components/ButtonWithSelect"; -import CardMenu from "@dashboard/components/CardMenu/CardMenu"; -import FilterBar from "@dashboard/components/FilterBar"; +import { BulkDeleteButton } from "@dashboard/components/BulkDeleteButton"; +import { ButtonWithDropdown } from "@dashboard/components/ButtonWithDropdown"; +import { FilterPresetsSelect } from "@dashboard/components/FilterPresetsSelect"; +import { Customers } from "@dashboard/customers/types"; import { customerAddUrl, CustomerListUrlSortField, + customerUrl, } from "@dashboard/customers/urls"; -import { ListCustomersQuery } from "@dashboard/graphql"; import useNavigator from "@dashboard/hooks/useNavigator"; import { sectionNames } from "@dashboard/intl"; import { - FilterPageProps, - ListActions, + FilterPagePropsWithPresets, PageListProps, - RelayToFlat, SortPage, - TabPageProps, } from "@dashboard/types"; -import { Card } from "@material-ui/core"; -import { makeStyles } from "@saleor/macaw-ui"; -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 CustomerList from "../CustomerList/CustomerList"; +import { CustomerListDatagrid } from "../CustomerListDatagrid/CustomerListDatagrid"; import { createFilterStructure, CustomerFilterKeys, CustomerListFilterOpts, } from "./filters"; -const useStyles = makeStyles( - theme => ({ - settings: { - marginRight: theme.spacing(2), - }, - }), - { name: "CustomerListPage" }, -); - export interface CustomerListPageProps extends PageListProps, - ListActions, - FilterPageProps, - SortPage, - TabPageProps { - customers: RelayToFlat; + FilterPagePropsWithPresets, + SortPage { + customers: Customers | undefined; selectedCustomerIds: string[]; + loading: boolean; + onSelectCustomerIds: (rows: number[], clearSelection: () => void) => void; + onCustomersDelete: () => void; } const CustomerListPage: React.FC = ({ - currentTab, + selectedFilterPreset, filterOpts, initialSearch, - onAll, + onFilterPresetsAll, onFilterChange, + onFilterPresetDelete, + onFilterPresetUpdate, onSearchChange, - onTabChange, - onTabDelete, - onTabSave, - tabs, + onFilterPresetChange, + onFilterPresetPresetSave, + filterPresets, selectedCustomerIds, + hasPresetsChanged, + onCustomersDelete, ...customerListProps }) => { const intl = useIntl(); - const classes = useStyles({}); const navigate = useNavigator(); const userPermissions = useUserPermissions(); const structure = createFilterStructure(intl, filterOpts, userPermissions); + const [isFilterPresetOpen, setFilterPresetOpen] = useState(false); const { CUSTOMER_OVERVIEW_CREATE, CUSTOMER_OVERVIEW_MORE_ACTIONS } = useExtensions(extensionMountPoints.CUSTOMER_LIST); @@ -87,49 +80,100 @@ const CustomerListPage: React.FC = ({ return ( <> - - {extensionMenuItems.length > 0 && ( - - )} - navigate(customerAddUrl)} - options={extensionCreateButtonItems} - data-test-id="create-customer" + + - - + + + + + + + + {extensionMenuItems.length > 0 && ( + + )} + {extensionCreateButtonItems.length > 0 ? ( + navigate(customerAddUrl)} + > + + + ) : ( + + )} + + - - + + {selectedCustomerIds.length > 0 && ( + + + + )} + + } /> - - + navigate(customerUrl(id))} + /> + ); }; diff --git a/src/customers/components/CustomerListPage/filters.ts b/src/customers/components/CustomerListPage/filters.ts index 4c41b686c..2e17f3700 100644 --- a/src/customers/components/CustomerListPage/filters.ts +++ b/src/customers/components/CustomerListPage/filters.ts @@ -54,5 +54,7 @@ export function createFilterStructure( active: opts.numberOfOrders.active, permissions: [PermissionEnum.MANAGE_ORDERS], }, - ].filter(filter => hasPermissions(userPermissions, filter.permissions ?? [])); + ].filter(filter => + hasPermissions(userPermissions ?? [], filter.permissions ?? []), + ); } diff --git a/src/customers/fixtures.ts b/src/customers/fixtures.ts index ce0ab5ef1..8df6efd76 100644 --- a/src/customers/fixtures.ts +++ b/src/customers/fixtures.ts @@ -1,11 +1,10 @@ -// @ts-strict-ignore import { CustomerAddressesQuery, CustomerDetailsQuery, - ListCustomersQuery, PaymentChargeStatusEnum, } from "@dashboard/graphql"; -import { RelayToFlat } from "@dashboard/types"; + +import { Customers } from "./types"; export const customers = [ { @@ -682,7 +681,7 @@ export const customers = [ }, ]; -export const customerList: RelayToFlat = [ +export const customerList: Customers = [ { __typename: "User", email: "Curtis.bailey@example.com", @@ -975,20 +974,20 @@ export const customer: CustomerDetailsQuery["user"] & __typename: "Address", city: "West Feliciamouth", cityArea: "Montana", - companyName: null, + companyName: "", country: { __typename: "CountryDisplay", code: "JA", country: "Japan", }, - countryArea: null, + countryArea: "", firstName: "Timmy", id: "33855", lastName: "Macejkovic", phone: "+41 460-907-9374", postalCode: "15926", streetAddress1: "0238 Cremin Freeway", - streetAddress2: null, + streetAddress2: "", }, ], dateJoined: "2017-05-07T09:37:30.124154+00:00", diff --git a/src/customers/types.ts b/src/customers/types.ts index f7b7e8d55..04c84e36e 100644 --- a/src/customers/types.ts +++ b/src/customers/types.ts @@ -1,3 +1,6 @@ +import { ListCustomersQuery } from "@dashboard/graphql"; +import { RelayToFlat } from "@dashboard/types"; + export interface AddressTypeInput { city: string; cityArea?: string; @@ -29,3 +32,8 @@ export interface AddressType { streetAddress1: string; streetAddress2?: string; } + +export type Customers = RelayToFlat< + NonNullable +>; +export type Customer = Customers[number]; diff --git a/src/customers/views/CustomerList/CustomerList.tsx b/src/customers/views/CustomerList/CustomerList.tsx index f56efea9b..b91eb4cc6 100644 --- a/src/customers/views/CustomerList/CustomerList.tsx +++ b/src/customers/views/CustomerList/CustomerList.tsx @@ -1,15 +1,12 @@ -// @ts-strict-ignore import ActionDialog from "@dashboard/components/ActionDialog"; 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 { useBulkRemoveCustomersMutation, useListCustomersQuery, } 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"; @@ -18,8 +15,8 @@ import usePaginator, { createPaginationState, PaginatorContext, } from "@dashboard/hooks/usePaginator"; +import { useRowSelection } from "@dashboard/hooks/useRowSelection"; import { commonMessages, sectionNames } 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"; @@ -27,8 +24,8 @@ import createSortHandler from "@dashboard/utils/handlers/sortHandler"; import { mapEdgesToItems } 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 from "react"; +import isEqual from "lodash/isEqual"; +import React, { useCallback } from "react"; import { FormattedMessage, useIntl } from "react-intl"; import CustomerListPage from "../../components/CustomerListPage"; @@ -38,14 +35,11 @@ import { CustomerListUrlQueryParams, } from "../../urls"; import { - deleteFilterTab, - getActiveFilters, getFilterOpts, getFilterQueryParam, - getFiltersCurrentTab, - getFilterTabs, getFilterVariables, - saveFilterTab, + getPresetNameToDelete, + storageUtils, } from "./filters"; import { getSortQueryVariables } from "./sort"; @@ -56,16 +50,36 @@ interface CustomerListProps { export const CustomerList: React.FC = ({ params }) => { const navigate = useNavigator(); const notify = useNotifier(); - const { isSelected, listElements, reset, toggle, toggleAll } = useBulkActions( - params.ids, - ); + const intl = useIntl(); const { updateListSettings, settings } = useListSettings( ListViews.CUSTOMER_LIST, ); usePaginationReset(customerListUrl, params, settings.rowNumber); - const intl = useIntl(); + const { + clearRowSelection, + selectedRowIds, + setClearDatagridRowSelectionCallback, + setSelectedRowIds, + } = useRowSelection(params); + + const { + selectedPreset, + presets, + hasPresetsChanged, + onPresetChange, + onPresetDelete, + onPresetSave, + onPresetUpdate, + setPresetIdToDelete, + presetIdToDelete, + } = useFilterPresets({ + params, + reset: clearRowSelection, + getUrl: customerListUrl, + storageUtils, + }); const paginationState = createPaginationState(settings.rowNumber, params); const queryVariables = React.useMemo( @@ -80,18 +94,16 @@ export const CustomerList: React.FC = ({ params }) => { displayLoader: true, variables: queryVariables, }); - - const tabs = getFilterTabs(); - - const currentTab = getFiltersCurrentTab(params, tabs); + const customers = mapEdgesToItems(data?.customers); const [changeFilters, resetFilters, handleSearchChange] = createFilterHandlers({ - cleanupFn: reset, + cleanupFn: clearRowSelection, createUrl: customerListUrl, getFilterQueryParam, navigate, params, + keepActiveTab: true, }); const [openModal, closeModal] = createDialogActionHandlers< @@ -99,29 +111,8 @@ export const CustomerList: React.FC = ({ params }) => { CustomerListUrlQueryParams >(navigate, customerListUrl, params); - const handleTabChange = (tab: number) => { - reset(); - navigate( - customerListUrl({ - activeTab: tab.toString(), - ...getFilterTabs()[tab - 1].data, - }), - ); - }; - - const handleTabDelete = () => { - deleteFilterTab(currentTab); - reset(); - navigate(customerListUrl()); - }; - - const handleTabSave = (data: SaveFilterTabDialogFormData) => { - saveFilterTab(data.name, getActiveFilters(params)); - handleTabChange(tabs.length + 1); - }; - const paginationValues = usePaginator({ - pageInfo: maybe(() => data.customers.pageInfo), + pageInfo: data?.customers?.pageInfo, paginationState, queryString: params, }); @@ -129,13 +120,13 @@ export const CustomerList: React.FC = ({ params }) => { const [bulkRemoveCustomers, bulkRemoveCustomersOpts] = useBulkRemoveCustomersMutation({ onCompleted: data => { - if (data.customerBulkDelete.errors.length === 0) { + if (data.customerBulkDelete?.errors.length === 0) { notify({ status: "success", text: intl.formatMessage(commonMessages.savedChanges), }); - reset(); refetch(); + clearRowSelection(); closeModal(); } }, @@ -143,53 +134,67 @@ export const CustomerList: React.FC = ({ params }) => { const handleSort = createSortHandler(navigate, customerListUrl, params); + const handleSetSelectedCustomerIds = useCallback( + (rows: number[], clearSelection: () => void) => { + if (!customers) { + return; + } + + const rowsIds = rows.map(row => customers[row].id); + const haveSaveValues = isEqual(rowsIds, selectedRowIds); + + if (!haveSaveValues) { + setSelectedRowIds(rowsIds); + } + + setClearDatagridRowSelectionCallback(clearSelection); + }, + [ + customers, + selectedRowIds, + setClearDatagridRowSelectionCallback, + setSelectedRowIds, + ], + ); + return ( openModal("delete-search")} - onTabSave={() => openModal("save-search")} - tabs={tabs.map(tab => tab.name)} - customers={mapEdgesToItems(data?.customers)} + onFilterPresetsAll={resetFilters} + onFilterPresetChange={onPresetChange} + onFilterPresetDelete={(id: number) => { + setPresetIdToDelete(id); + openModal("delete-search"); + }} + onFilterPresetPresetSave={() => openModal("save-search")} + onFilterPresetUpdate={onPresetUpdate} + filterPresets={presets.map(preset => preset.name)} + customers={customers} settings={settings} disabled={loading} + loading={loading} onUpdateListSettings={updateListSettings} onSort={handleSort} - toolbar={ - - openModal("remove", { - ids: listElements, - }) - } - > - - - } - isChecked={isSelected} - selected={listElements.length} - selectedCustomerIds={listElements} + selectedCustomerIds={selectedRowIds} + onSelectCustomerIds={handleSetSelectedCustomerIds} sort={getSortParams(params)} - toggle={toggle} - toggleAll={toggleAll} + hasPresetsChanged={hasPresetsChanged} + onCustomersDelete={() => openModal("remove", { ids: selectedRowIds })} /> params.ids.length > 0)} + open={params.action === "remove" && selectedRowIds?.length > 0} onClose={closeModal} confirmButtonState={bulkRemoveCustomersOpts.status} onConfirm={() => bulkRemoveCustomers({ variables: { - ids: params.ids, + ids: selectedRowIds, }, }) } @@ -205,10 +210,8 @@ export const CustomerList: React.FC = ({ params }) => { id="N2SbNc" defaultMessage="{counter,plural,one{Are you sure you want to delete this customer?} other{Are you sure you want to delete {displayQuantity} customers?}}" values={{ - counter: maybe(() => params.ids.length), - displayQuantity: ( - {maybe(() => params.ids.length)} - ), + counter: selectedRowIds?.length, + displayQuantity: {selectedRowIds?.length}, }} /> @@ -217,14 +220,14 @@ export const CustomerList: React.FC = ({ params }) => { open={params.action === "save-search"} confirmButtonState="default" onClose={closeModal} - onSubmit={handleTabSave} + onSubmit={onPresetSave} /> tabs[currentTab - 1].name, "...")} + onSubmit={onPresetDelete} + tabName={getPresetNameToDelete(presets, presetIdToDelete)} /> ); diff --git a/src/customers/views/CustomerList/filters.ts b/src/customers/views/CustomerList/filters.ts index 22f90a952..719a793bc 100644 --- a/src/customers/views/CustomerList/filters.ts +++ b/src/customers/views/CustomerList/filters.ts @@ -1,15 +1,14 @@ -// @ts-strict-ignore import { FilterElement } from "@dashboard/components/Filter"; import { CustomerFilterKeys, CustomerListFilterOpts, } from "@dashboard/customers/components/CustomerListPage"; import { CustomerFilterInput } from "@dashboard/graphql"; -import { maybe } from "@dashboard/misc"; import { createFilterTabUtils, createFilterUtils, + GetFilterTabsOutput, getGteLteVariables, getMinMaxQueryParam, } from "../../../utils/filters"; @@ -26,29 +25,23 @@ export function getFilterOpts( ): CustomerListFilterOpts { return { joined: { - active: maybe( - () => - [params.joinedFrom, params.joinedTo].some( - field => field !== undefined, - ), - false, - ), + active: + [params.joinedFrom, params.joinedTo].some( + field => field !== undefined, + ) ?? false, value: { - max: maybe(() => params.joinedTo, ""), - min: maybe(() => params.joinedFrom, ""), + max: params.joinedTo ?? "", + min: params.joinedFrom ?? "", }, }, numberOfOrders: { - active: maybe( - () => - [params.numberOfOrdersFrom, params.numberOfOrdersTo].some( - field => field !== undefined, - ), - false, - ), + active: + [params.numberOfOrdersFrom, params.numberOfOrdersTo].some( + field => field !== undefined, + ) ?? false, value: { - max: maybe(() => params.numberOfOrdersTo, ""), - min: maybe(() => params.numberOfOrdersFrom, ""), + max: params.numberOfOrdersTo ?? "", + min: params.numberOfOrdersFrom ?? "", }, }, }; @@ -63,8 +56,12 @@ export function getFilterVariables( lte: params.joinedTo, }), numberOfOrders: getGteLteVariables({ - gte: parseInt(params.numberOfOrdersFrom, 10), - lte: parseInt(params.numberOfOrdersTo, 10), + gte: params?.numberOfOrdersFrom + ? parseInt(params.numberOfOrdersFrom, 10) + : null, + lte: params?.numberOfOrdersTo + ? parseInt(params.numberOfOrdersTo, 10) + : null, }), search: params.query, }; @@ -92,10 +89,20 @@ export function getFilterQueryParam( } } -export const { deleteFilterTab, getFilterTabs, saveFilterTab } = - createFilterTabUtils(CUSTOMER_FILTERS_KEY); +export const storageUtils = createFilterTabUtils(CUSTOMER_FILTERS_KEY); export const { areFiltersApplied, getActiveFilters, getFiltersCurrentTab } = createFilterUtils( CustomerListUrlFiltersEnum, ); + +export const getPresetNameToDelete = ( + presets: GetFilterTabsOutput, + presetIdToDelete: number | null, +): string => { + const presetIndex = presetIdToDelete ? presetIdToDelete - 1 : 0; + const preset = presets?.[presetIndex]; + const tabName = preset?.name ?? "..."; + + return tabName; +}; diff --git a/src/discounts/views/SaleList/SaleList.tsx b/src/discounts/views/SaleList/SaleList.tsx index daff6e7fb..04e76679f 100644 --- a/src/discounts/views/SaleList/SaleList.tsx +++ b/src/discounts/views/SaleList/SaleList.tsx @@ -96,7 +96,7 @@ export const SaleList: React.FC = ({ params }) => { } = useRowSelection(params); const { - hasPresetsChange, + hasPresetsChanged, onPresetChange, onPresetDelete, onPresetSave, @@ -215,7 +215,7 @@ export const SaleList: React.FC = ({ params }) => { onFilterPresetsAll={resetFilters} filterPresets={presets.map(preset => preset.name)} selectedFilterPreset={selectedPreset} - hasPresetsChanged={hasPresetsChange} + hasPresetsChanged={hasPresetsChanged} onSalesDelete={() => openModal("remove")} selectedSaleIds={selectedRowIds} sales={sales} diff --git a/src/fixtures.ts b/src/fixtures.ts index 2658a77ee..370a919e2 100644 --- a/src/fixtures.ts +++ b/src/fixtures.ts @@ -313,8 +313,8 @@ export const filterPresetsProps: FilterPresetsProps = { onFilterPresetDelete: () => undefined, onFilterPresetPresetSave: () => undefined, onFilterPresetUpdate: () => undefined, - filterPresets: ["Tab X"], hasPresetsChanged: () => false, + filterPresets: ["Tab X"], }; export const paginatorContextValues: PaginatorContextValues = { diff --git a/src/giftCards/GiftCardsList/GiftCardsListHeader/GiftCardsListHeader.tsx b/src/giftCards/GiftCardsList/GiftCardsListHeader/GiftCardsListHeader.tsx index 58e1fd5d9..bfe062e9b 100644 --- a/src/giftCards/GiftCardsList/GiftCardsListHeader/GiftCardsListHeader.tsx +++ b/src/giftCards/GiftCardsList/GiftCardsListHeader/GiftCardsListHeader.tsx @@ -25,7 +25,7 @@ const GiftCardsListHeader: React.FC = () => { } = useGiftCardListDialogs(); const { - hasPresetsChange, + hasPresetsChanged, selectedPreset, presets, onPresetUpdate, @@ -57,7 +57,7 @@ const GiftCardsListHeader: React.FC = () => { { setPresetIdToDelete(id); diff --git a/src/hooks/useFilterPresets/useFilterPresets.ts b/src/hooks/useFilterPresets/useFilterPresets.ts index f64648e21..4fca50fcd 100644 --- a/src/hooks/useFilterPresets/useFilterPresets.ts +++ b/src/hooks/useFilterPresets/useFilterPresets.ts @@ -18,7 +18,7 @@ export interface UseFilterPresets { onPresetDelete: () => void; onPresetSave: (data: SaveFilterTabDialogFormData) => void; onPresetUpdate: (tabName: string) => void; - hasPresetsChange: () => boolean; + hasPresetsChanged: () => boolean; } export const useFilterPresets = < @@ -106,7 +106,7 @@ export const useFilterPresets = < onPresetChange(presets.findIndex(tab => tab.name === tabName) + 1); }; - const hasPresetsChange = () => { + const hasPresetsChanged = () => { const { parsedQs } = prepareQs(location.search); if (!selectedPreset) { @@ -131,6 +131,6 @@ export const useFilterPresets = < onPresetDelete, onPresetSave, onPresetUpdate, - hasPresetsChange, + hasPresetsChanged, }; }; diff --git a/src/hooks/useRowSelection.ts b/src/hooks/useRowSelection.ts index 5b0286d0a..4d2fb4d7e 100644 --- a/src/hooks/useRowSelection.ts +++ b/src/hooks/useRowSelection.ts @@ -17,10 +17,10 @@ export const useRowSelection = ( const clearDatagridRowSelectionCallback = useRef<(() => void) | null>(null); const clearRowSelection = () => { - setSelectedRowIds([]); if (clearDatagridRowSelectionCallback.current) { clearDatagridRowSelectionCallback.current(); } + setSelectedRowIds([]); }; const setClearDatagridRowSelectionCallback = (callback: () => void) => { diff --git a/src/orders/views/OrderDraftList/OrderDraftList.tsx b/src/orders/views/OrderDraftList/OrderDraftList.tsx index 78900b6f4..5e1acfc3a 100644 --- a/src/orders/views/OrderDraftList/OrderDraftList.tsx +++ b/src/orders/views/OrderDraftList/OrderDraftList.tsx @@ -125,7 +125,7 @@ export const OrderDraftList: React.FC = ({ params }) => { const { selectedPreset, presets, - hasPresetsChange, + hasPresetsChanged, onPresetChange, onPresetDelete, onPresetSave, @@ -221,7 +221,7 @@ export const OrderDraftList: React.FC = ({ params }) => { onSort={handleSort} sort={getSortParams(params)} currencySymbol={channel?.currencyCode} - hasPresetsChanged={hasPresetsChange} + hasPresetsChanged={hasPresetsChanged} onDraftOrdersDelete={() => openModal("remove", { ids: selectedRowIds, diff --git a/src/orders/views/OrderList/OrderList.tsx b/src/orders/views/OrderList/OrderList.tsx index 25de67712..224d9537e 100644 --- a/src/orders/views/OrderList/OrderList.tsx +++ b/src/orders/views/OrderList/OrderList.tsx @@ -54,7 +54,7 @@ export const OrderList: React.FC = ({ params }) => { ); const { - hasPresetsChange, + hasPresetsChanged, onPresetChange, onPresetDelete, onPresetSave, @@ -163,7 +163,7 @@ export const OrderList: React.FC = ({ params }) => { onAll={resetFilters} onSettingsOpen={() => navigate(orderSettingsPath)} params={params} - hasPresetsChanged={hasPresetsChange()} + hasPresetsChanged={hasPresetsChanged()} /> = ({ params }) => { } = useRowSelection(params); const { - hasPresetsChange, + hasPresetsChanged, onPresetChange, onPresetDelete, onPresetSave, @@ -432,7 +432,7 @@ export const ProductList: React.FC = ({ params }) => { }} onProductsDelete={() => openModal("delete")} onTabChange={onPresetChange} - hasPresetsChanged={hasPresetsChange()} + hasPresetsChanged={hasPresetsChanged()} initialSearch={params.query || ""} tabs={presets.map(tab => tab.name)} onExport={() => openModal("export")} diff --git a/src/types.ts b/src/types.ts index 2e7fad4f5..8e59a8e4c 100644 --- a/src/types.ts +++ b/src/types.ts @@ -134,7 +134,7 @@ export interface FilterPresetsProps { } export interface TabPageProps { - currentTab: number; + currentTab: number | undefined; tabs: string[]; onAll: () => void; onTabChange: (tab: number) => void; diff --git a/tsconfig.json b/tsconfig.json index 84dfd2330..ef9c40ef5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -21,7 +21,7 @@ "@locale/*": ["locale/*"], "@dashboard/*": ["src/*"], "@test/*": ["testUtils/*"] - }, + } }, "exclude": ["node_modules", "cypress"] }