diff --git a/locale/defaultMessages.json b/locale/defaultMessages.json index c62c56f2d..b76235a71 100644 --- a/locale/defaultMessages.json +++ b/locale/defaultMessages.json @@ -890,6 +890,10 @@ "context": "order status", "string": "Returned" }, + "4ELCCk": { + "context": "column picker section default header", + "string": "Categories" + }, "4IgzXs": { "context": "label", "string": "App Manifest URL" @@ -2565,6 +2569,10 @@ "context": "dialog header", "string": "delete Staff User" }, + "GhY+pm": { + "context": "dynamic column description", + "string": "Attributes" + }, "GhcypC": { "context": "header", "string": "Create Warehouse" @@ -4995,6 +5003,10 @@ "context": "product pricing, section header", "string": "Pricing" }, + "Xsh2Pa": { + "context": "column picker search input placeholder", + "string": "Search for columns" + }, "Xtd0AT": { "string": "Original String" }, @@ -5647,6 +5659,10 @@ "context": "button", "string": "Unassign and save" }, + "cPAc45": { + "context": "column picker search no results message", + "string": "No results found" + }, "cVjewM": { "context": "label for radio button", "string": "Product prices are entered with tax" @@ -7481,6 +7497,10 @@ "context": "page header", "string": "Return & replace products" }, + "rHQmCt": { + "context": "column picker section header", + "string": "Custom" + }, "rHoRbE": { "context": "Status label when object is unpublished in a channel", "string": "Unpublished" @@ -7993,6 +8013,10 @@ "uoKAmI": { "string": "Add new order" }, + "uqxjSR": { + "context": "column picker section header", + "string": "Columns" + }, "usSkzP": { "context": "navigator order mode description", "string": "Search Orders" diff --git a/package-lock.json b/package-lock.json index 558703cfb..11a0801bf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,7 +27,7 @@ "@material-ui/lab": "^4.0.0-alpha.61", "@material-ui/styles": "^4.11.4", "@reach/auto-id": "^0.16.0", - "@saleor/macaw-ui": "0.8.0-pre.96", + "@saleor/macaw-ui": "0.8.0-pre.98", "@saleor/sdk": "0.6.0", "@sentry/react": "^6.0.0", "@types/faker": "^5.1.6", @@ -7827,9 +7827,9 @@ } }, "node_modules/@saleor/macaw-ui": { - "version": "0.8.0-pre.96", - "resolved": "https://registry.npmjs.org/@saleor/macaw-ui/-/macaw-ui-0.8.0-pre.96.tgz", - "integrity": "sha512-uPVodJvUcmtz+pXAGZUZ87TZl77bx2Qt1y2jKLqZTzQGeKmqDwRDPCb0Qcb4TI7fDN93W0hFqpGIpoZm5DcbVQ==", + "version": "0.8.0-pre.98", + "resolved": "https://registry.npmjs.org/@saleor/macaw-ui/-/macaw-ui-0.8.0-pre.98.tgz", + "integrity": "sha512-W/KCjRoVVr751JxPET/ebXt9im5lleCGAtydFcuDwmxoU11wTlyxHM25owsJJBpTq1L+jdrMyXO/vZ1FL06Osg==", "dependencies": { "@dessert-box/react": "^0.4.0", "@floating-ui/react-dom-interactions": "^0.5.0", @@ -40607,9 +40607,9 @@ } }, "@saleor/macaw-ui": { - "version": "0.8.0-pre.96", - "resolved": "https://registry.npmjs.org/@saleor/macaw-ui/-/macaw-ui-0.8.0-pre.96.tgz", - "integrity": "sha512-uPVodJvUcmtz+pXAGZUZ87TZl77bx2Qt1y2jKLqZTzQGeKmqDwRDPCb0Qcb4TI7fDN93W0hFqpGIpoZm5DcbVQ==", + "version": "0.8.0-pre.98", + "resolved": "https://registry.npmjs.org/@saleor/macaw-ui/-/macaw-ui-0.8.0-pre.98.tgz", + "integrity": "sha512-W/KCjRoVVr751JxPET/ebXt9im5lleCGAtydFcuDwmxoU11wTlyxHM25owsJJBpTq1L+jdrMyXO/vZ1FL06Osg==", "requires": { "@dessert-box/react": "^0.4.0", "@floating-ui/react-dom-interactions": "^0.5.0", diff --git a/package.json b/package.json index 9e074c556..7318a6f54 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "@material-ui/lab": "^4.0.0-alpha.61", "@material-ui/styles": "^4.11.4", "@reach/auto-id": "^0.16.0", - "@saleor/macaw-ui": "0.8.0-pre.96", + "@saleor/macaw-ui": "0.8.0-pre.98", "@saleor/sdk": "0.6.0", "@sentry/react": "^6.0.0", "@types/faker": "^5.1.6", diff --git a/src/components/ButtonWithDropdown/ButtonWithDropdown.tsx b/src/components/ButtonWithDropdown/ButtonWithDropdown.tsx index af7f5c8b5..a58fb28f8 100644 --- a/src/components/ButtonWithDropdown/ButtonWithDropdown.tsx +++ b/src/components/ButtonWithDropdown/ButtonWithDropdown.tsx @@ -12,6 +12,7 @@ interface ButtonWithDropdownProps { onClick: () => void; options: Array<{ label: string; testId: string; onSelect: () => void }>; testId?: string; + disabled?: boolean; } export const ButtonWithDropdown: React.FC = ({ @@ -19,10 +20,11 @@ export const ButtonWithDropdown: React.FC = ({ options, children, testId, + disabled = false, }) => ( - diff --git a/src/components/CardMenu/CardMenu.tsx b/src/components/CardMenu/CardMenu.tsx index 7e248a624..150aa207c 100644 --- a/src/components/CardMenu/CardMenu.tsx +++ b/src/components/CardMenu/CardMenu.tsx @@ -70,7 +70,7 @@ const useStyles = makeStyles( ); /** - * @deprecated use [`TopNavMenu`](https://github.com/saleor/saleor-dashboard/blob/main/src/components/TopNavMenu/TopNavMenu.tsx) instead + * @deprecated use [`TopNav.Menu`](https://github.com/saleor/saleor-dashboard/blob/main/src/components/AppLayout/TopNav/Menu.tsx) instead */ const CardMenu: React.FC = props => { const { diff --git a/src/components/ColumnPicker/ColumnPicker.md b/src/components/ColumnPicker/ColumnPicker.md new file mode 100644 index 000000000..8af401b4f --- /dev/null +++ b/src/components/ColumnPicker/ColumnPicker.md @@ -0,0 +1,213 @@ +## Column Management Docs + +### System Architecture + +image + +### Column types +In datagrid views various types of columns are available. We can split them into two groups: +- static columns - e. g. `id`, `name`, `created_at`. These columns are simple to set up as a static object. +- dynamic columns - e.g. stocks, attributes, channels. These column values are not known in advance and must be fetched from the API. + +For identification, every column has an ID. It is a string that is unique within the view. For example, `id` column has ID `id`, `name` column has ID `name`. + +For dynamic columns, the naming convetion is as follows: + +``` +column_name:column_id +``` +For example: +``` +attribute:QXR0cmlidXRlOjIx +``` + +### useColumns hook +`useColumns` is a custom hook that is used as single source of truth for both Datagrid and Column Picker. It returns an object with the following properties: +- visible columns - array of visible columns for the datagrid +- static columns - array of static columns for the column picker +- dynamic columns - array of dynamic columns for the column picker +- column categories - array of column categories, which is abstraction for dynamic column. For example attributes is a column category, whereas Flavor attribute is an actual column value. This object has all API-related properties, like search handler, fetch more props, etc. +- selected columns - array of column IDs which are selected in the column picker. It is saved in local storage +- dynamic column settings - array of column IDs which are selected in the left section of the column picker. It is saved in local storage. +- recently added column - this value is used in datagrid component to enable auto-scroll to newly added column +- handlers: + - column resize handler (for datagrid) + - column reorder handler (for datagrid) + - column visibility handler (for column picker) + - dynamic column selection handler (for column picker) + +In order to use this hook, you need to provide four things: +- `staticColumns` - array of static columns in datagrid-ready format (`AvailableColumns[]`) +- `columnCategories` - array of column categories +- state & setter of column settings which we get from `useListSettings` +- state of column picker settings which we get from `useColumnPickerSettings` + +## Adapting new views + +### Column picker settings +Firstly, in the view file, we need to provide two settings object, one for the selected columns and one for the dynamic column settings. We should use `useColumnPickerSettings` and `useListSettings` hook for that. The first settings object manages columns selected for the datagrid (visible columns). The second manages state of seleceted dynamic columns (if we pick a value from left side of column picked, it is then displayed on the right side of the picker as dynamic column with togglable visibility). Toggling the visiblity saves the column in the first settings object. + +The reason why column picker settings object needs to be in the view file and cannot be integrated into internal logic of useColumns is because we use column picker settings in the query. We need to know which columns are selected in order to fetch the correct data from the API. + + ```tsx +const { columnPickerSettings, setDynamicColumnsSettings } = +useColumnPickerSettings("PRODUCT_LIST"); + +// Translates columnIDs to api IDs +const filteredColumnIds = columnPickerSettings + .filter(isAttributeColumnValue) + .map(getAttributeIdFromColumnValue); + +const gridAttributes = useGridAttributesQuery({ + variables: { ids: filteredColumnIds }, + skip: filteredColumnIds.length === 0, +}); + ``` + + +### Static columns adapter + +Writing an adapter for static columns is an easy task. We need to provide an array of static columns in datagrid-ready format (`AvailableColumns[]`). + +For example: +```tsx +export const parseStaticColumnsForProductListView = (intl, emptyColumn, sort) => + [ + emptyColumn, + { + id: "name", + title: intl.formatMessage(commonMessages.product), + width: 300, + icon: getColumnSortIconName(sort, ProductListUrlSortField.name), + }, + { + id: "productType", + title: intl.formatMessage(columnsMessages.type), + width: 200, + icon: getColumnSortIconName(sort, ProductListUrlSortField.productType), + }, + ].map(column => ({ + ...column, + icon: getColumnSortDirectionIcon(sort, column.id), + })); +``` + +Empty column is a special column that is used to add padding in the datagrid. It is filtered out by the column picker. + + +### Dynamic column adapter +This function creates ColumnCategory[] object from available data. + +Creating a column category requires two queries per category. Let's say we want to have custom attributes as columns. We need +- query which fetches all attributes +- query which fetches selected attributes + +We cannot rely on single query, because searching through attributes would influence already selected columns which are visible in the datagrid. + +Example: +```tsx +export const parseDynamicColumnsForProductListView = ({ + attributesData, + gridAttributesData, + activeAttributeSortId, + sort, + onSearch, + onFetchMore, + hasNextPage, + hasPreviousPage, + totalCount, +}) => [ + { + name: "Attributes", + prefix: "attribute", + availableNodes: parseAttributesColumns( + attributesData, + activeAttributeSortId, + sort, + ), + selectedNodes: parseAttributesColumns( + gridAttributesData, + activeAttributeSortId, + sort, + ), + onSearch, + onFetchMore, + hasNextPage, + hasPreviousPage, + totalCount, + }, +]; +``` +Here we only have 1 column category, attributes. `attributesData` is the result of the first query, `gridAttributesData` is the result of the second query. We also provide pagination props, which are used in the column picker. + +Queries which are used in this case are for categories. Let's look at the first query: +```tsx +export const availableColumnAttribues = gql` + query AvailableColumnAttributes( + $search: String! + $before: String + $after: String + $first: Int + $last: Int + ) { + attributes( + filter: { search: $search } + before: $before + after: $after + first: $first + last: $last + ) { + edges { + node { + id + name + } + } + pageInfo { + ...PageInfo + } + } + } +`; +``` +This query is used to fetch all **available** attributes. It is paginated and has a search filter and results are displayed in the left part of the column picker. + +The second query is similar, but it has a filter of IDs, which come from local storage settings (useColumnPickerSettngs): +```tsx +export const gridAttributes = gql` + query GridAttributes($ids: [ID!]!) { + grid: attributes(first: 25, filter: { ids: $ids }) { + edges { + node { + id + name + } + } + } + } +`; +``` +Data of this query is displayed in the right part of the column picker, below the static columns. + + +Here is the adapter for the dynamic columns inside the category: +```tsx +export const parseAttributesColumns = ( + attributes: RelayToFlat< + SearchAvailableInGridAttributesQuery["availableInGrid"] + >, + activeAttributeSortId: string, + sort: Sort, +) => + attributes.map(attribute => ({ + id: `attribute:${attribute.id}`, + title: attribute.name, + metaGroup: "Attribute", + width: 200, + icon: + attribute.id === activeAttributeSortId && + getColumnSortIconName(sort, ProductListUrlSortField.attribute), + })); +``` + +With the dynamic column adapter written, we can now use the `useColumns` hook. \ No newline at end of file diff --git a/src/components/Datagrid/ColumnPicker/ColumnPicker.tsx b/src/components/Datagrid/ColumnPicker/ColumnPicker.tsx new file mode 100644 index 000000000..c5b9ef624 --- /dev/null +++ b/src/components/Datagrid/ColumnPicker/ColumnPicker.tsx @@ -0,0 +1,121 @@ +import { + Box, + Button, + Popover, + sprinkles, + TableEditIcon, + Text, + vars, +} from "@saleor/macaw-ui/next"; +import React, { useState } from "react"; +import { FormattedMessage } from "react-intl"; + +import { AvailableColumn } from "../types"; +import { ColumnPickerCategories } from "./ColumnPickerCategories"; +import { ColumnPickerDynamicColumns } from "./ColumnPickerDynamicColumns"; +import { ColumnPickerStaticColumns } from "./ColumnPickerStaticColumns"; +import messages from "./messages"; +import { ColumnCategory } from "./useColumns"; + +export interface ColumnPickerProps { + staticColumns: AvailableColumn[]; + dynamicColumns?: AvailableColumn[]; + selectedColumns: string[]; + columnCategories?: ColumnCategory[]; + columnPickerSettings?: string[]; + onSave: (columns: string[]) => void; + onDynamicColumnSelect?: (columns: string[]) => void; +} + +export const ColumnPicker = ({ + staticColumns, + selectedColumns, + columnCategories, + dynamicColumns, + columnPickerSettings, + onDynamicColumnSelect, + onSave, +}: ColumnPickerProps) => { + const [pickerOpen, setPickerOpen] = useState(false); + const [expanded, setExpanded] = useState(false); + + const renderCategories = + columnCategories && + typeof onDynamicColumnSelect === "function" && + columnPickerSettings; + + const handleToggle = (id: string) => + selectedColumns.includes(id) + ? onSave(selectedColumns.filter(currentId => currentId !== id)) + : onSave([...selectedColumns, id]); + + return ( + { + setExpanded(false); + setPickerOpen(isPickerOpen => !isPickerOpen); + }} + > + + + )} {hasLimits(limits, "orders") && ( , PageListProps, SortPage, - FetchMoreProps, ChannelProps { activeAttributeSortId: string; - gridAttributes: RelayToFlat; - products: RelayToFlat; - onRowClick?: (id: string) => void; - rowAnchor?: (id: string) => string; - columnQuery: string; - availableInGridAttributes: RelayToFlat< - SearchAvailableInGridAttributesQuery["availableInGrid"] + gridAttributesOpts: LazyQueryResult< + GridAttributesQuery, + Exact<{ + ids: string | string[]; + }> + >; + products: RelayToFlat; + onRowClick: (id: string) => void; + rowAnchor?: (id: string) => string; + availableColumnsAttributesOpts: ReturnType< + typeof useAvailableColumnAttributesLazyQuery >; - onColumnQueryChange: (query: string) => void; onSelectProductIds: (rowsIndex: number[], clearSelection: () => void) => void; - isAttributeLoading?: boolean; hasRowHover?: boolean; + columnPickerSettings: string[]; + setDynamicColumnSettings: (cols: string[]) => void; + loading: boolean; } export const ProductListDatagrid: React.FC = ({ @@ -72,104 +84,114 @@ export const ProductListDatagrid: React.FC = ({ onSort, sort, loading, - gridAttributes, - hasMore, - isAttributeLoading, - onFetchMore, - columnQuery, - defaultSettings, - availableInGridAttributes, - onColumnQueryChange, + gridAttributesOpts, + availableColumnsAttributesOpts, activeAttributeSortId, filterDependency, onSelectProductIds, hasRowHover, rowAnchor, + columnPickerSettings, + setDynamicColumnSettings, }) => { const intl = useIntl(); const searchProductType = useSearchProductTypes(); const datagrid = useDatagridChangeState(); const { locale } = useLocale(); const productsLength = getProductRowsLength(disabled, products, disabled); - const gridAttributesFromSettings = useMemo( - () => settings.columns.filter(isAttributeColumnValue), - [settings.columns], + + const handleColumnChange = useCallback( + (picked: ProductListColumns[]) => { + onUpdateListSettings("columns", picked.filter(Boolean)); + }, + [onUpdateListSettings], ); - const { columns, setColumns } = useDatagridColumns({ - activeAttributeSortId, - gridAttributes, - gridAttributesFromSettings, - settings, - sort, + const memoizedStaticColumns = useMemo( + () => productListStaticColumnAdapter(intl, sort), + [intl, sort], + ); + + const [queryAvailableColumnsAttributes, availableColumnsAttributesData] = + availableColumnsAttributesOpts; + + const { + handlers, + visibleColumns, + staticColumns, + dynamicColumns, + selectedColumns, + columnCategories, + recentlyAddedColumn, + } = useColumns({ + staticColumns: memoizedStaticColumns, + columnCategories: productListDynamicColumnAdapter({ + availableAttributesData: getAvailableAttributesData({ + availableColumnsAttributesData, + gridAttributesOpts, + }), + selectedAttributesData: mapEdgesToItems( + gridAttributesOpts.data?.selectedAttributes, + ), + activeAttributeSortId, + sort, + onSearch: (query: string) => + queryAvailableColumnsAttributes({ + variables: { search: query, first: 10 }, + }), + initialSearch: availableColumnsAttributesData.variables?.search ?? "", + ...getAttributesFetchMoreProps({ + queryAvailableColumnsAttributes, + availableColumnsAttributesData, + gridAttributesOpts, + }), + intl, + }), + selectedColumns: settings.columns, + onSave: handleColumnChange, + columnPickerSettings, + setDynamicColumnSettings, }); - const handleColumnMoved = useCallback( - (startIndex: number, endIndex: number): void => { - setColumns(old => - addAtIndex(old[startIndex], removeAtIndex(old, startIndex), endIndex), - ); - }, - [setColumns], - ); - - const handleColumnResize = useCallback( - (column: GridColumn, newSize: number) => { - if (column.id === "empty") { - return; - } - - setColumns(prevColumns => - prevColumns.map(prevColumn => - prevColumn.id === column.id - ? { ...prevColumn, width: newSize } - : prevColumn, - ), - ); - }, - [setColumns], - ); - - const columnPickerColumns = useColumnPickerColumns( - gridAttributes, - availableInGridAttributes, - settings, - defaultSettings.columns, - ); - - const getCellContent = useMemo( - () => - createGetCellContent({ - columns, - products, - intl, - getProductTypes: searchProductType, - locale, - gridAttributes, - gridAttributesFromSettings, - selectedChannelId, + // Logic for updating sort icon in dynamic columns + // This is workaround before sorting is abstracted into useColumns + // Tracked in https://github.com/saleor/saleor-dashboard/issues/3685 + React.useEffect(() => { + handlers.onCustomUpdateVisible(prevColumns => + prevColumns?.map(column => { + if (isAttributeColumnValue(column.id)) { + if ( + getAttributeIdFromColumnValue(column.id) === activeAttributeSortId + ) { + return { + ...column, + icon: getColumnSortIconName( + sort, + ProductListUrlSortField.attribute, + ), + }; + } + return { + ...column, + icon: undefined, + }; + } + return column; }), - [ - columns, - gridAttributes, - gridAttributesFromSettings, - intl, - locale, - products, - searchProductType, - selectedChannelId, - ], - ); + ); + }, [activeAttributeSortId, sort]); const handleHeaderClicked = useCallback( (col: number) => { - const { columnName, columnId } = getColumnMetadata(columns[col].id); + const { columnName, columnId } = getColumnMetadata( + visibleColumns[col].id, + ); if (canBeSorted(columnName, !!selectedChannelId)) { onSort(columnName, columnId); } }, - [columns, onSort, selectedChannelId], + [visibleColumns, onSort, selectedChannelId], ); const handleRowClick = useCallback( @@ -196,11 +218,11 @@ export const ProductListDatagrid: React.FC = ({ const handleGetColumnTooltipContent = useCallback( (colIndex: number): string => { - const { columnName } = getColumnMetadata(columns[colIndex].id); + const { columnName } = getColumnMetadata(visibleColumns[colIndex].id); // Sortable column or empty if ( canBeSorted(columnName, !!selectedChannelId) || - columns[colIndex].id === "empty" + visibleColumns[colIndex].id === "empty" ) { return ""; } @@ -214,14 +236,27 @@ export const ProductListDatagrid: React.FC = ({ filterName: filterDependency.label, }); }, - [columns, filterDependency.label, intl, selectedChannelId], + [visibleColumns, filterDependency.label, intl, selectedChannelId], ); - const handleColumnChange = useCallback( - (picked: ProductListColumns[]) => { - onUpdateListSettings("columns", picked); - }, - [onUpdateListSettings], + const getCellContent = useMemo( + () => + createGetCellContent({ + columns: visibleColumns, + products, + intl, + getProductTypes: searchProductType, + locale, + selectedChannelId, + }), + [ + visibleColumns, + products, + intl, + searchProductType, + locale, + selectedChannelId, + ], ); return ( @@ -233,11 +268,11 @@ export const ProductListDatagrid: React.FC = ({ rowMarkers="checkbox" columnSelect="single" hasRowHover={hasRowHover} - onColumnMoved={handleColumnMoved} - onColumnResize={handleColumnResize} + onColumnMoved={handlers.onMove} + onColumnResize={handlers.onResize} verticalBorder={col => col > 0} getColumnTooltipContent={handleGetColumnTooltipContent} - availableColumns={columns} + availableColumns={visibleColumns} onHeaderClicked={handleHeaderClicked} emptyText={intl.formatMessage(messages.emptyText)} getCellContent={getCellContent} @@ -249,16 +284,16 @@ export const ProductListDatagrid: React.FC = ({ fullScreenTitle={intl.formatMessage(messages.products)} onRowClick={handleRowClick} rowAnchor={handleRowAnchor} - renderColumnPicker={defaultProps => ( + recentlyAddedColumn={recentlyAddedColumn} + renderColumnPicker={() => ( )} /> diff --git a/src/products/components/ProductListDatagrid/utils.ts b/src/products/components/ProductListDatagrid/datagrid.ts similarity index 65% rename from src/products/components/ProductListDatagrid/utils.ts rename to src/products/components/ProductListDatagrid/datagrid.ts index dd790c489..a98241b62 100644 --- a/src/products/components/ProductListDatagrid/utils.ts +++ b/src/products/components/ProductListDatagrid/datagrid.ts @@ -1,6 +1,8 @@ // @ts-strict-ignore +import { LazyQueryResult, QueryLazyOptions } from "@apollo/client"; import { messages } from "@dashboard/components/ChannelsAvailabilityDropdown/messages"; import { getChannelAvailabilityLabel } from "@dashboard/components/ChannelsAvailabilityDropdown/utils"; +import { ColumnCategory } from "@dashboard/components/Datagrid/ColumnPicker/useColumns"; import { dropdownCell, readonlyTextCell, @@ -15,47 +17,40 @@ import { GetCellContentOpts } from "@dashboard/components/Datagrid/Datagrid"; import { AvailableColumn } from "@dashboard/components/Datagrid/types"; import { Locale } from "@dashboard/components/Locale"; import { getMoneyRange } from "@dashboard/components/MoneyRange"; -import { ProductListColumns } from "@dashboard/config"; -import { GridAttributesQuery, ProductListQuery } from "@dashboard/graphql"; +import { + AvailableColumnAttributesQuery, + Exact, + GridAttributesQuery, + ProductListQuery, + SearchAvailableInGridAttributesQuery, +} from "@dashboard/graphql"; import { commonMessages } from "@dashboard/intl"; import { getDatagridRowDataIndex } from "@dashboard/misc"; import { ProductListUrlSortField } from "@dashboard/products/urls"; import { RelayToFlat, Sort } from "@dashboard/types"; import { getColumnSortDirectionIcon } from "@dashboard/utils/columns/getColumnSortDirectionIcon"; +import { mapEdgesToItems } from "@dashboard/utils/maps"; import { Item } from "@glideapps/glide-data-grid"; import moment from "moment-timezone"; import { IntlShape } from "react-intl"; import { getAttributeIdFromColumnValue } from "../ProductListPage/utils"; -import { columnsMessages } from "./messages"; +import { categoryMetaGroups, columnsMessages } from "./messages"; -interface GetColumnsProps { - intl: IntlShape; - sort: Sort; - gridAttributes: RelayToFlat; - gridAttributesFromSettings: ProductListColumns[]; - activeAttributeSortId: string; -} - -export function getColumns({ - intl, - sort, - gridAttributes, - gridAttributesFromSettings, - activeAttributeSortId, -}: GetColumnsProps): AvailableColumn[] { - return [ +export const productListStaticColumnAdapter = ( + intl: IntlShape, + sort: Sort, +) => + [ { id: "name", title: intl.formatMessage(commonMessages.product), width: 300, - icon: getColumnSortDirectionIcon(sort, ProductListUrlSortField.name), }, { id: "productType", title: intl.formatMessage(columnsMessages.type), width: 200, - icon: getColumnSortIconName(sort, ProductListUrlSortField.productType), }, { id: "description", @@ -66,50 +61,79 @@ export function getColumns({ id: "availability", title: intl.formatMessage(columnsMessages.availability), width: 250, - icon: getColumnSortIconName(sort, ProductListUrlSortField.availability), }, { id: "date", title: intl.formatMessage(columnsMessages.updatedAt), width: 250, - icon: getColumnSortIconName(sort, ProductListUrlSortField.date), }, { id: "price", title: intl.formatMessage(columnsMessages.price), width: 250, - icon: getColumnSortIconName(sort, ProductListUrlSortField.price), }, - ...gridAttributesFromSettings.map( - toAttributeColumnData(gridAttributes, activeAttributeSortId, sort), - ), - ]; -} + ].map(column => ({ + ...column, + icon: getColumnSortDirectionIcon(sort, column.id), + })); -export function toAttributeColumnData( - gridAttributes: RelayToFlat, +export const productListDynamicColumnAdapter = ({ + availableAttributesData, + selectedAttributesData, + activeAttributeSortId, + sort, + onSearch, + initialSearch, + hasNextPage, + hasPreviousPage, + onNextPage, + onPreviousPage, + intl, +}): ColumnCategory[] => [ + { + name: intl.formatMessage(categoryMetaGroups.attribute), + prefix: "attribute", + availableNodes: parseAttributesColumns( + availableAttributesData, + activeAttributeSortId, + sort, + intl, + ), + selectedNodes: parseAttributesColumns( + selectedAttributesData, + activeAttributeSortId, + sort, + intl, + ), + onSearch, + initialSearch, + hasNextPage, + hasPreviousPage, + onNextPage, + onPreviousPage, + }, +]; + +export const parseAttributesColumns = ( + attributes: RelayToFlat< + SearchAvailableInGridAttributesQuery["availableInGrid"] + >, activeAttributeSortId: string, sort: Sort, -) { - return (attribute: ProductListColumns) => { - const attributeId = getAttributeIdFromColumnValue(attribute); + intl: IntlShape, +) => + attributes?.map(attribute => ({ + id: `attribute:${attribute.id}`, + title: attribute.name, + metaGroup: intl.formatMessage(categoryMetaGroups.attribute), + width: 200, + icon: + attribute.id === activeAttributeSortId + ? getColumnSortIconName(sort, ProductListUrlSortField.attribute) + : undefined, + })); - const title = - gridAttributes.find(gridAttribute => attributeId === gridAttribute.id) - ?.name ?? ""; - - return { - id: attribute, - title, - width: 200, - icon: - attributeId === activeAttributeSortId && - getColumnSortDirectionIcon(sort, ProductListUrlSortField.attribute), - }; - }; -} - -function getColumnSortIconName( +export function getColumnSortIconName( { sort, asc }: Sort, columnName: ProductListUrlSortField, ) { @@ -130,8 +154,6 @@ interface GetCellContentProps { intl: IntlShape; getProductTypes: (query: string) => Promise; locale: Locale; - gridAttributes: RelayToFlat; - gridAttributesFromSettings: ProductListColumns[]; selectedChannelId?: string; } @@ -369,3 +391,99 @@ export function getProductRowsLength( return 0; } + +type AvailableAttributesDataQueryResult = LazyQueryResult< + AvailableColumnAttributesQuery, + Exact<{ + search: string; + before?: string; + after?: string; + first?: number; + last?: number; + }> +>; + +type GridAttributesDataQueryResult = LazyQueryResult< + GridAttributesQuery, + Exact<{ + ids: string | string[]; + }> +>; + +type AttributesLazyQuery = ( + options?: QueryLazyOptions< + Exact<{ + search: string; + before?: string; + after?: string; + first?: number; + last?: number; + }> + >, +) => void; + +/** + * To avoid overfetching we use single query for initial render + * (gridAttributesOpts) and when pagination / search is used + * we use separate query - availableColumnsAttributesData + */ +export const getAvailableAttributesData = ({ + availableColumnsAttributesData, + gridAttributesOpts, +}: { + availableColumnsAttributesData: AvailableAttributesDataQueryResult; + gridAttributesOpts: GridAttributesDataQueryResult; +}) => + mapEdgesToItems(availableColumnsAttributesData.data?.attributes) ?? + (availableColumnsAttributesData.loading + ? undefined + : mapEdgesToItems(gridAttributesOpts.data?.availableAttributes) ?? []); + +export const getAttributesFetchMoreProps = ({ + queryAvailableColumnsAttributes, + availableColumnsAttributesData, + gridAttributesOpts, +}: { + queryAvailableColumnsAttributes: AttributesLazyQuery; + availableColumnsAttributesData: AvailableAttributesDataQueryResult; + gridAttributesOpts: GridAttributesDataQueryResult; +}) => { + const onNextPage = (query: string) => + queryAvailableColumnsAttributes({ + variables: { + search: query, + after: + availableColumnsAttributesData.data?.attributes?.pageInfo.endCursor ?? + gridAttributesOpts.data?.availableAttributes?.pageInfo.endCursor, + first: 10, + last: null, + before: null, + }, + }); + const onPreviousPage = (query: string) => + queryAvailableColumnsAttributes({ + variables: { + search: query, + before: + availableColumnsAttributesData.data?.attributes?.pageInfo.startCursor, + last: 10, + first: null, + after: null, + }, + }); + + const hasNextPage = + availableColumnsAttributesData.data?.attributes?.pageInfo?.hasNextPage ?? + gridAttributesOpts.data?.availableAttributes?.pageInfo?.hasNextPage ?? + false; + const hasPreviousPage = + availableColumnsAttributesData.data?.attributes?.pageInfo + ?.hasPreviousPage ?? false; + + return { + hasNextPage, + hasPreviousPage, + onNextPage, + onPreviousPage, + }; +}; diff --git a/src/products/components/ProductListDatagrid/hooks/useColumnPickerColumns.test.ts b/src/products/components/ProductListDatagrid/hooks/useColumnPickerColumns.test.ts deleted file mode 100644 index 4a5799fd8..000000000 --- a/src/products/components/ProductListDatagrid/hooks/useColumnPickerColumns.test.ts +++ /dev/null @@ -1,171 +0,0 @@ -// @ts-strict-ignore -import { ProductListColumns } from "@dashboard/config"; -import { - GridAttributesQuery, - SearchAvailableInGridAttributesQuery, -} from "@dashboard/graphql"; -import { ListSettings, RelayToFlat } from "@dashboard/types"; -import { renderHook } from "@testing-library/react-hooks"; - -import { useColumnPickerColumns } from "./useColumnPickerColumns"; - -jest.mock("react-intl", () => ({ - useIntl: jest.fn(() => ({ - formatMessage: jest.fn(x => x.defaultMessage), - })), - defineMessages: jest.fn(x => x), -})); - -describe("useColumnPickerColumns", () => { - const staticColumns = [ - { - label: "Availability", - value: "availability" as ProductListColumns, - }, - { - label: "Price", - value: "price" as ProductListColumns, - }, - { - label: "Description", - value: "description" as ProductListColumns, - }, - { - label: "Type", - value: "productType" as ProductListColumns, - }, - { - label: "Last updated", - value: "date" as ProductListColumns, - }, - ]; - - it("should return static columns when attributes are empty and settings contains all columns", () => { - // Arrange - const settings = { - columns: [ - "availability", - "description", - "price", - "productType", - "date", - ] as ListSettings["columns"], - rowNumber: 20, - }; - - const defaultSettings = settings.columns; - - // Act - const { result } = renderHook(() => - useColumnPickerColumns([], [], settings, defaultSettings), - ); - - // Assert - expect(result.current).toEqual({ - initialColumns: [...staticColumns], - availableColumns: [...staticColumns], - defaultColumns: [...defaultSettings], - }); - }); - - it("should return columns selected in settings", () => { - // Arrange - const settings = { - columns: [ - "availability", - "date", - ] as ListSettings["columns"], - rowNumber: 20, - }; - - const defaultSettings = settings.columns; - - // Act - const { result } = renderHook(() => - useColumnPickerColumns([], [], settings, defaultSettings), - ); - - // Assert - expect(result.current).toEqual({ - initialColumns: [staticColumns[0], staticColumns[4]], - availableColumns: [...staticColumns], - defaultColumns: [...defaultSettings], - }); - }); - - it("should return selected in setting with attributes", () => { - // Arrange - const settings = { - columns: [ - "availability", - "date", - ] as ListSettings["columns"], - rowNumber: 20, - }; - const selectedAttibutes = [ - { - __typename: "Attribute", - id: "1", - name: "Attr1", - }, - { - __typename: "Attribute", - id: "2", - name: "Attr2", - }, - ] as RelayToFlat; - - const availableAttributesToSelect = [ - { - __typename: "Attribute", - id: "op1", - name: "AttrOption1", - }, - { - __typename: "Attribute", - id: "op2", - name: "AttrOption2", - }, - { - __typename: "Attribute", - id: "op3", - name: "AttrOption3", - }, - ] as RelayToFlat; - - const defaultSettings = settings.columns; - - // Act - const { result } = renderHook(() => - useColumnPickerColumns( - selectedAttibutes, - availableAttributesToSelect, - settings, - defaultSettings, - ), - ); - - // Assert - expect(result.current).toEqual({ - initialColumns: [ - staticColumns[0], - staticColumns[4], - { - label: "Attr1", - value: "attribute:1", - }, - { label: "Attr2", value: "attribute:2" }, - ], - availableColumns: [ - ...staticColumns, - { - label: "AttrOption1", - value: "attribute:op1", - }, - { label: "AttrOption2", value: "attribute:op2" }, - { label: "AttrOption3", value: "attribute:op3" }, - ], - defaultColumns: [...defaultSettings], - }); - }); -}); diff --git a/src/products/components/ProductListDatagrid/hooks/useColumnPickerColumns.ts b/src/products/components/ProductListDatagrid/hooks/useColumnPickerColumns.ts deleted file mode 100644 index bddb15798..000000000 --- a/src/products/components/ProductListDatagrid/hooks/useColumnPickerColumns.ts +++ /dev/null @@ -1,80 +0,0 @@ -// @ts-strict-ignore -import { MultiAutocompleteChoiceType } from "@dashboard/components/MultiAutocompleteSelectField"; -import { ProductListColumns } from "@dashboard/config"; -import { - GridAttributesQuery, - SearchAvailableInGridAttributesQuery, -} from "@dashboard/graphql"; -import { commonMessages } from "@dashboard/intl"; -import { ListSettings, RelayToFlat } from "@dashboard/types"; -import { useMemo } from "react"; -import { useIntl } from "react-intl"; - -import { getAttributeColumnValue } from "../../ProductListPage/utils"; -import { columnsMessages } from "../messages"; - -export const useColumnPickerColumns = ( - gridAttributes: RelayToFlat, - availableInGridAttributes: RelayToFlat< - SearchAvailableInGridAttributesQuery["availableInGrid"] - >, - settings: ListSettings, - defaultColumns: ProductListColumns[], -) => { - const intl = useIntl(); - - const staticColumns = useMemo( - () => [ - { - label: intl.formatMessage(columnsMessages.availability), - value: "availability" as ProductListColumns, - }, - { - label: intl.formatMessage(columnsMessages.price), - value: "price" as ProductListColumns, - }, - { - label: intl.formatMessage(commonMessages.description), - value: "description" as ProductListColumns, - }, - { - label: intl.formatMessage(columnsMessages.type), - value: "productType" as ProductListColumns, - }, - { - label: intl.formatMessage(columnsMessages.updatedAt), - value: "date" as ProductListColumns, - }, - ], - [intl], - ); - - const initialColumns = useMemo(() => { - const selectedStaticColumns = staticColumns.filter(column => - (settings.columns || []).includes(column.value), - ); - const selectedAttributeColumns = gridAttributes.map(attribute => ({ - label: attribute.name, - value: getAttributeColumnValue(attribute.id), - })); - - return [...selectedStaticColumns, ...selectedAttributeColumns]; - }, [gridAttributes, settings.columns, staticColumns]); - - const availableColumns: MultiAutocompleteChoiceType[] = [ - ...staticColumns, - ...availableInGridAttributes.map( - attribute => - ({ - label: attribute.name, - value: getAttributeColumnValue(attribute.id), - } as MultiAutocompleteChoiceType), - ), - ]; - - return { - availableColumns, - initialColumns, - defaultColumns, - }; -}; diff --git a/src/products/components/ProductListDatagrid/hooks/useDatagridColumns.ts b/src/products/components/ProductListDatagrid/hooks/useDatagridColumns.ts deleted file mode 100644 index 1bb672ace..000000000 --- a/src/products/components/ProductListDatagrid/hooks/useDatagridColumns.ts +++ /dev/null @@ -1,114 +0,0 @@ -// @ts-strict-ignore -import { AvailableColumn } from "@dashboard/components/Datagrid/types"; -import { ProductListColumns } from "@dashboard/config"; -import { GridAttributesQuery } from "@dashboard/graphql"; -import { ProductListUrlSortField } from "@dashboard/products/urls"; -import { ListSettings, RelayToFlat, Sort } from "@dashboard/types"; -import { getColumnSortDirectionIcon } from "@dashboard/utils/columns/getColumnSortDirectionIcon"; -import { useEffect, useRef, useState } from "react"; -import { useIntl } from "react-intl"; - -import { getColumns, toAttributeColumnData } from "../utils"; - -interface UseDatagridColumnsProps { - activeAttributeSortId: string; - gridAttributes: RelayToFlat; - gridAttributesFromSettings: ProductListColumns[]; - sort: Sort; - settings: ListSettings; -} - -export const useDatagridColumns = ({ - sort, - gridAttributes, - gridAttributesFromSettings, - activeAttributeSortId, - settings, -}: UseDatagridColumnsProps) => { - const intl = useIntl(); - - const initialColumns = useRef( - getColumns({ - intl, - sort, - gridAttributes, - gridAttributesFromSettings, - activeAttributeSortId, - }), - ); - - const [columns, setColumns] = useState([ - initialColumns.current[0], - ...initialColumns.current.filter(col => - settings.columns.includes(col.id as ProductListColumns), - ), - ]); - - useEffect(() => { - const attributeColumns = gridAttributesFromSettings.map( - toAttributeColumnData(gridAttributes, activeAttributeSortId, sort), - ); - - setColumns(prevColumns => [ - ...prevColumns - .filter(byColumnsInSettingsOrStaticColumns(settings)) - .map(toCurrentColumnData(sort, attributeColumns)), - ...settings.columns - .filter(byNewAddedColumns(prevColumns)) - .map( - toNewAddedColumData( - [...initialColumns.current, ...attributeColumns], - sort, - ), - ), - ]); - }, [ - activeAttributeSortId, - gridAttributes, - gridAttributesFromSettings, - settings, - sort, - ]); - - return { columns, setColumns }; -}; - -function byNewAddedColumns(currentColumns: AvailableColumn[]) { - return (column: ProductListColumns) => - !currentColumns.find(c => c.id === column); -} - -function byColumnsInSettingsOrStaticColumns( - settings: ListSettings, -) { - return (column: AvailableColumn) => - settings.columns.includes(column.id as ProductListColumns) || - ["name"].includes(column.id); -} - -function toCurrentColumnData( - sort: Sort, - attributeColumns: AvailableColumn[], -) { - return (column: AvailableColumn) => { - // Take newest attibutes data from attributeColumns - if (column.id.startsWith("attribute")) { - return attributeColumns.find(ac => ac.id === column.id); - } - - return { - ...column, - icon: getColumnSortDirectionIcon(sort, column.id), - }; - }; -} - -function toNewAddedColumData( - columnSource: AvailableColumn[], - sort: Sort, -) { - return (column: ProductListColumns) => ({ - ...columnSource.find(ac => ac.id === column), - icon: getColumnSortDirectionIcon(sort, column as ProductListUrlSortField), - }); -} diff --git a/src/products/components/ProductListDatagrid/messages.ts b/src/products/components/ProductListDatagrid/messages.ts index 711c2ca9c..6a281f3b4 100644 --- a/src/products/components/ProductListDatagrid/messages.ts +++ b/src/products/components/ProductListDatagrid/messages.ts @@ -41,3 +41,11 @@ export const columnsMessages = defineMessages({ description: "product updated at", }, }); + +export const categoryMetaGroups = defineMessages({ + attribute: { + id: "GhY+pm", + defaultMessage: "Attributes", + description: "dynamic column description", + }, +}); diff --git a/src/products/components/ProductListPage/ProductListPage.stories.tsx b/src/products/components/ProductListPage/ProductListPage.stories.tsx index a40468778..c7db172c4 100644 --- a/src/products/components/ProductListPage/ProductListPage.stories.tsx +++ b/src/products/components/ProductListPage/ProductListPage.stories.tsx @@ -10,10 +10,12 @@ import { pageListProps, sortPageProps, } from "@dashboard/fixtures"; -import { products as productListFixture } from "@dashboard/products/fixtures"; +import { + gridAttributesResult, + products as productListFixture, +} from "@dashboard/products/fixtures"; import { ProductListUrlSortField } from "@dashboard/products/urls"; import { productListFilterOpts } from "@dashboard/products/views/ProductList/fixtures"; -import { attributes } from "@dashboard/productTypes/fixtures"; import { ListViews } from "@dashboard/types"; import { Meta, StoryObj } from "@storybook/react"; @@ -41,14 +43,19 @@ const props: ProductListPageProps = { hasPresetsChanged: false, onTabSave: () => undefined, onTabUpdate: () => undefined, - availableInGridAttributes: [], + availableColumnsAttributesOpts: [ + () => undefined, + { data: undefined }, + ] as any, onColumnQueryChange: () => undefined, }, activeAttributeSortId: undefined, currencySymbol: "USD", defaultSettings: defaultListSettings[ListViews.PRODUCT_LIST], filterOpts: productListFilterOpts, - gridAttributes: attributes, + gridAttributesOpts: { + data: gridAttributesResult, + } as any, limits, onExport: () => undefined, products, @@ -60,6 +67,8 @@ const props: ProductListPageProps = { ...pageListProps.default.settings, columns: ["availability", "productType", "price"], }, + columnPickerSettings: [], + setDynamicColumnSettings: () => undefined, }; const meta: Meta = { diff --git a/src/products/components/ProductListPage/ProductListPage.tsx b/src/products/components/ProductListPage/ProductListPage.tsx index 840087d7d..ade638f1c 100644 --- a/src/products/components/ProductListPage/ProductListPage.tsx +++ b/src/products/components/ProductListPage/ProductListPage.tsx @@ -1,4 +1,5 @@ // @ts-strict-ignore +import { LazyQueryResult } from "@apollo/client/react"; import { extensionMountPoints, mapToMenuItems, @@ -14,17 +15,17 @@ import { ListPageLayout } from "@dashboard/components/Layouts"; import LimitReachedAlert from "@dashboard/components/LimitReachedAlert"; import { ProductListColumns } from "@dashboard/config"; import { + Exact, GridAttributesQuery, ProductListQuery, RefreshLimitsQuery, - SearchAvailableInGridAttributesQuery, + useAvailableColumnAttributesLazyQuery, } from "@dashboard/graphql"; import useLocalStorage from "@dashboard/hooks/useLocalStorage"; import useNavigator from "@dashboard/hooks/useNavigator"; import { sectionNames } from "@dashboard/intl"; import { ChannelProps, - FetchMoreProps, FilterPageProps, PageListProps, RelayToFlat, @@ -53,25 +54,29 @@ export interface ProductListPageProps FilterPageProps, "onTabDelete" >, - FetchMoreProps, SortPage, ChannelProps { activeAttributeSortId: string; - availableInGridAttributes: RelayToFlat< - SearchAvailableInGridAttributesQuery["availableInGrid"] - >; - columnQuery: string; currencySymbol: string; - gridAttributes: RelayToFlat; + gridAttributesOpts: LazyQueryResult< + GridAttributesQuery, + Exact<{ + ids: string | string[]; + }> + >; limits: RefreshLimitsQuery["shop"]["limits"]; products: RelayToFlat; selectedProductIds: string[]; hasPresetsChanged: boolean; onAdd: () => void; onExport: () => void; - onColumnQueryChange: (query: string) => void; onTabUpdate: (tabName: string) => void; onTabDelete: (tabIndex: number) => void; + columnPickerSettings: string[]; + setDynamicColumnSettings: (cols: string[]) => void; + availableColumnsAttributesOpts: ReturnType< + typeof useAvailableColumnAttributesLazyQuery + >; onProductsDelete: () => void; onSelectProductIds: (ids: number[], clearSelection: () => void) => void; clearRowSelection: () => void; @@ -83,21 +88,16 @@ const DEFAULT_PRODUCT_LIST_VIEW_TYPE: ProductListViewType = "datagrid"; export const ProductListPage: React.FC = props => { const { - columnQuery, currencySymbol, defaultSettings, - gridAttributes, + gridAttributesOpts, limits, - availableInGridAttributes, + availableColumnsAttributesOpts, filterOpts, - hasMore, initialSearch, - loading, settings, onAdd, - onColumnQueryChange, onExport, - onFetchMore, onFilterChange, onFilterAttributeFocus, onSearchChange, @@ -112,6 +112,8 @@ export const ProductListPage: React.FC = props => { tabs, onTabUpdate, hasPresetsChanged, + columnPickerSettings, + setDynamicColumnSettings, selectedProductIds, onProductsDelete, clearRowSelection, @@ -291,20 +293,20 @@ export const ProductListPage: React.FC = props => { hasRowHover={!isFilterPresetOpen} filterDependency={filterDependency} activeAttributeSortId={activeAttributeSortId} - columnQuery={columnQuery} defaultSettings={defaultSettings} - availableInGridAttributes={availableInGridAttributes} - isAttributeLoading={loading} + availableColumnsAttributesOpts={availableColumnsAttributesOpts} loading={listProps.disabled} - hasMore={hasMore} - gridAttributes={gridAttributes} - onColumnQueryChange={onColumnQueryChange} - onFetchMore={onFetchMore} + gridAttributesOpts={gridAttributesOpts} products={listProps.products} settings={settings} selectedChannelId={selectedChannelId} onUpdateListSettings={onUpdateListSettings} rowAnchor={productUrl} + onRowClick={id => { + navigate(productUrl(id)); + }} + columnPickerSettings={columnPickerSettings} + setDynamicColumnSettings={setDynamicColumnSettings} /> ) : ( = ({ params }) => { ListViews.PRODUCT_LIST, ); + const { columnPickerSettings, setDynamicColumnsSettings } = + useColumnPickerSettings("PRODUCT_LIST"); // Keep reference to clear datagrid selection function const clearRowSelectionCallback = React.useRef<() => void | null>(null); const clearRowSelection = () => { @@ -354,7 +356,7 @@ export const ProductList: React.FC = ({ params }) => { [params, settings.rowNumber], ); - const filteredColumnIds = settings.columns + const filteredColumnIds = (columnPickerSettings ?? []) .filter(isAttributeColumnValue) .map(getAttributeIdFromColumnValue); @@ -363,7 +365,6 @@ export const ProductList: React.FC = ({ params }) => { variables: { ...queryVariables, hasChannel: !!selectedChannel, - hasSelectedAttributes: filteredColumnIds.length > 0, }, }); @@ -387,16 +388,21 @@ export const ProductList: React.FC = ({ params }) => { [products, selectedProductIds], ); - const availableInGridAttributesOpts = useAvailableInGridAttributesSearch({ - variables: { - ...DEFAULT_INITIAL_SEARCH_DATA, - first: 5, - }, - }); - const gridAttributes = useGridAttributesQuery({ - variables: { ids: filteredColumnIds }, - skip: filteredColumnIds.length === 0, - }); + const availableColumnsAttributesOpts = + useAvailableColumnAttributesLazyQuery(); + + const [gridAttributesQuery, gridAttributesOpts] = + useGridAttributesLazyQuery(); + + useEffect(() => { + // Fetch this only on initial render + gridAttributesQuery({ + variables: { + ids: filteredColumnIds, + hasAttributes: !!filteredColumnIds.length, + }, + }); + }, []); const { loadMore: loadMoreDialogProductTypes, @@ -460,31 +466,16 @@ export const ProductList: React.FC = ({ params }) => { sort: params.sort, }} onSort={handleSort} - availableInGridAttributes={ - mapEdgesToItems( - availableInGridAttributesOpts.result?.data?.availableInGrid, - ) || [] - } currencySymbol={selectedChannel?.currencyCode || ""} currentTab={currentTab} defaultSettings={defaultListSettings[ListViews.PRODUCT_LIST]} filterOpts={filterOpts} - gridAttributes={mapEdgesToItems(gridAttributes?.data?.grid) || []} + gridAttributesOpts={gridAttributesOpts} settings={settings} - loading={ - availableInGridAttributesOpts.result.loading || gridAttributes.loading - } - hasMore={maybe( - () => - availableInGridAttributesOpts.result.data.availableInGrid.pageInfo - .hasNextPage, - false, - )} + availableColumnsAttributesOpts={availableColumnsAttributesOpts} disabled={loading} limits={limitOpts.data?.shop.limits} products={products} - onColumnQueryChange={availableInGridAttributesOpts.search} - onFetchMore={availableInGridAttributesOpts.loadMore} onUpdateListSettings={(...props) => { clearRowSelection(); updateListSettings(...props); @@ -507,9 +498,10 @@ export const ProductList: React.FC = ({ params }) => { tabs={tabs.map(tab => tab.name)} onExport={() => openModal("export")} selectedChannelId={selectedChannel?.id} + columnPickerSettings={columnPickerSettings} + setDynamicColumnSettings={setDynamicColumnsSettings} selectedProductIds={selectedProductIds} onSelectProductIds={handleSetSelectedProductIds} - columnQuery={availableInGridAttributesOpts.query} clearRowSelection={clearRowSelection} setBulkDeleteButtonRef={(ref: HTMLButtonElement) => { deleteButtonRef.current = ref;