🏛️ Implement new column picker (#3514)
* (wip) basic column picker implementation * (wip) fix stories error * (wip) fix custom column cells & sorting * (wip) Group attributes query values in props * (wip) Add docs * (wip) fix story error * Implement new column picker in order list * Fix search input padding * Fix empty settings crash * Fix empty settings crash * Portalize popover * Preselect category when there is only one * Remove legacy code * Move product column adapters to proper directory * Improve file structure * Rename custom columns to dynamic columns * Improve strings * (wip) Implement pagination logic * Beautify pagination * Update macaw-ui * Add unit test for useColumns * Improve naming * Code review style fixes * Update docs * Remove empty column * Remove unexpected padding from attribute columns * Add min height to prevent jumping * Fix typo in column picker files * CSS fixes to popover content * Implement auto-scroll to new column * Improve docs * Improve type in useColumns reducer * Extract dynamic columns from NewColumnPicker as a smaller component * Disable movement of empty column * Fix circular loader * Do not persist expanded state when closing picker * Fix dynamic columns with long names line break * Prevent user from disabling all static columns * Simplify dynamic column reducer into flatMap * Optimistic UIbasic implementation * Make available attributes query lazy * Fix displaying arrow next to sorted column * Add explainatory comment * Fix product list stories * Fix dynamic columns and query handling for empty lists * Remove jump on first attribute selection * Remove jump on first attribute selection * Fix search * Add debounce to search * CR - base state change on previous in popover * Extract search into separate component * Rename useDynamicColumnSettings to useColumnPickerSettings * Use difference from lodash * Improve static column rendering * Improve order list * Fix stories error * Improve search for edge cases * Extract messages * Calculate more complex dynamic column adapter parameters in separate functions * Fix url pointing to CardMenu substitute * Extract nodes to separate component * Move files, remove New prefix * Extract to static columns * Refactor utility functions * Render column picker categories only when provided in props * Replace switch with if statements * Refactor to hooks * Remove comments with legacy stacing * Add spaces for better readibility * Tighten types * Add space * Add extra comments to useColumns optimistic update logic * Fix paddings * Add arbitrary min/max height for handling various resolutions * Add full-width hover & click effect on candidate columns labels * Handle long attribute names * Make column picker popover a modal * Bump macaw-ui * Fix macaw-ui type mismatch * Update types * Add data-test-id * Fix closing picker with trigger button * Replace list settings deep merging with shallow merging * Fix list settings deep merging behaviour for arrays * Replace conditional data-test-id- with close-search * Rename targetArray to columnsToCheck * Rename filterColumns to filterSelectedColumns * Rename gridAttributes query objects to more descriptive * Add explainatory comment to customizer function in useListSettings * Update strictness * Fix rendering dynamic columns
This commit is contained in:
parent
85c8f15605
commit
66bab6feab
45 changed files with 2334 additions and 740 deletions
|
@ -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"
|
||||
|
|
14
package-lock.json
generated
14
package-lock.json
generated
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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<ButtonWithDropdownProps> = ({
|
||||
|
@ -19,10 +20,11 @@ export const ButtonWithDropdown: React.FC<ButtonWithDropdownProps> = ({
|
|||
options,
|
||||
children,
|
||||
testId,
|
||||
disabled = false,
|
||||
}) => (
|
||||
<Dropdown>
|
||||
<Dropdown.Trigger>
|
||||
<Button data-test-id={testId} onClick={onClick}>
|
||||
<Button data-test-id={testId} onClick={onClick} disabled={disabled}>
|
||||
{children}
|
||||
<ChervonDownIcon />
|
||||
</Button>
|
||||
|
|
|
@ -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<CardMenuProps> = props => {
|
||||
const {
|
||||
|
|
213
src/components/ColumnPicker/ColumnPicker.md
Normal file
213
src/components/ColumnPicker/ColumnPicker.md
Normal file
|
@ -0,0 +1,213 @@
|
|||
## Column Management Docs
|
||||
|
||||
### System Architecture
|
||||
|
||||
<img width="977" alt="image" src="https://user-images.githubusercontent.com/41952692/233042483-d2cb30f3-26b7-40b5-9d08-2ea42f7f0242.png">
|
||||
|
||||
### 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<ProductListUrlSortField>,
|
||||
) =>
|
||||
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.
|
121
src/components/Datagrid/ColumnPicker/ColumnPicker.tsx
Normal file
121
src/components/Datagrid/ColumnPicker/ColumnPicker.tsx
Normal file
|
@ -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 (
|
||||
<Popover
|
||||
modal
|
||||
open={pickerOpen}
|
||||
onOpenChange={() => {
|
||||
setExpanded(false);
|
||||
setPickerOpen(isPickerOpen => !isPickerOpen);
|
||||
}}
|
||||
>
|
||||
<Popover.Trigger>
|
||||
<Button
|
||||
variant="tertiary"
|
||||
icon={<TableEditIcon />}
|
||||
pointerEvents={pickerOpen ? "none" : undefined}
|
||||
__backgroundColor={
|
||||
pickerOpen
|
||||
? vars.colors.background.interactiveNeutralSecondaryPressing
|
||||
: undefined
|
||||
}
|
||||
__borderColor={
|
||||
pickerOpen ? vars.colors.border.neutralSubdued : undefined
|
||||
}
|
||||
/>
|
||||
</Popover.Trigger>
|
||||
<Popover.Content className={sprinkles({ margin: 1.5 })}>
|
||||
<Box
|
||||
display="grid"
|
||||
gridTemplateColumns={expanded ? 2 : 1}
|
||||
overflow="hidden"
|
||||
>
|
||||
{expanded && renderCategories && (
|
||||
<ColumnPickerCategories
|
||||
columnCategories={columnCategories}
|
||||
columnPickerSettings={columnPickerSettings}
|
||||
onDynamicColumnSelect={onDynamicColumnSelect}
|
||||
onClose={() => setExpanded(false)}
|
||||
/>
|
||||
)}
|
||||
<Box
|
||||
__width="320px"
|
||||
__maxHeight="70vh"
|
||||
__minHeight={expanded ? "502px" : undefined}
|
||||
backgroundColor="plain"
|
||||
padding={4}
|
||||
overflow="scroll"
|
||||
>
|
||||
<Box marginBottom={3}>
|
||||
<Text variant="caption" size="small" color="textNeutralSubdued">
|
||||
<FormattedMessage {...messages.column} />
|
||||
</Text>
|
||||
</Box>
|
||||
<ColumnPickerStaticColumns
|
||||
staticColumns={staticColumns}
|
||||
handleToggle={handleToggle}
|
||||
selectedColumns={selectedColumns}
|
||||
/>
|
||||
{columnCategories && (
|
||||
<ColumnPickerDynamicColumns
|
||||
dynamicColumns={dynamicColumns}
|
||||
selectedColumns={selectedColumns}
|
||||
setExpanded={setExpanded}
|
||||
handleToggle={handleToggle}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</Popover.Content>
|
||||
</Popover>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,85 @@
|
|||
import { CircularProgress } from "@material-ui/core";
|
||||
import { Box, Checkbox, Text } from "@saleor/macaw-ui/next";
|
||||
import React from "react";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
|
||||
import { ColumnPickerSearch } from "./ColumnPickerSearch";
|
||||
import messages from "./messages";
|
||||
import { ColumnCategory } from "./useColumns";
|
||||
|
||||
export interface ColumnPickerAvailableNodesProps {
|
||||
currentCategory: ColumnCategory;
|
||||
columnPickerSettings: string[];
|
||||
query: string;
|
||||
setQuery: React.Dispatch<React.SetStateAction<string>>;
|
||||
changeHandler: (column: string) => void;
|
||||
}
|
||||
|
||||
export const ColumnPickerAvailableNodes = ({
|
||||
currentCategory,
|
||||
columnPickerSettings,
|
||||
query,
|
||||
setQuery,
|
||||
changeHandler,
|
||||
}: ColumnPickerAvailableNodesProps) => {
|
||||
const areNodesLoading = currentCategory.availableNodes === undefined;
|
||||
const areNodesEmpty = currentCategory.availableNodes?.length === 0;
|
||||
|
||||
const renderNodes = () => {
|
||||
if (areNodesLoading) {
|
||||
return (
|
||||
<Box
|
||||
width="100%"
|
||||
height="100%"
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (areNodesEmpty) {
|
||||
return (
|
||||
<Text size="small" color="textNeutralSubdued">
|
||||
<FormattedMessage {...messages.noResultsFound} />
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
return currentCategory.availableNodes!.map(node => (
|
||||
<Box padding={2} key={node.id}>
|
||||
<Checkbox
|
||||
onCheckedChange={() => changeHandler(node.id)}
|
||||
checked={columnPickerSettings.includes(node.id)}
|
||||
data-test-id={`search-dynamic-${node.id}`}
|
||||
>
|
||||
<Text size="small" color="textNeutralSubdued" ellipsis>
|
||||
{node.title}
|
||||
</Text>
|
||||
</Checkbox>
|
||||
</Box>
|
||||
));
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
display="flex"
|
||||
paddingX={4}
|
||||
style={{ boxSizing: "border-box" }}
|
||||
data-test-id="search-container"
|
||||
>
|
||||
<ColumnPickerSearch
|
||||
currentCategory={currentCategory}
|
||||
query={query}
|
||||
setQuery={setQuery}
|
||||
/>
|
||||
</Box>
|
||||
<Box paddingX={5} paddingY={1.5} flexGrow="1">
|
||||
{renderNodes()}
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,99 @@
|
|||
import { Box, Button, Text } from "@saleor/macaw-ui/next";
|
||||
import React from "react";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
|
||||
import { ColumnPickerAvailableNodes } from "./ColumnPickerAvailableNodes";
|
||||
import { ColumnPickerCategoryList } from "./ColumnPickerCategoryList";
|
||||
import { ColumnPickerPagination } from "./ColumnPickerPagination";
|
||||
import messages from "./messages";
|
||||
import { useAvailableColumnsQuery } from "./useAvailableColumnsQuery";
|
||||
import { useCategorySelection } from "./useCategorySelection";
|
||||
import { ColumnCategory } from "./useColumns";
|
||||
import { getExitIcon, getExitOnClick } from "./utils";
|
||||
|
||||
export interface ColumnPickerCategoriesProps {
|
||||
columnCategories: ColumnCategory[];
|
||||
columnPickerSettings: string[];
|
||||
onClose: () => void;
|
||||
onDynamicColumnSelect: (columns: string[]) => void;
|
||||
}
|
||||
|
||||
export const ColumnPickerCategories = ({
|
||||
columnCategories,
|
||||
onClose,
|
||||
onDynamicColumnSelect,
|
||||
columnPickerSettings,
|
||||
}: ColumnPickerCategoriesProps) => {
|
||||
const { currentCategory, setCurrentCategory } =
|
||||
useCategorySelection(columnCategories);
|
||||
const { query, setQuery } = useAvailableColumnsQuery(currentCategory);
|
||||
|
||||
const changeHandler = (column: string) =>
|
||||
columnPickerSettings.includes(column)
|
||||
? onDynamicColumnSelect(
|
||||
columnPickerSettings.filter(currentCol => currentCol !== column),
|
||||
)
|
||||
: onDynamicColumnSelect([...columnPickerSettings, column]);
|
||||
|
||||
return (
|
||||
<Box
|
||||
backgroundColor="subdued"
|
||||
display="flex"
|
||||
flexDirection="column"
|
||||
justifyContent="space-between"
|
||||
__minHeight="502px"
|
||||
__width="320px"
|
||||
>
|
||||
<Box display="flex" flexDirection="column" height="100%">
|
||||
<Box
|
||||
display="flex"
|
||||
paddingX={4}
|
||||
paddingY={2}
|
||||
gap={2}
|
||||
alignItems="center"
|
||||
>
|
||||
<Button
|
||||
variant="tertiary"
|
||||
size="small"
|
||||
icon={getExitIcon(columnCategories, currentCategory)}
|
||||
onClick={getExitOnClick({
|
||||
columnCategories,
|
||||
currentCategory,
|
||||
setCurrentCategory,
|
||||
onClose,
|
||||
})}
|
||||
data-test-id="close-search"
|
||||
/>
|
||||
<Text size="small">
|
||||
{currentCategory?.name ?? (
|
||||
<FormattedMessage {...messages.categories} />
|
||||
)}
|
||||
</Text>
|
||||
</Box>
|
||||
{currentCategory ? (
|
||||
<ColumnPickerAvailableNodes
|
||||
currentCategory={currentCategory}
|
||||
columnPickerSettings={columnPickerSettings}
|
||||
query={query}
|
||||
setQuery={setQuery}
|
||||
changeHandler={changeHandler}
|
||||
/>
|
||||
) : (
|
||||
<ColumnPickerCategoryList
|
||||
columnCategories={columnCategories}
|
||||
setCurrentCategory={setCurrentCategory}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
{currentCategory && (
|
||||
<ColumnPickerPagination
|
||||
query={query}
|
||||
hasNextPage={currentCategory.hasNextPage}
|
||||
hasPreviousPage={currentCategory.hasPreviousPage}
|
||||
onNextPage={currentCategory.onNextPage}
|
||||
onPreviousPage={currentCategory.onPreviousPage}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,27 @@
|
|||
import { List, Text } from "@saleor/macaw-ui/next";
|
||||
import React from "react";
|
||||
|
||||
import { ColumnCategory } from "./useColumns";
|
||||
|
||||
export interface ColumnPickerCategoryListProps {
|
||||
columnCategories: ColumnCategory[];
|
||||
setCurrentCategory: React.Dispatch<React.SetStateAction<string | null>>;
|
||||
}
|
||||
export const ColumnPickerCategoryList = ({
|
||||
columnCategories,
|
||||
setCurrentCategory,
|
||||
}: ColumnPickerCategoryListProps) => (
|
||||
<List padding={8} data-test-id="dynamic-category-container">
|
||||
{columnCategories.map(category => (
|
||||
<List.Item
|
||||
key={category.prefix}
|
||||
padding={1.5}
|
||||
borderRadius={3}
|
||||
onClick={() => setCurrentCategory(category.name)}
|
||||
data-test-id={`dynamic-category-${category.prefix}`}
|
||||
>
|
||||
<Text size="small">{category.name}</Text>
|
||||
</List.Item>
|
||||
))}
|
||||
</List>
|
||||
);
|
|
@ -0,0 +1,61 @@
|
|||
import { Box, Button, PlusIcon, Text, Toggle } from "@saleor/macaw-ui/next";
|
||||
import React from "react";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
|
||||
import { AvailableColumn } from "../types";
|
||||
import messages from "./messages";
|
||||
|
||||
export interface ColumnPickerDynamicColumnsProps {
|
||||
dynamicColumns: AvailableColumn[] | undefined;
|
||||
setExpanded: (value: React.SetStateAction<boolean>) => void;
|
||||
handleToggle: (id: string) => void;
|
||||
selectedColumns: string[];
|
||||
}
|
||||
|
||||
export const ColumnPickerDynamicColumns = ({
|
||||
dynamicColumns,
|
||||
setExpanded,
|
||||
handleToggle,
|
||||
selectedColumns,
|
||||
}: ColumnPickerDynamicColumnsProps) => (
|
||||
<Box data-test-id="dynamic-col-container">
|
||||
<Box
|
||||
display="flex"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
marginTop={3}
|
||||
>
|
||||
<Text variant="caption" size="small" color="textNeutralSubdued">
|
||||
<FormattedMessage {...messages.custom} />
|
||||
</Text>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="small"
|
||||
icon={<PlusIcon size="small" />}
|
||||
onClick={() => setExpanded(true)}
|
||||
data-test-id="open-dynamic-search"
|
||||
/>
|
||||
</Box>
|
||||
{dynamicColumns?.map(column => (
|
||||
<Box padding={1} key={column.id}>
|
||||
<Toggle
|
||||
onPressedChange={() => handleToggle(column.id)}
|
||||
pressed={selectedColumns.includes(column.id)}
|
||||
data-test-id={`dynamic-col-${column.id}`}
|
||||
>
|
||||
<Text
|
||||
variant="body"
|
||||
size="small"
|
||||
color="textNeutralSubdued"
|
||||
whiteSpace="nowrap"
|
||||
>
|
||||
{`${column.metaGroup} /`}
|
||||
</Text>
|
||||
<Text variant="body" size="small" color="textNeutralDefault" ellipsis>
|
||||
{column.title}
|
||||
</Text>
|
||||
</Toggle>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
);
|
|
@ -0,0 +1,42 @@
|
|||
import {
|
||||
Box,
|
||||
Button,
|
||||
ChevronLeftIcon,
|
||||
ChevronRightIcon,
|
||||
} from "@saleor/macaw-ui/next";
|
||||
import React from "react";
|
||||
|
||||
export interface ColumnPickerPagination {
|
||||
query: string;
|
||||
onNextPage: (query: string) => void;
|
||||
onPreviousPage: (query: string) => void;
|
||||
hasNextPage: boolean;
|
||||
hasPreviousPage: boolean;
|
||||
}
|
||||
|
||||
export const ColumnPickerPagination = ({
|
||||
query,
|
||||
onNextPage,
|
||||
onPreviousPage,
|
||||
hasNextPage,
|
||||
hasPreviousPage,
|
||||
}: ColumnPickerPagination) => (
|
||||
<Box display="flex" gap={1.5} padding={3}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="small"
|
||||
icon={<ChevronLeftIcon size="small" />}
|
||||
onClick={() => onPreviousPage(query)}
|
||||
disabled={!hasPreviousPage}
|
||||
data-test-id="pagination-back"
|
||||
/>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="small"
|
||||
icon={<ChevronRightIcon size="small" />}
|
||||
onClick={() => onNextPage(query)}
|
||||
disabled={!hasNextPage}
|
||||
data-test-id="pagination-forward"
|
||||
/>
|
||||
</Box>
|
||||
);
|
45
src/components/Datagrid/ColumnPicker/ColumnPickerSearch.tsx
Normal file
45
src/components/Datagrid/ColumnPicker/ColumnPickerSearch.tsx
Normal file
|
@ -0,0 +1,45 @@
|
|||
import Debounce from "@dashboard/components/Debounce";
|
||||
import { SearchInput } from "@saleor/macaw-ui/next";
|
||||
import React from "react";
|
||||
import { useIntl } from "react-intl";
|
||||
|
||||
import messages from "./messages";
|
||||
import { ColumnCategory } from "./useColumns";
|
||||
|
||||
interface ColumnPickerSearchProps {
|
||||
currentCategory: ColumnCategory;
|
||||
setQuery: (value: React.SetStateAction<string>) => void;
|
||||
query: string;
|
||||
}
|
||||
|
||||
export const ColumnPickerSearch: React.FC<ColumnPickerSearchProps> = ({
|
||||
currentCategory,
|
||||
setQuery,
|
||||
query,
|
||||
}: ColumnPickerSearchProps) => {
|
||||
const intl = useIntl();
|
||||
|
||||
return (
|
||||
<Debounce
|
||||
debounceFn={(value: string) => currentCategory.onSearch(value)}
|
||||
time={500}
|
||||
>
|
||||
{debounceSearchChange => {
|
||||
const handleSearchChange = (event: React.ChangeEvent<any>) => {
|
||||
const value = event.target.value ?? "";
|
||||
setQuery(value);
|
||||
debounceSearchChange(value);
|
||||
};
|
||||
return (
|
||||
<SearchInput
|
||||
size="small"
|
||||
placeholder={intl.formatMessage(messages.searchForColumns)}
|
||||
value={query}
|
||||
onChange={handleSearchChange}
|
||||
data-test-id="search-columns"
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</Debounce>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,39 @@
|
|||
import { Box, Text, Toggle } from "@saleor/macaw-ui/next";
|
||||
import React from "react";
|
||||
|
||||
import { AvailableColumn } from "../types";
|
||||
import { filterEmptyColumn, isLastEnabledColumn } from "./utils";
|
||||
|
||||
export interface ColumnPickerStaticColumnsProps {
|
||||
staticColumns: AvailableColumn[];
|
||||
handleToggle: (id: string) => void;
|
||||
selectedColumns: string[];
|
||||
}
|
||||
|
||||
export const ColumnPickerStaticColumns = ({
|
||||
staticColumns,
|
||||
handleToggle,
|
||||
selectedColumns,
|
||||
}: ColumnPickerStaticColumnsProps) => (
|
||||
<Box data-test-id="static-col-container">
|
||||
{staticColumns.filter(filterEmptyColumn).map(column => (
|
||||
<Box padding={1} key={column.id}>
|
||||
<Toggle
|
||||
data-test-id={`stat-col-${column.id}`}
|
||||
onPressedChange={() => handleToggle(column.id)}
|
||||
pressed={selectedColumns.includes(column.id)}
|
||||
// One static column must always be enabled
|
||||
disabled={isLastEnabledColumn(
|
||||
column.id,
|
||||
staticColumns,
|
||||
selectedColumns,
|
||||
)}
|
||||
>
|
||||
<Text variant="body" size="small" color="textNeutralDefault">
|
||||
{column.title}
|
||||
</Text>
|
||||
</Toggle>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
);
|
31
src/components/Datagrid/ColumnPicker/messages.ts
Normal file
31
src/components/Datagrid/ColumnPicker/messages.ts
Normal file
|
@ -0,0 +1,31 @@
|
|||
import { defineMessages } from "react-intl";
|
||||
|
||||
const messages = defineMessages({
|
||||
column: {
|
||||
defaultMessage: "Columns",
|
||||
id: "uqxjSR",
|
||||
description: "column picker section header",
|
||||
},
|
||||
custom: {
|
||||
id: "rHQmCt",
|
||||
defaultMessage: "Custom",
|
||||
description: "column picker section header",
|
||||
},
|
||||
categories: {
|
||||
id: "4ELCCk",
|
||||
defaultMessage: "Categories",
|
||||
description: "column picker section default header",
|
||||
},
|
||||
searchForColumns: {
|
||||
id: "Xsh2Pa",
|
||||
defaultMessage: "Search for columns",
|
||||
description: "column picker search input placeholder",
|
||||
},
|
||||
noResultsFound: {
|
||||
id: "cPAc45",
|
||||
defaultMessage: "No results found",
|
||||
description: "column picker search no results message",
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
|
@ -0,0 +1,16 @@
|
|||
import React from "react";
|
||||
|
||||
import { ColumnCategory } from "./useColumns";
|
||||
|
||||
export const useAvailableColumnsQuery = (
|
||||
currentCategory: ColumnCategory | undefined,
|
||||
) => {
|
||||
const [query, setQuery] = React.useState<string>("");
|
||||
React.useEffect(() => {
|
||||
if (currentCategory) {
|
||||
setQuery(currentCategory.initialSearch ?? "");
|
||||
}
|
||||
}, [currentCategory]);
|
||||
|
||||
return { query, setQuery };
|
||||
};
|
23
src/components/Datagrid/ColumnPicker/useCategorySelection.ts
Normal file
23
src/components/Datagrid/ColumnPicker/useCategorySelection.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
import React from "react";
|
||||
|
||||
import { ColumnCategory } from "./useColumns";
|
||||
|
||||
export const useCategorySelection = (columnCategories: ColumnCategory[]) => {
|
||||
const [selectedCategory, setSelectedCategory] = React.useState<string | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
const currentCategory = React.useMemo(
|
||||
() => columnCategories.find(category => category.name === selectedCategory),
|
||||
[columnCategories, selectedCategory],
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
// Preselect category when there is only one
|
||||
if (columnCategories.length === 1) {
|
||||
setSelectedCategory(columnCategories[0].name);
|
||||
}
|
||||
}, [columnCategories]);
|
||||
|
||||
return { currentCategory, setCurrentCategory: setSelectedCategory };
|
||||
};
|
|
@ -0,0 +1,39 @@
|
|||
import useLocalStorage from "@dashboard/hooks/useLocalStorage";
|
||||
|
||||
const COLUMN_PICKER_KEY = "columnPickerConfig";
|
||||
|
||||
export type DatagridViews =
|
||||
| "PRODUCT_LIST"
|
||||
| "PRODUCT_DETAILS"
|
||||
| "ORDER_LIST"
|
||||
| "ORDER_DETAILS"
|
||||
| "ORDER_DRAFT_DETAILS";
|
||||
|
||||
type DynamicColumnSettings = {
|
||||
[view in DatagridViews]: string[];
|
||||
};
|
||||
|
||||
export const defaultDynamicColumns: DynamicColumnSettings = {
|
||||
PRODUCT_LIST: [],
|
||||
PRODUCT_DETAILS: [],
|
||||
ORDER_LIST: [],
|
||||
ORDER_DETAILS: [],
|
||||
ORDER_DRAFT_DETAILS: [],
|
||||
};
|
||||
|
||||
export const useColumnPickerSettings = (view: DatagridViews) => {
|
||||
const [config, setConfig] = useLocalStorage(
|
||||
COLUMN_PICKER_KEY,
|
||||
defaultDynamicColumns,
|
||||
);
|
||||
|
||||
const setDynamicColumnsSettings = (cols: string[]) =>
|
||||
setConfig(currentSettings => ({
|
||||
...currentSettings,
|
||||
[view]: cols,
|
||||
}));
|
||||
|
||||
const columnPickerSettings = config[view] ?? [];
|
||||
|
||||
return { columnPickerSettings, setDynamicColumnsSettings };
|
||||
};
|
364
src/components/Datagrid/ColumnPicker/useColumns.test.ts
Normal file
364
src/components/Datagrid/ColumnPicker/useColumns.test.ts
Normal file
|
@ -0,0 +1,364 @@
|
|||
import { act, renderHook } from "@testing-library/react-hooks";
|
||||
|
||||
import { AvailableColumn } from "../types";
|
||||
import { ColumnCategory, useColumns } from "./useColumns";
|
||||
|
||||
const mockedColumns: AvailableColumn[] = [
|
||||
{
|
||||
id: "name",
|
||||
title: "Name",
|
||||
width: 200,
|
||||
metaGroup: "Product",
|
||||
hasMenu: false,
|
||||
icon: "arrowUp",
|
||||
},
|
||||
{
|
||||
id: "description",
|
||||
title: "Description",
|
||||
width: 100,
|
||||
metaGroup: "Sales Information",
|
||||
hasMenu: false,
|
||||
icon: "arrowUp",
|
||||
},
|
||||
];
|
||||
|
||||
const mockedSelectedColumns = ["name", "attribute:QXR0cmlidXRlOjE0"];
|
||||
|
||||
const mockedCategories: ColumnCategory[] = [
|
||||
{
|
||||
name: "Attributes",
|
||||
prefix: "attribute",
|
||||
availableNodes: [
|
||||
{
|
||||
id: "attribute:QXR0cmlidXRlOjIx",
|
||||
title: "ABV",
|
||||
metaGroup: "Attributes",
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
id: "attribute:QXR0cmlidXRlOjI3",
|
||||
title: "Author",
|
||||
metaGroup: "Attributes",
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
id: "attribute:QXR0cmlidXRlOjE1",
|
||||
title: "Bottle Size",
|
||||
metaGroup: "Attributes",
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
id: "attribute:QXR0cmlidXRlOjE4",
|
||||
title: "Bucket Size",
|
||||
metaGroup: "Attributes",
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
id: "attribute:QXR0cmlidXRlOjE0",
|
||||
title: "Color",
|
||||
metaGroup: "Attributes",
|
||||
width: 200,
|
||||
},
|
||||
],
|
||||
selectedNodes: [
|
||||
{
|
||||
id: "attribute:QXR0cmlidXRlOjIx",
|
||||
title: "ABV",
|
||||
metaGroup: "Attributes",
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
id: "attribute:QXR0cmlidXRlOjE1",
|
||||
title: "Bottle Size",
|
||||
metaGroup: "Attributes",
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
id: "attribute:QXR0cmlidXRlOjE0",
|
||||
title: "Color",
|
||||
metaGroup: "Attributes",
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
id: "attribute:QXR0cmlidXRlOjUwOQ==",
|
||||
title: "storage size ",
|
||||
metaGroup: "Attributes",
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
id: "attribute:QXR0cmlidXRlOjI5",
|
||||
title: "Tag",
|
||||
metaGroup: "Attributes",
|
||||
width: 200,
|
||||
},
|
||||
],
|
||||
hasNextPage: false,
|
||||
hasPreviousPage: false,
|
||||
onNextPage: () => undefined,
|
||||
onPreviousPage: () => undefined,
|
||||
onSearch: () => undefined,
|
||||
},
|
||||
];
|
||||
|
||||
const mockedColumnPickerSettings = [
|
||||
"attribute:QXR0cmlidXRlOjIx",
|
||||
"attribute:QXR0cmlidXRlOjE1",
|
||||
"attribute:QXR0cmlidXRlOjUwOQ==",
|
||||
"attribute:QXR0cmlidXRlOjI5",
|
||||
"attribute:QXR0cmlidXRlOjE0",
|
||||
];
|
||||
|
||||
const expectedVisibleColumns = [
|
||||
{
|
||||
id: "name",
|
||||
title: "Name",
|
||||
width: 200,
|
||||
metaGroup: "Product",
|
||||
hasMenu: false,
|
||||
icon: "arrowUp",
|
||||
},
|
||||
{
|
||||
id: "attribute:QXR0cmlidXRlOjE0",
|
||||
title: "Color",
|
||||
metaGroup: "Attributes",
|
||||
width: 200,
|
||||
},
|
||||
];
|
||||
|
||||
// In order of mockedColumnPickerSettings
|
||||
const expectedDynamicColumns = [
|
||||
{
|
||||
id: "attribute:QXR0cmlidXRlOjIx",
|
||||
title: "ABV",
|
||||
metaGroup: "Attributes",
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
id: "attribute:QXR0cmlidXRlOjE1",
|
||||
title: "Bottle Size",
|
||||
metaGroup: "Attributes",
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
id: "attribute:QXR0cmlidXRlOjUwOQ==",
|
||||
title: "storage size ",
|
||||
metaGroup: "Attributes",
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
id: "attribute:QXR0cmlidXRlOjI5",
|
||||
title: "Tag",
|
||||
metaGroup: "Attributes",
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
id: "attribute:QXR0cmlidXRlOjE0",
|
||||
title: "Color",
|
||||
metaGroup: "Attributes",
|
||||
width: 200,
|
||||
},
|
||||
];
|
||||
|
||||
const setDynamicColumnSettings = jest.fn();
|
||||
const onSave = jest.fn();
|
||||
|
||||
describe("useColumns", () => {
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
it("should return initial state", () => {
|
||||
// Arrange
|
||||
const { result } = renderHook(() =>
|
||||
useColumns({
|
||||
staticColumns: mockedColumns,
|
||||
selectedColumns: mockedSelectedColumns,
|
||||
columnCategories: mockedCategories,
|
||||
onSave,
|
||||
columnPickerSettings: mockedColumnPickerSettings,
|
||||
setDynamicColumnSettings,
|
||||
}),
|
||||
);
|
||||
// Assert
|
||||
expect(result.current.visibleColumns).toEqual(expectedVisibleColumns);
|
||||
expect(result.current.staticColumns).toEqual(mockedColumns);
|
||||
expect(result.current.dynamicColumns).toEqual(expectedDynamicColumns);
|
||||
expect(result.current.selectedColumns).toEqual(mockedSelectedColumns);
|
||||
expect(result.current.columnCategories).toEqual(mockedCategories);
|
||||
expect(result.current.columnPickerSettings).toEqual(
|
||||
mockedColumnPickerSettings,
|
||||
);
|
||||
});
|
||||
it("should update visible column info when resized", () => {
|
||||
// Arrange
|
||||
const { result } = renderHook(() =>
|
||||
useColumns({
|
||||
staticColumns: mockedColumns,
|
||||
selectedColumns: mockedSelectedColumns,
|
||||
columnCategories: mockedCategories,
|
||||
onSave,
|
||||
columnPickerSettings: mockedColumnPickerSettings,
|
||||
setDynamicColumnSettings,
|
||||
}),
|
||||
);
|
||||
|
||||
// Act
|
||||
act(() =>
|
||||
result.current.handlers.onResize({ id: "name", title: "Name" }, 150),
|
||||
);
|
||||
|
||||
// Assert
|
||||
expect(result.current.visibleColumns[0].width).toEqual(150);
|
||||
});
|
||||
it("should rearrange columns when moved", () => {
|
||||
// Arrange
|
||||
const { result } = renderHook(() =>
|
||||
useColumns({
|
||||
staticColumns: mockedColumns,
|
||||
selectedColumns: mockedSelectedColumns,
|
||||
columnCategories: mockedCategories,
|
||||
onSave,
|
||||
columnPickerSettings: mockedColumnPickerSettings,
|
||||
setDynamicColumnSettings,
|
||||
}),
|
||||
);
|
||||
|
||||
// Act
|
||||
act(() => result.current.handlers.onMove(1, 0));
|
||||
|
||||
// Assert
|
||||
expect(result.current.visibleColumns).toEqual(
|
||||
expectedVisibleColumns.reverse(),
|
||||
);
|
||||
});
|
||||
it("should call onSave when column is changed", () => {
|
||||
// Arrange
|
||||
const { result } = renderHook(() =>
|
||||
useColumns({
|
||||
staticColumns: mockedColumns,
|
||||
selectedColumns: mockedSelectedColumns,
|
||||
columnCategories: mockedCategories,
|
||||
onSave,
|
||||
columnPickerSettings: mockedColumnPickerSettings,
|
||||
setDynamicColumnSettings,
|
||||
}),
|
||||
);
|
||||
|
||||
// Act
|
||||
act(() => result.current.handlers.onChange(["name"]));
|
||||
|
||||
// Assert
|
||||
expect(onSave).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
it("should set recentlyAddedColumn when column is added", () => {
|
||||
// Arrange
|
||||
const { result } = renderHook(() =>
|
||||
useColumns({
|
||||
staticColumns: mockedColumns,
|
||||
selectedColumns: mockedSelectedColumns,
|
||||
columnCategories: mockedCategories,
|
||||
onSave,
|
||||
columnPickerSettings: mockedColumnPickerSettings,
|
||||
setDynamicColumnSettings,
|
||||
}),
|
||||
);
|
||||
|
||||
// Act
|
||||
act(() =>
|
||||
result.current.handlers.onChange([
|
||||
"name",
|
||||
"attribute:QXR0cmlidXRlOjE0",
|
||||
"attribute:QXR0cmlidXRlOjIx",
|
||||
]),
|
||||
);
|
||||
|
||||
// Assert
|
||||
expect(result.current.recentlyAddedColumn).toEqual(
|
||||
"attribute:QXR0cmlidXRlOjIx",
|
||||
);
|
||||
});
|
||||
it("should update dynamic columns when new column is picked", () => {
|
||||
// Arrange
|
||||
const { result } = renderHook(() =>
|
||||
useColumns({
|
||||
staticColumns: mockedColumns,
|
||||
selectedColumns: mockedSelectedColumns,
|
||||
columnCategories: mockedCategories,
|
||||
onSave,
|
||||
columnPickerSettings: mockedColumnPickerSettings,
|
||||
setDynamicColumnSettings,
|
||||
}),
|
||||
);
|
||||
|
||||
// Act
|
||||
act(() =>
|
||||
result.current.handlers.onDynamicColumnSelect([
|
||||
...mockedColumnPickerSettings,
|
||||
"attribute:QXR0cmlidXRlOjI3",
|
||||
]),
|
||||
);
|
||||
|
||||
// Assert
|
||||
expect(setDynamicColumnSettings).toHaveBeenCalledTimes(1);
|
||||
expect(result.current.dynamicColumns).toEqual([
|
||||
...expectedDynamicColumns,
|
||||
{
|
||||
id: "attribute:QXR0cmlidXRlOjI3",
|
||||
title: "Author",
|
||||
metaGroup: "Attributes",
|
||||
width: 200,
|
||||
},
|
||||
]);
|
||||
});
|
||||
it("should update dynamic columns when column is removed", () => {
|
||||
// Arrange
|
||||
const { result } = renderHook(() =>
|
||||
useColumns({
|
||||
staticColumns: mockedColumns,
|
||||
selectedColumns: mockedSelectedColumns,
|
||||
columnCategories: mockedCategories,
|
||||
onSave,
|
||||
columnPickerSettings: mockedColumnPickerSettings,
|
||||
setDynamicColumnSettings,
|
||||
}),
|
||||
);
|
||||
|
||||
// Act
|
||||
act(() =>
|
||||
result.current.handlers.onDynamicColumnSelect([
|
||||
"attribute:QXR0cmlidXRlOjIx",
|
||||
"attribute:QXR0cmlidXRlOjE1",
|
||||
"attribute:QXR0cmlidXRlOjI5",
|
||||
"attribute:QXR0cmlidXRlOjE0",
|
||||
]),
|
||||
);
|
||||
|
||||
// Assert
|
||||
expect(setDynamicColumnSettings).toHaveBeenCalledTimes(1);
|
||||
expect(result.current.dynamicColumns).toEqual([
|
||||
{
|
||||
id: "attribute:QXR0cmlidXRlOjIx",
|
||||
title: "ABV",
|
||||
metaGroup: "Attributes",
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
id: "attribute:QXR0cmlidXRlOjE1",
|
||||
title: "Bottle Size",
|
||||
metaGroup: "Attributes",
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
id: "attribute:QXR0cmlidXRlOjI5",
|
||||
title: "Tag",
|
||||
metaGroup: "Attributes",
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
id: "attribute:QXR0cmlidXRlOjE0",
|
||||
title: "Color",
|
||||
metaGroup: "Attributes",
|
||||
width: 200,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
174
src/components/Datagrid/ColumnPicker/useColumns.ts
Normal file
174
src/components/Datagrid/ColumnPicker/useColumns.ts
Normal file
|
@ -0,0 +1,174 @@
|
|||
// @ts-strict-ignore
|
||||
import useStateFromProps from "@dashboard/hooks/useStateFromProps";
|
||||
import { addAtIndex, removeAtIndex } from "@dashboard/utils/lists";
|
||||
import { GridColumn } from "@glideapps/glide-data-grid";
|
||||
import difference from "lodash/difference";
|
||||
import React from "react";
|
||||
|
||||
import { AvailableColumn } from "../types";
|
||||
import {
|
||||
areCategoriesLoaded,
|
||||
extractAvailableNodesFromCategories,
|
||||
extractSelectedNodesFromCategories,
|
||||
filterSelectedColumns,
|
||||
mergeCurrentDynamicColumnsWithCandidates,
|
||||
mergeSelectedColumns,
|
||||
sortColumns,
|
||||
} from "./utils";
|
||||
|
||||
export interface ColumnCategory {
|
||||
name: string;
|
||||
prefix: string;
|
||||
availableNodes: AvailableColumn[] | undefined;
|
||||
selectedNodes: AvailableColumn[] | undefined;
|
||||
initialSearch?: string;
|
||||
onSearch: (query: string) => void;
|
||||
hasNextPage: boolean;
|
||||
hasPreviousPage: boolean;
|
||||
onNextPage: (query: string) => void;
|
||||
onPreviousPage: (query: string) => void;
|
||||
}
|
||||
|
||||
export interface UseColumnsProps {
|
||||
staticColumns: AvailableColumn[];
|
||||
columnCategories?: ColumnCategory[];
|
||||
selectedColumns: string[];
|
||||
onSave: (columns: string[]) => void;
|
||||
columnPickerSettings?: string[];
|
||||
setDynamicColumnSettings?: (cols: string[]) => void;
|
||||
}
|
||||
|
||||
export const useColumns = ({
|
||||
staticColumns,
|
||||
selectedColumns,
|
||||
columnCategories,
|
||||
onSave,
|
||||
columnPickerSettings,
|
||||
setDynamicColumnSettings,
|
||||
}: UseColumnsProps) => {
|
||||
const [dynamicColumns, updateDynamicColumns] = React.useState<
|
||||
AvailableColumn[] | null | undefined
|
||||
>(null);
|
||||
|
||||
// Dynamic columns are loaded from the API, thus they need to be updated
|
||||
// after query resolves with data. Then we also sort them by order of addition
|
||||
// by the user, which is saved in LS (columnPickerSettings).
|
||||
React.useEffect(() => {
|
||||
if (dynamicColumns === null && areCategoriesLoaded(columnCategories)) {
|
||||
updateDynamicColumns(
|
||||
sortColumns(
|
||||
extractSelectedNodesFromCategories(columnCategories),
|
||||
columnPickerSettings,
|
||||
),
|
||||
);
|
||||
}
|
||||
}, [columnCategories, columnPickerSettings, dynamicColumns]);
|
||||
|
||||
const initialColumnsState = React.useMemo(
|
||||
() =>
|
||||
mergeSelectedColumns({ staticColumns, dynamicColumns, selectedColumns }),
|
||||
[dynamicColumns, staticColumns, selectedColumns],
|
||||
);
|
||||
|
||||
const [recentlyAddedColumn, setRecentlyAddedColumn] = React.useState<
|
||||
string | null
|
||||
>(null);
|
||||
|
||||
const [visibleColumns, setVisibleColumns] =
|
||||
useStateFromProps(initialColumnsState);
|
||||
|
||||
const onMove = React.useCallback(
|
||||
(startIndex: number, endIndex: number): void => {
|
||||
// When empty column prevent to rearrange it order
|
||||
if (visibleColumns[0]?.id === "empty") {
|
||||
if (startIndex === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Keep empty column always at beginning
|
||||
if (endIndex === 0) {
|
||||
return setVisibleColumns(old =>
|
||||
addAtIndex(
|
||||
old[startIndex],
|
||||
removeAtIndex(old, startIndex),
|
||||
endIndex + 1,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
setVisibleColumns(old =>
|
||||
addAtIndex(old[startIndex], removeAtIndex(old, startIndex), endIndex),
|
||||
);
|
||||
},
|
||||
[visibleColumns, setVisibleColumns],
|
||||
);
|
||||
|
||||
const onResize = React.useCallback(
|
||||
(column: GridColumn, newSize: number) => {
|
||||
if (column.id === "empty") {
|
||||
return;
|
||||
}
|
||||
return setVisibleColumns(prevColumns =>
|
||||
prevColumns.map(prevColumn =>
|
||||
prevColumn.id === column.id
|
||||
? { ...prevColumn, width: newSize }
|
||||
: prevColumn,
|
||||
),
|
||||
);
|
||||
},
|
||||
[setVisibleColumns],
|
||||
);
|
||||
|
||||
const onChange = (columns: string[]) => {
|
||||
// Recently added is used by datagrid to auto-scroll to the column
|
||||
setRecentlyAddedColumn(difference(columns, selectedColumns)[0]);
|
||||
// Saves in LS
|
||||
onSave(columns);
|
||||
};
|
||||
|
||||
// Should be used only for special cases
|
||||
const onCustomUpdateVisible = setVisibleColumns;
|
||||
|
||||
const onDynamicColumnSelect = (selected: string[]) => {
|
||||
if (typeof setDynamicColumnSettings !== "function") {
|
||||
return;
|
||||
}
|
||||
|
||||
// This is optimistic update - dynamic columns are only synced
|
||||
// with the API on the initial render
|
||||
setDynamicColumnSettings(selected);
|
||||
updateDynamicColumns(prevDynamicColumns =>
|
||||
filterSelectedColumns(
|
||||
sortColumns(
|
||||
mergeCurrentDynamicColumnsWithCandidates(
|
||||
prevDynamicColumns,
|
||||
filterSelectedColumns(
|
||||
extractAvailableNodesFromCategories(columnCategories),
|
||||
selected,
|
||||
),
|
||||
),
|
||||
selected,
|
||||
),
|
||||
selected,
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
return {
|
||||
handlers: {
|
||||
onMove,
|
||||
onResize,
|
||||
onChange,
|
||||
onDynamicColumnSelect,
|
||||
onCustomUpdateVisible,
|
||||
},
|
||||
visibleColumns,
|
||||
staticColumns,
|
||||
dynamicColumns,
|
||||
selectedColumns,
|
||||
columnCategories,
|
||||
columnPickerSettings,
|
||||
recentlyAddedColumn,
|
||||
};
|
||||
};
|
95
src/components/Datagrid/ColumnPicker/utils.tsx
Normal file
95
src/components/Datagrid/ColumnPicker/utils.tsx
Normal file
|
@ -0,0 +1,95 @@
|
|||
import { ArrowLeftIcon, CloseIcon } from "@saleor/macaw-ui/next";
|
||||
import uniqBy from "lodash/uniqBy";
|
||||
import React, { Dispatch, SetStateAction } from "react";
|
||||
|
||||
import { AvailableColumn } from "../types";
|
||||
import { ColumnCategory } from "./useColumns";
|
||||
|
||||
export const filterEmptyColumn = (column: AvailableColumn) =>
|
||||
column.title !== "";
|
||||
|
||||
export const getExitIcon = (
|
||||
columnCategories: ColumnCategory[],
|
||||
currentCategory: ColumnCategory | undefined,
|
||||
) => {
|
||||
if (columnCategories.length === 1) {
|
||||
return <CloseIcon />;
|
||||
}
|
||||
if (currentCategory) {
|
||||
return <ArrowLeftIcon />;
|
||||
}
|
||||
return <CloseIcon />;
|
||||
};
|
||||
|
||||
export const getExitOnClick = ({
|
||||
columnCategories,
|
||||
currentCategory,
|
||||
setCurrentCategory,
|
||||
onClose,
|
||||
}: {
|
||||
columnCategories: ColumnCategory[];
|
||||
currentCategory: ColumnCategory | undefined;
|
||||
setCurrentCategory: Dispatch<SetStateAction<string | null>>;
|
||||
onClose: () => void;
|
||||
}) => {
|
||||
if (columnCategories?.length === 1) {
|
||||
return onClose;
|
||||
}
|
||||
if (currentCategory) {
|
||||
return () => setCurrentCategory(null);
|
||||
} else {
|
||||
return onClose;
|
||||
}
|
||||
};
|
||||
|
||||
export const isLastEnabledColumn = (
|
||||
columnIdToCheck: string,
|
||||
columnsToCheck: AvailableColumn[],
|
||||
selectedColumns: string[],
|
||||
): boolean => {
|
||||
const enabledColumns = columnsToCheck.filter(column =>
|
||||
selectedColumns.includes(column.id),
|
||||
);
|
||||
return (
|
||||
enabledColumns.length === 1 && enabledColumns[0].id === columnIdToCheck
|
||||
);
|
||||
};
|
||||
|
||||
export const sortColumns = (
|
||||
columns: AvailableColumn[] | undefined,
|
||||
order: string[],
|
||||
) => columns?.sort((a, b) => order.indexOf(a.id) - order.indexOf(b.id));
|
||||
|
||||
export const filterSelectedColumns = (
|
||||
columns: AvailableColumn[] | undefined,
|
||||
selected: string[],
|
||||
) => columns?.filter(column => selected.includes(column.id));
|
||||
|
||||
export const areCategoriesLoaded = (categories: ColumnCategory[] | undefined) =>
|
||||
categories?.every(category => Array.isArray(category.selectedNodes));
|
||||
|
||||
export const extractSelectedNodesFromCategories = (
|
||||
categories: ColumnCategory[] | undefined,
|
||||
) => categories?.flatMap(category => category.selectedNodes);
|
||||
|
||||
export const extractAvailableNodesFromCategories = (
|
||||
categories: ColumnCategory[] | undefined,
|
||||
) => categories?.flatMap(category => category.availableNodes);
|
||||
|
||||
export const mergeSelectedColumns = ({
|
||||
staticColumns,
|
||||
dynamicColumns,
|
||||
selectedColumns,
|
||||
}: {
|
||||
staticColumns: AvailableColumn[];
|
||||
dynamicColumns: AvailableColumn[];
|
||||
selectedColumns: string[];
|
||||
}) =>
|
||||
[...staticColumns, ...(dynamicColumns ?? [])].filter(
|
||||
column => selectedColumns.includes(column.id) || column.id === "empty",
|
||||
);
|
||||
|
||||
export const mergeCurrentDynamicColumnsWithCandidates = (
|
||||
dynamicColumns: AvailableColumn[] | undefined,
|
||||
candidates: AvailableColumn[],
|
||||
) => uniqBy([...(dynamicColumns ?? []), ...candidates], "id");
|
|
@ -104,6 +104,7 @@ export interface DatagridProps {
|
|||
columnSelect?: DataEditorProps["columnSelect"];
|
||||
showEmptyDatagrid?: boolean;
|
||||
rowAnchor?: (item: Item) => string;
|
||||
recentlyAddedColumn?: string; // Enables scroll to recently added column
|
||||
}
|
||||
|
||||
export const Datagrid: React.FC<DatagridProps> = ({
|
||||
|
@ -134,6 +135,7 @@ export const Datagrid: React.FC<DatagridProps> = ({
|
|||
rowAnchor,
|
||||
hasRowHover = false,
|
||||
onRowSelectionChange,
|
||||
recentlyAddedColumn,
|
||||
...datagridProps
|
||||
}): ReactElement => {
|
||||
const classes = useStyles();
|
||||
|
@ -170,6 +172,16 @@ export const Datagrid: React.FC<DatagridProps> = ({
|
|||
}
|
||||
}, [onRowSelectionChange, selection]);
|
||||
|
||||
useEffect(() => {
|
||||
if (recentlyAddedColumn && editor.current) {
|
||||
const columnIndex = availableColumns.findIndex(
|
||||
column => column.id === recentlyAddedColumn,
|
||||
);
|
||||
const datagridScroll = editor.current.scrollTo;
|
||||
datagridScroll(columnIndex, 0, "horizontal", 0, 0, { hAlign: "start" });
|
||||
}
|
||||
}, [recentlyAddedColumn, availableColumns, editor]);
|
||||
|
||||
usePortalClasses({ className: classes.portal });
|
||||
usePreventHistoryBack(scroller);
|
||||
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import { useTheme } from "@saleor/macaw-ui/next";
|
||||
|
||||
export const useEmptyColumn = () => {
|
||||
import { AvailableColumn } from "../types";
|
||||
|
||||
export const useEmptyColumn = (): AvailableColumn => {
|
||||
const { themeValues } = useTheme();
|
||||
|
||||
return {
|
||||
|
|
|
@ -5,6 +5,7 @@ export interface AvailableColumn {
|
|||
title: string;
|
||||
width: number;
|
||||
group?: string;
|
||||
metaGroup?: string; // Description for column picker
|
||||
hasMenu?: boolean;
|
||||
icon?: string;
|
||||
themeOverride?: Partial<Theme>;
|
||||
|
|
|
@ -189,7 +189,6 @@ export const DocumentMap: Record<string, Document> = {
|
|||
variables: {
|
||||
first: 100,
|
||||
hasChannel: true,
|
||||
hasSelectedAttributes: true,
|
||||
},
|
||||
displayedAttribute: "name",
|
||||
},
|
||||
|
|
|
@ -33,6 +33,7 @@ export const PAGINATE_BY = 20;
|
|||
export const VALUES_PAGINATE_BY = 10;
|
||||
|
||||
export type ProductListColumns =
|
||||
| "name"
|
||||
| "productType"
|
||||
| "description"
|
||||
| "availability"
|
||||
|
@ -86,6 +87,7 @@ export const defaultListSettings: AppListViewSettings = {
|
|||
},
|
||||
[ListViews.ORDER_LIST]: {
|
||||
rowNumber: PAGINATE_BY,
|
||||
columns: ["number", "date", "customer", "payment", "status", "total"],
|
||||
},
|
||||
[ListViews.PAGES_LIST]: {
|
||||
rowNumber: PAGINATE_BY,
|
||||
|
@ -94,7 +96,14 @@ export const defaultListSettings: AppListViewSettings = {
|
|||
rowNumber: PAGINATE_BY,
|
||||
},
|
||||
[ListViews.PRODUCT_LIST]: {
|
||||
columns: ["availability", "description", "price", "productType", "date"],
|
||||
columns: [
|
||||
"name",
|
||||
"availability",
|
||||
"description",
|
||||
"price",
|
||||
"productType",
|
||||
"date",
|
||||
],
|
||||
rowNumber: PAGINATE_BY,
|
||||
},
|
||||
[ListViews.SALES_LIST]: {
|
||||
|
|
|
@ -13367,7 +13367,7 @@ export type InitialProductFilterProductTypesQueryHookResult = ReturnType<typeof
|
|||
export type InitialProductFilterProductTypesLazyQueryHookResult = ReturnType<typeof useInitialProductFilterProductTypesLazyQuery>;
|
||||
export type InitialProductFilterProductTypesQueryResult = Apollo.QueryResult<Types.InitialProductFilterProductTypesQuery, Types.InitialProductFilterProductTypesQueryVariables>;
|
||||
export const ProductListDocument = gql`
|
||||
query ProductList($first: Int, $after: String, $last: Int, $before: String, $filter: ProductFilterInput, $channel: String, $sort: ProductOrder, $hasChannel: Boolean!, $hasSelectedAttributes: Boolean!) {
|
||||
query ProductList($first: Int, $after: String, $last: Int, $before: String, $filter: ProductFilterInput, $channel: String, $sort: ProductOrder, $hasChannel: Boolean!) {
|
||||
products(
|
||||
before: $before
|
||||
after: $after
|
||||
|
@ -13382,7 +13382,7 @@ export const ProductListDocument = gql`
|
|||
...ProductWithChannelListings
|
||||
updatedAt
|
||||
description
|
||||
attributes @include(if: $hasSelectedAttributes) {
|
||||
attributes {
|
||||
...ProductListAttribute
|
||||
}
|
||||
}
|
||||
|
@ -13419,7 +13419,6 @@ ${ProductListAttributeFragmentDoc}`;
|
|||
* channel: // value for 'channel'
|
||||
* sort: // value for 'sort'
|
||||
* hasChannel: // value for 'hasChannel'
|
||||
* hasSelectedAttributes: // value for 'hasSelectedAttributes'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
|
@ -13746,8 +13745,19 @@ export type ProductMediaByIdQueryHookResult = ReturnType<typeof useProductMediaB
|
|||
export type ProductMediaByIdLazyQueryHookResult = ReturnType<typeof useProductMediaByIdLazyQuery>;
|
||||
export type ProductMediaByIdQueryResult = Apollo.QueryResult<Types.ProductMediaByIdQuery, Types.ProductMediaByIdQueryVariables>;
|
||||
export const GridAttributesDocument = gql`
|
||||
query GridAttributes($ids: [ID!]!) {
|
||||
grid: attributes(first: 25, filter: {ids: $ids}) {
|
||||
query GridAttributes($ids: [ID!]!, $hasAttributes: Boolean!) {
|
||||
availableAttributes: attributes(first: 10) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
pageInfo {
|
||||
...PageInfo
|
||||
}
|
||||
}
|
||||
selectedAttributes: attributes(first: 25, filter: {ids: $ids}) @include(if: $hasAttributes) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
|
@ -13756,7 +13766,7 @@ export const GridAttributesDocument = gql`
|
|||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
${PageInfoFragmentDoc}`;
|
||||
|
||||
/**
|
||||
* __useGridAttributesQuery__
|
||||
|
@ -13771,6 +13781,7 @@ export const GridAttributesDocument = gql`
|
|||
* const { data, loading, error } = useGridAttributesQuery({
|
||||
* variables: {
|
||||
* ids: // value for 'ids'
|
||||
* hasAttributes: // value for 'hasAttributes'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
|
@ -13785,6 +13796,59 @@ export function useGridAttributesLazyQuery(baseOptions?: ApolloReactHooks.LazyQu
|
|||
export type GridAttributesQueryHookResult = ReturnType<typeof useGridAttributesQuery>;
|
||||
export type GridAttributesLazyQueryHookResult = ReturnType<typeof useGridAttributesLazyQuery>;
|
||||
export type GridAttributesQueryResult = Apollo.QueryResult<Types.GridAttributesQuery, Types.GridAttributesQueryVariables>;
|
||||
export const AvailableColumnAttributesDocument = 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
|
||||
}
|
||||
}
|
||||
}
|
||||
${PageInfoFragmentDoc}`;
|
||||
|
||||
/**
|
||||
* __useAvailableColumnAttributesQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useAvailableColumnAttributesQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useAvailableColumnAttributesQuery` returns an object from Apollo Client that contains loading, error, and data properties
|
||||
* you can use to render your UI.
|
||||
*
|
||||
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
|
||||
*
|
||||
* @example
|
||||
* const { data, loading, error } = useAvailableColumnAttributesQuery({
|
||||
* variables: {
|
||||
* search: // value for 'search'
|
||||
* before: // value for 'before'
|
||||
* after: // value for 'after'
|
||||
* first: // value for 'first'
|
||||
* last: // value for 'last'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useAvailableColumnAttributesQuery(baseOptions: ApolloReactHooks.QueryHookOptions<Types.AvailableColumnAttributesQuery, Types.AvailableColumnAttributesQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return ApolloReactHooks.useQuery<Types.AvailableColumnAttributesQuery, Types.AvailableColumnAttributesQueryVariables>(AvailableColumnAttributesDocument, options);
|
||||
}
|
||||
export function useAvailableColumnAttributesLazyQuery(baseOptions?: ApolloReactHooks.LazyQueryHookOptions<Types.AvailableColumnAttributesQuery, Types.AvailableColumnAttributesQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return ApolloReactHooks.useLazyQuery<Types.AvailableColumnAttributesQuery, Types.AvailableColumnAttributesQueryVariables>(AvailableColumnAttributesDocument, options);
|
||||
}
|
||||
export type AvailableColumnAttributesQueryHookResult = ReturnType<typeof useAvailableColumnAttributesQuery>;
|
||||
export type AvailableColumnAttributesLazyQueryHookResult = ReturnType<typeof useAvailableColumnAttributesLazyQuery>;
|
||||
export type AvailableColumnAttributesQueryResult = Apollo.QueryResult<Types.AvailableColumnAttributesQuery, Types.AvailableColumnAttributesQueryVariables>;
|
||||
export const SearchAttributesDocument = gql`
|
||||
query SearchAttributes($after: String, $first: Int!, $query: String!) {
|
||||
search: attributes(after: $after, first: $first, filter: {search: $query}) {
|
||||
|
|
|
@ -10339,11 +10339,10 @@ export type ProductListQueryVariables = Exact<{
|
|||
channel?: InputMaybe<Scalars['String']>;
|
||||
sort?: InputMaybe<ProductOrder>;
|
||||
hasChannel: Scalars['Boolean'];
|
||||
hasSelectedAttributes: Scalars['Boolean'];
|
||||
}>;
|
||||
|
||||
|
||||
export type ProductListQuery = { __typename: 'Query', products: { __typename: 'ProductCountableConnection', totalCount: number | null, edges: Array<{ __typename: 'ProductCountableEdge', node: { __typename: 'Product', updatedAt: any, description: any | null, id: string, name: string, attributes?: Array<{ __typename: 'SelectedAttribute', attribute: { __typename: 'Attribute', id: string }, values: Array<{ __typename: 'AttributeValue', id: string, name: string | null, slug: string | null, reference: string | null, boolean: boolean | null, date: any | null, dateTime: any | null, value: string | null, file: { __typename: 'File', url: string, contentType: string | null } | null }> }>, thumbnail: { __typename: 'Image', url: string } | null, productType: { __typename: 'ProductType', id: string, name: string, hasVariants: boolean }, channelListings: Array<{ __typename: 'ProductChannelListing', isPublished: boolean, publicationDate: any | null, isAvailableForPurchase: boolean | null, availableForPurchase: any | null, visibleInListings: boolean, pricing?: { __typename: 'ProductPricingInfo', priceRange: { __typename: 'TaxedMoneyRange', start: { __typename: 'TaxedMoney', net: { __typename: 'Money', amount: number, currency: string } } | null, stop: { __typename: 'TaxedMoney', net: { __typename: 'Money', amount: number, currency: string } } | null } | null } | null, channel: { __typename: 'Channel', id: string, name: string, currencyCode: string } }> | null } }>, pageInfo: { __typename: 'PageInfo', hasPreviousPage: boolean, hasNextPage: boolean, startCursor: string | null, endCursor: string | null } } | null };
|
||||
export type ProductListQuery = { __typename: 'Query', products: { __typename: 'ProductCountableConnection', totalCount: number | null, edges: Array<{ __typename: 'ProductCountableEdge', node: { __typename: 'Product', updatedAt: any, description: any | null, id: string, name: string, attributes: Array<{ __typename: 'SelectedAttribute', attribute: { __typename: 'Attribute', id: string }, values: Array<{ __typename: 'AttributeValue', id: string, name: string | null, slug: string | null, reference: string | null, boolean: boolean | null, date: any | null, dateTime: any | null, value: string | null, file: { __typename: 'File', url: string, contentType: string | null } | null }> }>, thumbnail: { __typename: 'Image', url: string } | null, productType: { __typename: 'ProductType', id: string, name: string, hasVariants: boolean }, channelListings: Array<{ __typename: 'ProductChannelListing', isPublished: boolean, publicationDate: any | null, isAvailableForPurchase: boolean | null, availableForPurchase: any | null, visibleInListings: boolean, pricing?: { __typename: 'ProductPricingInfo', priceRange: { __typename: 'TaxedMoneyRange', start: { __typename: 'TaxedMoney', net: { __typename: 'Money', amount: number, currency: string } } | null, stop: { __typename: 'TaxedMoney', net: { __typename: 'Money', amount: number, currency: string } } | null } | null } | null, channel: { __typename: 'Channel', id: string, name: string, currencyCode: string } }> | null } }>, pageInfo: { __typename: 'PageInfo', hasPreviousPage: boolean, hasNextPage: boolean, startCursor: string | null, endCursor: string | null } } | null };
|
||||
|
||||
export type ProductCountQueryVariables = Exact<{
|
||||
filter?: InputMaybe<ProductFilterInput>;
|
||||
|
@ -10408,10 +10407,22 @@ export type ProductMediaByIdQuery = { __typename: 'Query', product: { __typename
|
|||
|
||||
export type GridAttributesQueryVariables = Exact<{
|
||||
ids: Array<Scalars['ID']> | Scalars['ID'];
|
||||
hasAttributes: Scalars['Boolean'];
|
||||
}>;
|
||||
|
||||
|
||||
export type GridAttributesQuery = { __typename: 'Query', grid: { __typename: 'AttributeCountableConnection', edges: Array<{ __typename: 'AttributeCountableEdge', node: { __typename: 'Attribute', id: string, name: string | null } }> } | null };
|
||||
export type GridAttributesQuery = { __typename: 'Query', availableAttributes: { __typename: 'AttributeCountableConnection', edges: Array<{ __typename: 'AttributeCountableEdge', node: { __typename: 'Attribute', id: string, name: string | null } }>, pageInfo: { __typename: 'PageInfo', endCursor: string | null, hasNextPage: boolean, hasPreviousPage: boolean, startCursor: string | null } } | null, selectedAttributes: { __typename: 'AttributeCountableConnection', edges: Array<{ __typename: 'AttributeCountableEdge', node: { __typename: 'Attribute', id: string, name: string | null } }> } | null };
|
||||
|
||||
export type AvailableColumnAttributesQueryVariables = Exact<{
|
||||
search: Scalars['String'];
|
||||
before?: InputMaybe<Scalars['String']>;
|
||||
after?: InputMaybe<Scalars['String']>;
|
||||
first?: InputMaybe<Scalars['Int']>;
|
||||
last?: InputMaybe<Scalars['Int']>;
|
||||
}>;
|
||||
|
||||
|
||||
export type AvailableColumnAttributesQuery = { __typename: 'Query', attributes: { __typename: 'AttributeCountableConnection', edges: Array<{ __typename: 'AttributeCountableEdge', node: { __typename: 'Attribute', id: string, name: string | null } }>, pageInfo: { __typename: 'PageInfo', endCursor: string | null, hasNextPage: boolean, hasPreviousPage: boolean, startCursor: string | null } } | null };
|
||||
|
||||
export type SearchAttributesQueryVariables = Exact<{
|
||||
after?: InputMaybe<Scalars['String']>;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
// @ts-strict-ignore
|
||||
import useLocalStorage from "@dashboard/hooks/useLocalStorage";
|
||||
import merge from "lodash/merge";
|
||||
import mergeWith from "lodash/mergeWith";
|
||||
|
||||
import { AppListViewSettings, defaultListSettings } from "./../config";
|
||||
import { ListSettings, ListViews } from "./../types";
|
||||
|
@ -13,6 +13,21 @@ export interface UseListSettings<TColumns extends string = string> {
|
|||
value: ListSettings<TColumns>[T],
|
||||
) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* This customizer is used to keep state of the columns field
|
||||
* consistent in the list settings. Deep merge is used to update
|
||||
* settigns with defaults when they are missing in the LS, but
|
||||
* we want to avoid updating columns array to default when
|
||||
* they are explicitly set by a user to array which doesn't
|
||||
* contain all default values.
|
||||
*/
|
||||
const mergeCustomizer = (objValue: unknown, srcValue: unknown) => {
|
||||
if (Array.isArray(objValue) && Array.isArray(srcValue)) {
|
||||
return srcValue;
|
||||
}
|
||||
};
|
||||
|
||||
export default function useListSettings<TColumns extends string = string>(
|
||||
listName: ListViews,
|
||||
): UseListSettings<TColumns> {
|
||||
|
@ -23,7 +38,12 @@ export default function useListSettings<TColumns extends string = string>(
|
|||
return defaultListSettings;
|
||||
}
|
||||
|
||||
return merge({}, defaultListSettings, storedListSettings);
|
||||
return mergeWith(
|
||||
{},
|
||||
defaultListSettings,
|
||||
storedListSettings,
|
||||
mergeCustomizer,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
|
|
|
@ -151,7 +151,7 @@ export const OrderChangeWarehouseDialog: React.FC<
|
|||
</DialogContent>
|
||||
</ScrollShadow>
|
||||
|
||||
<DialogTable ref={setAnchor}>
|
||||
<DialogTable ref={setAnchor} css>
|
||||
{filteredWarehouses ? (
|
||||
<RadioGroup
|
||||
value={selectedWarehouseId}
|
||||
|
|
|
@ -1,21 +1,22 @@
|
|||
// @ts-strict-ignore
|
||||
import ColumnPicker from "@dashboard/components/ColumnPicker";
|
||||
import { ColumnPicker } from "@dashboard/components/Datagrid/ColumnPicker/ColumnPicker";
|
||||
import { useColumns } from "@dashboard/components/Datagrid/ColumnPicker/useColumns";
|
||||
import Datagrid from "@dashboard/components/Datagrid/Datagrid";
|
||||
import { useColumnsDefault } from "@dashboard/components/Datagrid/hooks/useColumnsDefault";
|
||||
import {
|
||||
DatagridChangeStateContext,
|
||||
useDatagridChangeState,
|
||||
} from "@dashboard/components/Datagrid/hooks/useDatagridChange";
|
||||
import { useEmptyColumn } from "@dashboard/components/Datagrid/hooks/useEmptyColumn";
|
||||
import { TablePaginationWithContext } from "@dashboard/components/TablePagination";
|
||||
import { OrderListQuery } from "@dashboard/graphql";
|
||||
import { OrderListUrlSortField } from "@dashboard/orders/urls";
|
||||
import { ListProps, RelayToFlat, SortPage } from "@dashboard/types";
|
||||
import { Item } from "@glideapps/glide-data-grid";
|
||||
import { Box } from "@saleor/macaw-ui/next";
|
||||
import React, { useCallback } from "react";
|
||||
import React, { useCallback, useMemo } from "react";
|
||||
import { useIntl } from "react-intl";
|
||||
|
||||
import { useColumns, useGetCellContent } from "./datagrid";
|
||||
import { orderListStaticColumnAdapter, useGetCellContent } from "./datagrid";
|
||||
import { messages } from "./messages";
|
||||
import { canBeSorted, getColumnNameAndId, getOrdersRowsLength } from "./utils";
|
||||
|
||||
|
@ -41,29 +42,39 @@ export const OrderListDatagrid: React.FC<OrderListDatagridProps> = ({
|
|||
}) => {
|
||||
const intl = useIntl();
|
||||
const datagrid = useDatagridChangeState();
|
||||
const availableColumns = useColumns(sort);
|
||||
const ordersLength = getOrdersRowsLength(orders, disabled);
|
||||
|
||||
const {
|
||||
availableColumnsChoices,
|
||||
columnChoices,
|
||||
columns,
|
||||
defaultColumns,
|
||||
onColumnMoved,
|
||||
onColumnResize,
|
||||
onColumnsChange,
|
||||
picker,
|
||||
} = useColumnsDefault(availableColumns);
|
||||
const handleColumnChange = useCallback(
|
||||
picked => {
|
||||
onUpdateListSettings("columns", picked.filter(Boolean));
|
||||
},
|
||||
[onUpdateListSettings],
|
||||
);
|
||||
|
||||
const emptyColumn = useEmptyColumn();
|
||||
const memoizedStaticColumns = useMemo(
|
||||
() => orderListStaticColumnAdapter(emptyColumn, intl, sort),
|
||||
[emptyColumn, intl, sort],
|
||||
);
|
||||
|
||||
const { handlers, staticColumns, visibleColumns, selectedColumns } =
|
||||
useColumns({
|
||||
staticColumns: memoizedStaticColumns,
|
||||
selectedColumns: settings?.columns ?? [],
|
||||
onSave: handleColumnChange,
|
||||
});
|
||||
|
||||
const handleHeaderClick = useCallback(
|
||||
(col: number) => {
|
||||
const { columnName, columnId } = getColumnNameAndId(columns[col].id);
|
||||
const { columnName, columnId } = getColumnNameAndId(
|
||||
visibleColumns[col].id,
|
||||
);
|
||||
|
||||
if (canBeSorted(columnName)) {
|
||||
onSort(columnName, columnId);
|
||||
}
|
||||
},
|
||||
[columns, onSort],
|
||||
[visibleColumns, onSort],
|
||||
);
|
||||
|
||||
const handleRowClick = useCallback(
|
||||
|
@ -90,7 +101,7 @@ export const OrderListDatagrid: React.FC<OrderListDatagridProps> = ({
|
|||
);
|
||||
|
||||
const getCellContent = useGetCellContent({
|
||||
columns,
|
||||
columns: visibleColumns,
|
||||
orders,
|
||||
});
|
||||
|
||||
|
@ -105,7 +116,7 @@ export const OrderListDatagrid: React.FC<OrderListDatagridProps> = ({
|
|||
hasRowHover={hasRowHover}
|
||||
freezeColumns={2}
|
||||
verticalBorder={col => (col > 1 ? true : false)}
|
||||
availableColumns={columns}
|
||||
availableColumns={visibleColumns}
|
||||
onHeaderClicked={handleHeaderClick}
|
||||
emptyText={intl.formatMessage(messages.emptyText)}
|
||||
getCellContent={getCellContent}
|
||||
|
@ -113,20 +124,13 @@ export const OrderListDatagrid: React.FC<OrderListDatagridProps> = ({
|
|||
menuItems={() => []}
|
||||
rows={getOrdersRowsLength(orders, disabled)}
|
||||
selectionActions={() => null}
|
||||
onColumnResize={onColumnResize}
|
||||
onColumnMoved={onColumnMoved}
|
||||
renderColumnPicker={defaultProps => (
|
||||
onColumnResize={handlers.onResize}
|
||||
onColumnMoved={handlers.onMove}
|
||||
renderColumnPicker={() => (
|
||||
<ColumnPicker
|
||||
{...defaultProps}
|
||||
availableColumns={availableColumnsChoices}
|
||||
initialColumns={columnChoices}
|
||||
defaultColumns={defaultColumns}
|
||||
onSave={onColumnsChange}
|
||||
hasMore={false}
|
||||
loading={false}
|
||||
onFetchMore={() => undefined}
|
||||
onQueryChange={picker.setQuery}
|
||||
query={picker.query}
|
||||
staticColumns={staticColumns}
|
||||
selectedColumns={selectedColumns}
|
||||
onSave={handlers.onChange}
|
||||
/>
|
||||
)}
|
||||
fullScreenTitle={intl.formatMessage(messages.orders)}
|
||||
|
|
|
@ -6,14 +6,12 @@ import {
|
|||
textCell,
|
||||
} from "@dashboard/components/Datagrid/customCells/cells";
|
||||
import { GetCellContentOpts } from "@dashboard/components/Datagrid/Datagrid";
|
||||
import { useEmptyColumn } from "@dashboard/components/Datagrid/hooks/useEmptyColumn";
|
||||
import { AvailableColumn } from "@dashboard/components/Datagrid/types";
|
||||
import { Locale } from "@dashboard/components/Locale";
|
||||
import { OrderListQuery } from "@dashboard/graphql";
|
||||
import useLocale from "@dashboard/hooks/useLocale";
|
||||
import {
|
||||
getStatusColor,
|
||||
isFirstColumn,
|
||||
transformOrderStatus,
|
||||
transformPaymentStatus,
|
||||
} from "@dashboard/misc";
|
||||
|
@ -27,62 +25,51 @@ import {
|
|||
useTheme,
|
||||
} from "@saleor/macaw-ui/next";
|
||||
import moment from "moment-timezone";
|
||||
import { useMemo } from "react";
|
||||
import { IntlShape, useIntl } from "react-intl";
|
||||
|
||||
import { columnsMessages } from "./messages";
|
||||
|
||||
export const useColumns = (sort: Sort<OrderListUrlSortField>) => {
|
||||
const intl = useIntl();
|
||||
const emptyColumn = useEmptyColumn();
|
||||
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
export const orderListStaticColumnAdapter = (
|
||||
emptyColumn: AvailableColumn,
|
||||
intl: IntlShape,
|
||||
sort: Sort<OrderListUrlSortField>,
|
||||
) =>
|
||||
[
|
||||
emptyColumn,
|
||||
{
|
||||
id: "number",
|
||||
title: intl.formatMessage(columnsMessages.number),
|
||||
width: 100,
|
||||
icon: getColumnSortDirectionIcon(sort, OrderListUrlSortField.number),
|
||||
},
|
||||
{
|
||||
id: "date",
|
||||
title: intl.formatMessage(columnsMessages.date),
|
||||
width: 200,
|
||||
icon: getColumnSortDirectionIcon(sort, OrderListUrlSortField.date),
|
||||
},
|
||||
{
|
||||
id: "customer",
|
||||
title: intl.formatMessage(columnsMessages.customer),
|
||||
width: 200,
|
||||
icon: getColumnSortDirectionIcon(sort, OrderListUrlSortField.customer),
|
||||
},
|
||||
{
|
||||
id: "payment",
|
||||
title: intl.formatMessage(columnsMessages.payment),
|
||||
width: 200,
|
||||
icon: getColumnSortDirectionIcon(sort, OrderListUrlSortField.payment),
|
||||
},
|
||||
{
|
||||
id: "status",
|
||||
title: intl.formatMessage(columnsMessages.status),
|
||||
width: 200,
|
||||
icon: getColumnSortDirectionIcon(
|
||||
sort,
|
||||
OrderListUrlSortField.fulfillment,
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "total",
|
||||
title: intl.formatMessage(columnsMessages.total),
|
||||
width: 150,
|
||||
},
|
||||
],
|
||||
[emptyColumn, intl, sort],
|
||||
);
|
||||
|
||||
return columns;
|
||||
};
|
||||
].map(column => ({
|
||||
...column,
|
||||
icon: getColumnSortDirectionIcon(sort, column.id),
|
||||
}));
|
||||
|
||||
interface GetCellContentProps {
|
||||
columns: AvailableColumn[];
|
||||
|
@ -102,10 +89,6 @@ export const useGetCellContent = ({ columns, orders }: GetCellContentProps) => {
|
|||
[column, row]: Item,
|
||||
{ added, removed }: GetCellContentOpts,
|
||||
): GridCell => {
|
||||
if (isFirstColumn(column)) {
|
||||
return readonlyTextCell("");
|
||||
}
|
||||
|
||||
const columnId = columns[column]?.id;
|
||||
|
||||
if (!columnId) {
|
||||
|
|
|
@ -7,8 +7,7 @@ import {
|
|||
import { LimitsInfo } from "@dashboard/components/AppLayout/LimitsInfo";
|
||||
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";
|
||||
import { ButtonWithDropdown } from "@dashboard/components/ButtonWithDropdown";
|
||||
import { useDevModeContext } from "@dashboard/components/DevModePanel/hooks";
|
||||
import { FilterPresetsSelect } from "@dashboard/components/FilterPresetsSelect";
|
||||
import { ListPageLayout } from "@dashboard/components/Layouts";
|
||||
|
@ -30,8 +29,7 @@ import {
|
|||
} from "@dashboard/types";
|
||||
import { hasLimits, isLimitReached } from "@dashboard/utils/limits";
|
||||
import { Card } from "@material-ui/core";
|
||||
import { makeStyles } from "@saleor/macaw-ui";
|
||||
import { Box, ChevronRightIcon } from "@saleor/macaw-ui/next";
|
||||
import { Box, Button, ChevronRightIcon } from "@saleor/macaw-ui/next";
|
||||
import React, { useState } from "react";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
|
||||
|
@ -57,15 +55,6 @@ export interface OrderListPageProps
|
|||
onTabDelete: (tabIndex: number) => void;
|
||||
}
|
||||
|
||||
const useStyles = makeStyles(
|
||||
theme => ({
|
||||
settings: {
|
||||
marginRight: theme.spacing(2),
|
||||
},
|
||||
}),
|
||||
{ name: "OrderListPage" },
|
||||
);
|
||||
|
||||
const OrderListPage: React.FC<OrderListPageProps> = ({
|
||||
initialSearch,
|
||||
filterOpts,
|
||||
|
@ -86,7 +75,6 @@ const OrderListPage: React.FC<OrderListPageProps> = ({
|
|||
...listProps
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const classes = useStyles({});
|
||||
const filterStructure = createFilterStructure(intl, filterOpts);
|
||||
const limitsReached = isLimitReached(limits, "orders");
|
||||
const [isFilterPresetOpen, setFilterPresetOpen] = useState(false);
|
||||
|
@ -152,9 +140,8 @@ const OrderListPage: React.FC<OrderListPageProps> = ({
|
|||
|
||||
<Box display="flex" alignItems="center" gap={2}>
|
||||
{!!onSettingsOpen && (
|
||||
<CardMenu
|
||||
className={classes.settings}
|
||||
menuItems={[
|
||||
<TopNav.Menu
|
||||
items={[
|
||||
{
|
||||
label: intl.formatMessage({
|
||||
id: "vEwjub",
|
||||
|
@ -175,18 +162,32 @@ const OrderListPage: React.FC<OrderListPageProps> = ({
|
|||
]}
|
||||
/>
|
||||
)}
|
||||
<ButtonWithSelect
|
||||
disabled={limitsReached}
|
||||
options={extensionCreateButtonItems}
|
||||
data-test-id="create-order-button"
|
||||
{extensionCreateButtonItems.length > 0 ? (
|
||||
<ButtonWithDropdown
|
||||
onClick={onAdd}
|
||||
testId={"create-order-button"}
|
||||
options={extensionCreateButtonItems}
|
||||
disabled={limitsReached}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="LshEVn"
|
||||
defaultMessage="Create order"
|
||||
description="button"
|
||||
/>
|
||||
</ButtonWithSelect>
|
||||
</ButtonWithDropdown>
|
||||
) : (
|
||||
<Button
|
||||
data-test-id="create-order-button"
|
||||
onClick={onAdd}
|
||||
disabled={limitsReached}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="LshEVn"
|
||||
defaultMessage="Create order"
|
||||
description="button"
|
||||
/>
|
||||
</Button>
|
||||
)}
|
||||
{hasLimits(limits, "orders") && (
|
||||
<LimitsInfo
|
||||
text={intl.formatMessage(
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
// @ts-strict-ignore
|
||||
import ColumnPicker from "@dashboard/components/ColumnPicker";
|
||||
import { LazyQueryResult } from "@apollo/client/react";
|
||||
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,
|
||||
|
@ -9,9 +11,10 @@ import { TablePaginationWithContext } from "@dashboard/components/TablePaginatio
|
|||
import { commonTooltipMessages } from "@dashboard/components/TooltipTableCellHeader/messages";
|
||||
import { ProductListColumns } from "@dashboard/config";
|
||||
import {
|
||||
Exact,
|
||||
GridAttributesQuery,
|
||||
ProductListQuery,
|
||||
SearchAvailableInGridAttributesQuery,
|
||||
useAvailableColumnAttributesLazyQuery,
|
||||
} from "@dashboard/graphql";
|
||||
import useLocale from "@dashboard/hooks/useLocale";
|
||||
import { ProductListUrlSortField } from "@dashboard/products/urls";
|
||||
|
@ -19,47 +22,56 @@ import { canBeSorted } from "@dashboard/products/views/ProductList/sort";
|
|||
import { useSearchProductTypes } from "@dashboard/searches/useProductTypeSearch";
|
||||
import {
|
||||
ChannelProps,
|
||||
FetchMoreProps,
|
||||
ListProps,
|
||||
PageListProps,
|
||||
RelayToFlat,
|
||||
SortPage,
|
||||
} from "@dashboard/types";
|
||||
import { addAtIndex, removeAtIndex } from "@dashboard/utils/lists";
|
||||
import { GridColumn, Item } from "@glideapps/glide-data-grid";
|
||||
import { mapEdgesToItems } from "@dashboard/utils/maps";
|
||||
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 { isAttributeColumnValue } from "../ProductListPage/utils";
|
||||
import { useColumnPickerColumns } from "./hooks/useColumnPickerColumns";
|
||||
import { useDatagridColumns } from "./hooks/useDatagridColumns";
|
||||
import { messages } from "./messages";
|
||||
import {
|
||||
getAttributeIdFromColumnValue,
|
||||
isAttributeColumnValue,
|
||||
} from "../ProductListPage/utils";
|
||||
import {
|
||||
createGetCellContent,
|
||||
getAttributesFetchMoreProps,
|
||||
getAvailableAttributesData,
|
||||
getColumnMetadata,
|
||||
getColumnSortIconName,
|
||||
getProductRowsLength,
|
||||
} from "./utils";
|
||||
productListDynamicColumnAdapter,
|
||||
productListStaticColumnAdapter,
|
||||
} from "./datagrid";
|
||||
import { messages } from "./messages";
|
||||
|
||||
interface ProductListDatagridProps
|
||||
extends ListProps<ProductListColumns>,
|
||||
PageListProps<ProductListColumns>,
|
||||
SortPage<ProductListUrlSortField>,
|
||||
FetchMoreProps,
|
||||
ChannelProps {
|
||||
activeAttributeSortId: string;
|
||||
gridAttributes: RelayToFlat<GridAttributesQuery["grid"]>;
|
||||
products: RelayToFlat<ProductListQuery["products"]>;
|
||||
onRowClick?: (id: string) => void;
|
||||
rowAnchor?: (id: string) => string;
|
||||
columnQuery: string;
|
||||
availableInGridAttributes: RelayToFlat<
|
||||
SearchAvailableInGridAttributesQuery["availableInGrid"]
|
||||
gridAttributesOpts: LazyQueryResult<
|
||||
GridAttributesQuery,
|
||||
Exact<{
|
||||
ids: string | string[];
|
||||
}>
|
||||
>;
|
||||
products: RelayToFlat<ProductListQuery["products"]>;
|
||||
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<ProductListDatagridProps> = ({
|
||||
|
@ -72,104 +84,114 @@ export const ProductListDatagrid: React.FC<ProductListDatagridProps> = ({
|
|||
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({
|
||||
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,
|
||||
gridAttributes,
|
||||
gridAttributesFromSettings,
|
||||
settings,
|
||||
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,
|
||||
// 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,
|
||||
),
|
||||
);
|
||||
},
|
||||
[setColumns],
|
||||
);
|
||||
|
||||
const columnPickerColumns = useColumnPickerColumns(
|
||||
gridAttributes,
|
||||
availableInGridAttributes,
|
||||
settings,
|
||||
defaultSettings.columns,
|
||||
);
|
||||
|
||||
const getCellContent = useMemo(
|
||||
() =>
|
||||
createGetCellContent({
|
||||
columns,
|
||||
products,
|
||||
intl,
|
||||
getProductTypes: searchProductType,
|
||||
locale,
|
||||
gridAttributes,
|
||||
gridAttributesFromSettings,
|
||||
selectedChannelId,
|
||||
};
|
||||
}
|
||||
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<ProductListDatagridProps> = ({
|
|||
|
||||
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<ProductListDatagridProps> = ({
|
|||
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<ProductListDatagridProps> = ({
|
|||
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<ProductListDatagridProps> = ({
|
|||
fullScreenTitle={intl.formatMessage(messages.products)}
|
||||
onRowClick={handleRowClick}
|
||||
rowAnchor={handleRowAnchor}
|
||||
renderColumnPicker={defaultProps => (
|
||||
recentlyAddedColumn={recentlyAddedColumn}
|
||||
renderColumnPicker={() => (
|
||||
<ColumnPicker
|
||||
{...defaultProps}
|
||||
{...columnPickerColumns}
|
||||
hasMore={hasMore}
|
||||
loading={isAttributeLoading}
|
||||
onFetchMore={onFetchMore}
|
||||
query={columnQuery}
|
||||
onQueryChange={onColumnQueryChange}
|
||||
onSave={handleColumnChange}
|
||||
staticColumns={staticColumns}
|
||||
dynamicColumns={dynamicColumns}
|
||||
selectedColumns={selectedColumns}
|
||||
columnCategories={columnCategories}
|
||||
onDynamicColumnSelect={handlers.onDynamicColumnSelect}
|
||||
columnPickerSettings={columnPickerSettings}
|
||||
onSave={handlers.onChange}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
|
|
@ -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<ProductListUrlSortField>;
|
||||
gridAttributes: RelayToFlat<GridAttributesQuery["grid"]>;
|
||||
gridAttributesFromSettings: ProductListColumns[];
|
||||
activeAttributeSortId: string;
|
||||
}
|
||||
|
||||
export function getColumns({
|
||||
intl,
|
||||
sort,
|
||||
gridAttributes,
|
||||
gridAttributesFromSettings,
|
||||
activeAttributeSortId,
|
||||
}: GetColumnsProps): AvailableColumn[] {
|
||||
return [
|
||||
export const productListStaticColumnAdapter = (
|
||||
intl: IntlShape,
|
||||
sort: Sort<ProductListUrlSortField>,
|
||||
) =>
|
||||
[
|
||||
{
|
||||
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<GridAttributesQuery["grid"]>,
|
||||
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<ProductListUrlSortField>,
|
||||
) {
|
||||
return (attribute: ProductListColumns) => {
|
||||
const attributeId = getAttributeIdFromColumnValue(attribute);
|
||||
|
||||
const title =
|
||||
gridAttributes.find(gridAttribute => attributeId === gridAttribute.id)
|
||||
?.name ?? "";
|
||||
|
||||
return {
|
||||
id: attribute,
|
||||
title,
|
||||
intl: IntlShape,
|
||||
) =>
|
||||
attributes?.map(attribute => ({
|
||||
id: `attribute:${attribute.id}`,
|
||||
title: attribute.name,
|
||||
metaGroup: intl.formatMessage(categoryMetaGroups.attribute),
|
||||
width: 200,
|
||||
icon:
|
||||
attributeId === activeAttributeSortId &&
|
||||
getColumnSortDirectionIcon(sort, ProductListUrlSortField.attribute),
|
||||
};
|
||||
};
|
||||
}
|
||||
attribute.id === activeAttributeSortId
|
||||
? getColumnSortIconName(sort, ProductListUrlSortField.attribute)
|
||||
: undefined,
|
||||
}));
|
||||
|
||||
function getColumnSortIconName(
|
||||
export function getColumnSortIconName(
|
||||
{ sort, asc }: Sort<ProductListUrlSortField>,
|
||||
columnName: ProductListUrlSortField,
|
||||
) {
|
||||
|
@ -130,8 +154,6 @@ interface GetCellContentProps {
|
|||
intl: IntlShape;
|
||||
getProductTypes: (query: string) => Promise<DropdownChoice[]>;
|
||||
locale: Locale;
|
||||
gridAttributes: RelayToFlat<GridAttributesQuery["grid"]>;
|
||||
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,
|
||||
};
|
||||
};
|
|
@ -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<ProductListColumns>["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<ProductListColumns>["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<ProductListColumns>["columns"],
|
||||
rowNumber: 20,
|
||||
};
|
||||
const selectedAttibutes = [
|
||||
{
|
||||
__typename: "Attribute",
|
||||
id: "1",
|
||||
name: "Attr1",
|
||||
},
|
||||
{
|
||||
__typename: "Attribute",
|
||||
id: "2",
|
||||
name: "Attr2",
|
||||
},
|
||||
] as RelayToFlat<GridAttributesQuery["grid"]>;
|
||||
|
||||
const availableAttributesToSelect = [
|
||||
{
|
||||
__typename: "Attribute",
|
||||
id: "op1",
|
||||
name: "AttrOption1",
|
||||
},
|
||||
{
|
||||
__typename: "Attribute",
|
||||
id: "op2",
|
||||
name: "AttrOption2",
|
||||
},
|
||||
{
|
||||
__typename: "Attribute",
|
||||
id: "op3",
|
||||
name: "AttrOption3",
|
||||
},
|
||||
] as RelayToFlat<SearchAvailableInGridAttributesQuery["availableInGrid"]>;
|
||||
|
||||
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],
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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<GridAttributesQuery["grid"]>,
|
||||
availableInGridAttributes: RelayToFlat<
|
||||
SearchAvailableInGridAttributesQuery["availableInGrid"]
|
||||
>,
|
||||
settings: ListSettings<ProductListColumns>,
|
||||
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,
|
||||
};
|
||||
};
|
|
@ -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<GridAttributesQuery["grid"]>;
|
||||
gridAttributesFromSettings: ProductListColumns[];
|
||||
sort: Sort<ProductListUrlSortField>;
|
||||
settings: ListSettings<ProductListColumns>;
|
||||
}
|
||||
|
||||
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<AvailableColumn[]>([
|
||||
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<ProductListColumns>,
|
||||
) {
|
||||
return (column: AvailableColumn) =>
|
||||
settings.columns.includes(column.id as ProductListColumns) ||
|
||||
["name"].includes(column.id);
|
||||
}
|
||||
|
||||
function toCurrentColumnData(
|
||||
sort: Sort<ProductListUrlSortField>,
|
||||
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<ProductListUrlSortField>,
|
||||
) {
|
||||
return (column: ProductListColumns) => ({
|
||||
...columnSource.find(ac => ac.id === column),
|
||||
icon: getColumnSortDirectionIcon(sort, column as ProductListUrlSortField),
|
||||
});
|
||||
}
|
|
@ -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",
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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<typeof ProductListPage> = {
|
||||
|
|
|
@ -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<ProductFilterKeys, ProductListFilterOpts>,
|
||||
"onTabDelete"
|
||||
>,
|
||||
FetchMoreProps,
|
||||
SortPage<ProductListUrlSortField>,
|
||||
ChannelProps {
|
||||
activeAttributeSortId: string;
|
||||
availableInGridAttributes: RelayToFlat<
|
||||
SearchAvailableInGridAttributesQuery["availableInGrid"]
|
||||
>;
|
||||
columnQuery: string;
|
||||
currencySymbol: string;
|
||||
gridAttributes: RelayToFlat<GridAttributesQuery["grid"]>;
|
||||
gridAttributesOpts: LazyQueryResult<
|
||||
GridAttributesQuery,
|
||||
Exact<{
|
||||
ids: string | string[];
|
||||
}>
|
||||
>;
|
||||
limits: RefreshLimitsQuery["shop"]["limits"];
|
||||
products: RelayToFlat<ProductListQuery["products"]>;
|
||||
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<ProductListPageProps> = 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<ProductListPageProps> = props => {
|
|||
tabs,
|
||||
onTabUpdate,
|
||||
hasPresetsChanged,
|
||||
columnPickerSettings,
|
||||
setDynamicColumnSettings,
|
||||
selectedProductIds,
|
||||
onProductsDelete,
|
||||
clearRowSelection,
|
||||
|
@ -291,20 +293,20 @@ export const ProductListPage: React.FC<ProductListPageProps> = 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}
|
||||
/>
|
||||
) : (
|
||||
<ProductListTiles
|
||||
|
|
|
@ -5,7 +5,7 @@ export function getAttributeColumnValue(id: string) {
|
|||
}
|
||||
|
||||
export function isAttributeColumnValue(value: string) {
|
||||
return value.includes(`${prefix}:`);
|
||||
return value?.includes(`${prefix}:`);
|
||||
}
|
||||
|
||||
export function getAttributeIdFromColumnValue(value: string) {
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
import { channelsList } from "@dashboard/channels/fixtures";
|
||||
import {
|
||||
AttributeInputTypeEnum,
|
||||
GridAttributesQuery,
|
||||
ProductFragment,
|
||||
ProductListQuery,
|
||||
ProductMediaType,
|
||||
|
@ -3847,3 +3848,152 @@ export const productTypesList: Array<
|
|||
name: "Mushroom",
|
||||
},
|
||||
];
|
||||
|
||||
export const gridAttributesResult: GridAttributesQuery = {
|
||||
__typename: "Query",
|
||||
availableAttributes: {
|
||||
__typename: "AttributeCountableConnection",
|
||||
edges: [
|
||||
{
|
||||
__typename: "AttributeCountableEdge",
|
||||
node: {
|
||||
__typename: "Attribute",
|
||||
id: "QXR0cmlidXRlOjIx",
|
||||
name: "ABV",
|
||||
},
|
||||
},
|
||||
{
|
||||
__typename: "AttributeCountableEdge",
|
||||
node: {
|
||||
__typename: "Attribute",
|
||||
id: "QXR0cmlidXRlOjI3",
|
||||
name: "Author",
|
||||
},
|
||||
},
|
||||
{
|
||||
__typename: "AttributeCountableEdge",
|
||||
node: {
|
||||
__typename: "Attribute",
|
||||
id: "QXR0cmlidXRlOjE1",
|
||||
name: "Bottle Size",
|
||||
},
|
||||
},
|
||||
{
|
||||
__typename: "AttributeCountableEdge",
|
||||
node: {
|
||||
__typename: "Attribute",
|
||||
id: "QXR0cmlidXRlOjE4",
|
||||
name: "Bucket Size",
|
||||
},
|
||||
},
|
||||
{
|
||||
__typename: "AttributeCountableEdge",
|
||||
node: {
|
||||
__typename: "Attribute",
|
||||
id: "QXR0cmlidXRlOjE0",
|
||||
name: "Color",
|
||||
},
|
||||
},
|
||||
{
|
||||
__typename: "AttributeCountableEdge",
|
||||
node: {
|
||||
__typename: "Attribute",
|
||||
id: "QXR0cmlidXRlOjIy",
|
||||
name: "Cushion Size",
|
||||
},
|
||||
},
|
||||
{
|
||||
__typename: "AttributeCountableEdge",
|
||||
node: {
|
||||
__typename: "Attribute",
|
||||
id: "QXR0cmlidXRlOjY3Nw==",
|
||||
name: "CyCreateProduct-33370",
|
||||
},
|
||||
},
|
||||
{
|
||||
__typename: "AttributeCountableEdge",
|
||||
node: {
|
||||
__typename: "Attribute",
|
||||
id: "QXR0cmlidXRlOjY3Ng==",
|
||||
name: "CyCreateProduct-66614",
|
||||
},
|
||||
},
|
||||
{
|
||||
__typename: "AttributeCountableEdge",
|
||||
node: {
|
||||
__typename: "Attribute",
|
||||
id: "QXR0cmlidXRlOjY3OQ==",
|
||||
name: "CyCreateVariants-18905",
|
||||
},
|
||||
},
|
||||
{
|
||||
__typename: "AttributeCountableEdge",
|
||||
node: {
|
||||
__typename: "Attribute",
|
||||
id: "QXR0cmlidXRlOjY4MA==",
|
||||
name: "CyCreateVariants-9040",
|
||||
},
|
||||
},
|
||||
],
|
||||
pageInfo: {
|
||||
__typename: "PageInfo",
|
||||
endCursor: "WyIwIiwgImN5Y3JlYXRldmFyaWFudHMtOTA0MCJd",
|
||||
hasNextPage: true,
|
||||
hasPreviousPage: false,
|
||||
startCursor: "WyIwIiwgImFidiJd",
|
||||
},
|
||||
},
|
||||
selectedAttributes: {
|
||||
__typename: "AttributeCountableConnection",
|
||||
edges: [
|
||||
{
|
||||
__typename: "AttributeCountableEdge",
|
||||
node: {
|
||||
__typename: "Attribute",
|
||||
id: "QXR0cmlidXRlOjIx",
|
||||
name: "ABV",
|
||||
},
|
||||
},
|
||||
{
|
||||
__typename: "AttributeCountableEdge",
|
||||
node: {
|
||||
__typename: "Attribute",
|
||||
id: "QXR0cmlidXRlOjI3",
|
||||
name: "Author",
|
||||
},
|
||||
},
|
||||
{
|
||||
__typename: "AttributeCountableEdge",
|
||||
node: {
|
||||
__typename: "Attribute",
|
||||
id: "QXR0cmlidXRlOjE1",
|
||||
name: "Bottle Size",
|
||||
},
|
||||
},
|
||||
{
|
||||
__typename: "AttributeCountableEdge",
|
||||
node: {
|
||||
__typename: "Attribute",
|
||||
id: "QXR0cmlidXRlOjMw",
|
||||
name: "Details",
|
||||
},
|
||||
},
|
||||
{
|
||||
__typename: "AttributeCountableEdge",
|
||||
node: {
|
||||
__typename: "Attribute",
|
||||
id: "QXR0cmlidXRlOjI1",
|
||||
name: "Format",
|
||||
},
|
||||
},
|
||||
{
|
||||
__typename: "AttributeCountableEdge",
|
||||
node: {
|
||||
__typename: "Attribute",
|
||||
id: "QXR0cmlidXRlOjI2",
|
||||
name: "Publisher",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
|
|
@ -64,7 +64,6 @@ export const productListQuery = gql`
|
|||
$channel: String
|
||||
$sort: ProductOrder
|
||||
$hasChannel: Boolean!
|
||||
$hasSelectedAttributes: Boolean!
|
||||
) {
|
||||
products(
|
||||
before: $before
|
||||
|
@ -80,7 +79,7 @@ export const productListQuery = gql`
|
|||
...ProductWithChannelListings
|
||||
updatedAt
|
||||
description
|
||||
attributes @include(if: $hasSelectedAttributes) {
|
||||
attributes {
|
||||
...ProductListAttribute
|
||||
}
|
||||
}
|
||||
|
@ -250,14 +249,54 @@ export const productMediaQuery = gql`
|
|||
`;
|
||||
|
||||
export const gridAttributes = gql`
|
||||
query GridAttributes($ids: [ID!]!) {
|
||||
grid: attributes(first: 25, filter: { ids: $ids }) {
|
||||
query GridAttributes($ids: [ID!]!, $hasAttributes: Boolean!) {
|
||||
availableAttributes: attributes(first: 10) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
pageInfo {
|
||||
...PageInfo
|
||||
}
|
||||
}
|
||||
selectedAttributes: attributes(first: 25, filter: { ids: $ids })
|
||||
@include(if: $hasAttributes) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
import { filterable } from "@dashboard/attributes/utils/data";
|
||||
import ActionDialog from "@dashboard/components/ActionDialog";
|
||||
import useAppChannel from "@dashboard/components/AppLayout/AppChannelContext";
|
||||
import { useColumnPickerSettings } from "@dashboard/components/Datagrid/ColumnPicker/useColumnPickerSettings";
|
||||
import DeleteFilterTabDialog from "@dashboard/components/DeleteFilterTabDialog";
|
||||
import SaveFilterTabDialog, {
|
||||
SaveFilterTabDialogFormData,
|
||||
|
@ -16,7 +17,8 @@ import {
|
|||
import { Task } from "@dashboard/containers/BackgroundTasks/types";
|
||||
import {
|
||||
ProductListQueryVariables,
|
||||
useGridAttributesQuery,
|
||||
useAvailableColumnAttributesLazyQuery,
|
||||
useGridAttributesLazyQuery,
|
||||
useInitialProductFilterAttributesQuery,
|
||||
useInitialProductFilterCategoriesQuery,
|
||||
useInitialProductFilterCollectionsQuery,
|
||||
|
@ -38,7 +40,6 @@ import usePaginator, {
|
|||
PaginatorContext,
|
||||
} from "@dashboard/hooks/usePaginator";
|
||||
import { commonMessages } from "@dashboard/intl";
|
||||
import { maybe } from "@dashboard/misc";
|
||||
import ProductExportDialog from "@dashboard/products/components/ProductExportDialog";
|
||||
import {
|
||||
getAttributeIdFromColumnValue,
|
||||
|
@ -54,7 +55,6 @@ import {
|
|||
} from "@dashboard/products/urls";
|
||||
import useAttributeSearch from "@dashboard/searches/useAttributeSearch";
|
||||
import useAttributeValueSearch from "@dashboard/searches/useAttributeValueSearch";
|
||||
import useAvailableInGridAttributesSearch from "@dashboard/searches/useAvailableInGridAttributesSearch";
|
||||
import useCategorySearch from "@dashboard/searches/useCategorySearch";
|
||||
import useCollectionSearch from "@dashboard/searches/useCollectionSearch";
|
||||
import useProductTypeSearch from "@dashboard/searches/useProductTypeSearch";
|
||||
|
@ -104,6 +104,8 @@ export const ProductList: React.FC<ProductListProps> = ({ 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<ProductListProps> = ({ params }) => {
|
|||
[params, settings.rowNumber],
|
||||
);
|
||||
|
||||
const filteredColumnIds = settings.columns
|
||||
const filteredColumnIds = (columnPickerSettings ?? [])
|
||||
.filter(isAttributeColumnValue)
|
||||
.map(getAttributeIdFromColumnValue);
|
||||
|
||||
|
@ -363,7 +365,6 @@ export const ProductList: React.FC<ProductListProps> = ({ params }) => {
|
|||
variables: {
|
||||
...queryVariables,
|
||||
hasChannel: !!selectedChannel,
|
||||
hasSelectedAttributes: filteredColumnIds.length > 0,
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -387,16 +388,21 @@ export const ProductList: React.FC<ProductListProps> = ({ params }) => {
|
|||
[products, selectedProductIds],
|
||||
);
|
||||
|
||||
const availableInGridAttributesOpts = useAvailableInGridAttributesSearch({
|
||||
const availableColumnsAttributesOpts =
|
||||
useAvailableColumnAttributesLazyQuery();
|
||||
|
||||
const [gridAttributesQuery, gridAttributesOpts] =
|
||||
useGridAttributesLazyQuery();
|
||||
|
||||
useEffect(() => {
|
||||
// Fetch this only on initial render
|
||||
gridAttributesQuery({
|
||||
variables: {
|
||||
...DEFAULT_INITIAL_SEARCH_DATA,
|
||||
first: 5,
|
||||
ids: filteredColumnIds,
|
||||
hasAttributes: !!filteredColumnIds.length,
|
||||
},
|
||||
});
|
||||
const gridAttributes = useGridAttributesQuery({
|
||||
variables: { ids: filteredColumnIds },
|
||||
skip: filteredColumnIds.length === 0,
|
||||
});
|
||||
}, []);
|
||||
|
||||
const {
|
||||
loadMore: loadMoreDialogProductTypes,
|
||||
|
@ -460,31 +466,16 @@ export const ProductList: React.FC<ProductListProps> = ({ 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<ProductListProps> = ({ 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;
|
||||
|
|
Loading…
Reference in a new issue