Add sorting ability to product list

This commit is contained in:
dominik-zeglen 2019-09-13 13:33:42 +02:00
parent 5dea135b2e
commit 87fca41f07
13 changed files with 270 additions and 41 deletions

View file

@ -24,6 +24,12 @@ const useStyles = makeStyles((theme: Theme) => ({
display: "flex",
height: 24
},
labelContainerCenter: {
justifyContent: "center"
},
labelContainerRight: {
justifyContent: "flex-end"
},
root: {
cursor: "pointer"
}
@ -31,18 +37,31 @@ const useStyles = makeStyles((theme: Theme) => ({
export type TableCellHeaderArrowDirection = "asc" | "desc";
export type TableCellHeaderArrowPosition = "left" | "right";
export interface TableCellHeader extends TableCellProps {
export interface TableCellHeaderProps extends TableCellProps {
arrowPosition?: TableCellHeaderArrowPosition;
direction?: TableCellHeaderArrowDirection;
textAlign?: "left" | "center" | "right";
}
const TableCellHeader: React.FC<TableCellHeader> = props => {
const TableCellHeader: React.FC<TableCellHeaderProps> = props => {
const classes = useStyles(props);
const { arrowPosition, children, className, direction, ...rest } = props;
const {
arrowPosition,
children,
className,
direction,
textAlign,
...rest
} = props;
return (
<TableCell {...rest} className={classNames(className, classes.root)}>
<div className={classes.labelContainer}>
<TableCell {...rest} className={classNames(classes.root, className)}>
<div
className={classNames(classes.labelContainer, {
[classes.labelContainerCenter]: textAlign === "center",
[classes.labelContainerRight]: textAlign === "right"
})}
>
{!!direction && arrowPosition === "left" && (
<ArrowSort
className={classNames(classes.arrow, classes.arrowLeft, {
@ -65,6 +84,7 @@ const TableCellHeader: React.FC<TableCellHeader> = props => {
TableCellHeader.displayName = "TableCellHeader";
TableCellHeader.defaultProps = {
arrowPosition: "left"
arrowPosition: "left",
textAlign: "left"
};
export default TableCellHeader;

View file

@ -4,11 +4,13 @@ import urlJoin from "url-join";
import { defineMessages, IntlShape } from "react-intl";
import { ConfirmButtonTransitionState } from "./components/ConfirmButton/ConfirmButton";
import { TableCellHeaderArrowDirection } from "./components/TableCellHeader";
import { APP_MOUNT_URI } from "./config";
import { AddressType } from "./customers/types";
import { PartialMutationProviderOutput, UserError } from "./types";
import {
AuthorizationKeyType,
OrderDirection,
OrderStatus,
PaymentChargeStatusEnum,
TaxRateType
@ -478,3 +480,17 @@ export function findInEnum<TEnum extends object>(
throw new Error(`Key ${needle} not found in enum`);
}
export function getOrderDirection(asc: boolean): OrderDirection {
return asc ? OrderDirection.ASC : OrderDirection.DESC;
}
export function getArrowDirection(
order: OrderDirection
): TableCellHeaderArrowDirection {
return order === OrderDirection.ASC ? "asc" : "desc";
}
export function parseBoolean(a: string): boolean {
return a === "true";
}

View file

@ -20,18 +20,22 @@ import StatusLabel from "@saleor/components/StatusLabel";
import TableCellAvatar, {
AVATAR_MARGIN
} from "@saleor/components/TableCellAvatar";
import TableCellHeader from "@saleor/components/TableCellHeader";
import TableHead from "@saleor/components/TableHead";
import TablePagination from "@saleor/components/TablePagination";
import { ProductListColumns } from "@saleor/config";
import { maybe, renderCollection } from "@saleor/misc";
import { getArrowDirection, maybe, renderCollection } from "@saleor/misc";
import {
getAttributeIdFromColumnValue,
isAttributeColumnValue
} from "@saleor/products/components/ProductListPage/utils";
import { AvailableInGridAttributes_grid_edges_node } from "@saleor/products/types/AvailableInGridAttributes";
import { ProductList_products_edges_node } from "@saleor/products/types/ProductList";
import { ListActions, ListProps } from "@saleor/types";
import TDisplayColumn from "@saleor/utils/columns/DisplayColumn";
import { ListActions, ListProps, SortPage } from "@saleor/types";
import { ProductOrder, ProductOrderField } from "@saleor/types/globalTypes";
import TDisplayColumn, {
DisplayColumnProps
} from "@saleor/utils/columns/DisplayColumn";
const styles = (theme: Theme) =>
createStyles({
@ -87,12 +91,18 @@ const styles = (theme: Theme) =>
}
});
const DisplayColumn = TDisplayColumn as React.FunctionComponent<
DisplayColumnProps<ProductListColumns>
>;
interface ProductListProps
extends ListProps<ProductListColumns>,
ListActions,
SortPage<ProductOrderField>,
WithStyles<typeof styles> {
gridAttributes: AvailableInGridAttributes_grid_edges_node[];
products: ProductList_products_edges_node[];
sort: ProductOrder;
}
export const ProductList = withStyles(styles, { name: "ProductList" })(
@ -105,19 +115,18 @@ export const ProductList = withStyles(styles, { name: "ProductList" })(
pageInfo,
products,
selected,
sort,
toggle,
toggleAll,
toolbar,
onNextPage,
onPreviousPage,
onUpdateListSettings,
onRowClick
onRowClick,
onSort
}: ProductListProps) => {
const intl = useIntl();
const DisplayColumn: React.FC<{ column: ProductListColumns }> = props => (
<TDisplayColumn displayColumns={settings.columns} {...props} />
);
const gridAttributesFromSettings = settings.columns.filter(
isAttributeColumnValue
);
@ -129,16 +138,22 @@ export const ProductList = withStyles(styles, { name: "ProductList" })(
<colgroup>
<col />
<col className={classes.colName} />
<DisplayColumn column="productType">
<DisplayColumn
column="productType"
displayColumns={settings.columns}
>
<col className={classes.colType} />
</DisplayColumn>
<DisplayColumn column="isPublished">
<DisplayColumn
column="isPublished"
displayColumns={settings.columns}
>
<col className={classes.colPublished} />
</DisplayColumn>
{gridAttributesFromSettings.map(gridAttribute => (
<col className={classes.colAttribute} key={gridAttribute} />
))}
<DisplayColumn column="price">
<DisplayColumn column="price" displayColumns={settings.columns}>
<col className={classes.colPrice} />
</DisplayColumn>
</colgroup>
@ -150,30 +165,59 @@ export const ProductList = withStyles(styles, { name: "ProductList" })(
toggleAll={toggleAll}
toolbar={toolbar}
>
<TableCell
<TableCellHeader
arrowPosition="right"
className={classNames(classes.colName, {
[classes.colNameFixed]: settings.columns.length > 4
})}
direction={
sort.field === ProductOrderField.NAME
? getArrowDirection(sort.direction)
: undefined
}
onClick={() => onSort(ProductOrderField.NAME)}
>
<span className={classes.colNameHeader}>
<FormattedMessage defaultMessage="Name" description="product" />
</span>
</TableCell>
<DisplayColumn column="productType">
<TableCell className={classes.colType}>
</TableCellHeader>
<DisplayColumn
column="productType"
displayColumns={settings.columns}
>
<TableCellHeader
className={classes.colType}
direction={
sort.field === ProductOrderField.TYPE
? getArrowDirection(sort.direction)
: undefined
}
onClick={() => onSort(ProductOrderField.TYPE)}
>
<FormattedMessage
defaultMessage="Type"
description="product type"
/>
</TableCell>
</TableCellHeader>
</DisplayColumn>
<DisplayColumn column="isPublished">
<TableCell className={classes.colPublished}>
<DisplayColumn
column="isPublished"
displayColumns={settings.columns}
>
<TableCellHeader
className={classes.colPublished}
direction={
sort.field === ProductOrderField.PUBLISHED
? getArrowDirection(sort.direction)
: undefined
}
onClick={() => onSort(ProductOrderField.PUBLISHED)}
>
<FormattedMessage
defaultMessage="Published"
description="product status"
/>
</TableCell>
</TableCellHeader>
</DisplayColumn>
{gridAttributesFromSettings.map(gridAttributeFromSettings => (
<TableCell
@ -192,13 +236,22 @@ export const ProductList = withStyles(styles, { name: "ProductList" })(
)}
</TableCell>
))}
<DisplayColumn column="price">
<TableCell className={classes.colPrice}>
<DisplayColumn column="price" displayColumns={settings.columns}>
<TableCellHeader
className={classes.colPrice}
direction={
sort.field === ProductOrderField.PRICE
? getArrowDirection(sort.direction)
: undefined
}
textAlign="right"
onClick={() => onSort(ProductOrderField.PRICE)}
>
<FormattedMessage
defaultMessage="Price"
description="product price"
/>
</TableCell>
</TableCellHeader>
</DisplayColumn>
</TableHead>
<TableFooter>
@ -246,7 +299,10 @@ export const ProductList = withStyles(styles, { name: "ProductList" })(
>
{maybe<React.ReactNode>(() => product.name, <Skeleton />)}
</TableCellAvatar>
<DisplayColumn column="productType">
<DisplayColumn
column="productType"
displayColumns={settings.columns}
>
<TableCell className={classes.colType}>
{product && product.productType ? (
product.productType.name
@ -255,7 +311,10 @@ export const ProductList = withStyles(styles, { name: "ProductList" })(
)}
</TableCell>
</DisplayColumn>
<DisplayColumn column="isPublished">
<DisplayColumn
column="isPublished"
displayColumns={settings.columns}
>
<TableCell className={classes.colPublished}>
{product &&
maybe(() => product.isAvailable !== undefined) ? (
@ -299,7 +358,10 @@ export const ProductList = withStyles(styles, { name: "ProductList" })(
}, <Skeleton />)}
</TableCell>
))}
<DisplayColumn column="price">
<DisplayColumn
column="price"
displayColumns={settings.columns}
>
<TableCell className={classes.colPrice}>
{maybe(() => product.basePrice) &&
maybe(() => product.basePrice.amount) !== undefined &&

View file

@ -1,7 +1,6 @@
import Button from "@material-ui/core/Button";
import Card from "@material-ui/core/Card";
import { Theme } from "@material-ui/core/styles";
import makeStyles from "@material-ui/styles/makeStyles";
import React from "react";
import { FormattedMessage, useIntl } from "react-intl";
@ -22,8 +21,11 @@ import {
FetchMoreProps,
FilterPageProps,
ListActions,
PageListProps
PageListProps,
SortPage
} from "@saleor/types";
import { ProductOrder, ProductOrderField } from "@saleor/types/globalTypes";
import { ProductListUrlFilters } from "../../urls";
import ProductList from "../ProductList";
import ProductListFilter, { ProductFilterKeys } from "../ProductListFilter";
@ -31,12 +33,14 @@ export interface ProductListPageProps
extends PageListProps<ProductListColumns>,
ListActions,
FilterPageProps<ProductFilterKeys>,
FetchMoreProps {
FetchMoreProps,
SortPage<ProductOrderField> {
availableInGridAttributes: AvailableInGridAttributes_availableInGrid_edges_node[];
currencySymbol: string;
gridAttributes: AvailableInGridAttributes_grid_edges_node[];
totalGridAttributes: number;
products: ProductList_products_edges_node[];
sort: ProductOrder;
}
const useStyles = makeStyles((theme: Theme) => ({

View file

@ -4,6 +4,7 @@ import { useIntl } from "react-intl";
import { Route, RouteComponentProps, Switch } from "react-router-dom";
import { sectionNames } from "@saleor/intl";
import { parseBoolean } from "@saleor/misc";
import { WindowTitle } from "../components/WindowTitle";
import {
productAddPath,
@ -28,7 +29,10 @@ const ProductList: React.StatelessComponent<RouteComponentProps<any>> = ({
location
}) => {
const qs = parseQs(location.search.substr(1));
const params: ProductListUrlQueryParams = qs;
const params: ProductListUrlQueryParams = {
...qs,
asc: parseBoolean(qs.asc)
};
return <ProductListComponent params={params} />;
};

View file

@ -218,6 +218,7 @@ const productListQuery = gql`
$last: Int
$before: String
$filter: ProductFilterInput
$sort: ProductOrder
) {
products(
before: $before
@ -225,6 +226,7 @@ const productListQuery = gql`
first: $first
last: $last
filter: $filter
sortBy: $sort
) {
edges {
node {

View file

@ -2,7 +2,7 @@
/* eslint-disable */
// This file was automatically generated and should not be edited.
import { ProductFilterInput } from "./../../types/globalTypes";
import { ProductFilterInput, ProductOrder } from "./../../types/globalTypes";
// ====================================================
// GraphQL query operation: ProductList
@ -82,4 +82,5 @@ export interface ProductListVariables {
last?: number | null;
before?: string | null;
filter?: ProductFilterInput | null;
sort?: ProductOrder | null;
}

View file

@ -7,6 +7,7 @@ import {
Dialog,
Filters,
Pagination,
Sort,
TabActionDialog
} from "../types";
@ -29,9 +30,17 @@ export enum ProductListUrlFiltersEnum {
query = "query"
}
export type ProductListUrlFilters = Filters<ProductListUrlFiltersEnum>;
export enum ProductListUrlSortFields {
name = "name",
productType = "type",
status = "status",
price = "price"
}
export type ProductListUrlSort = Sort<ProductListUrlSortFields>;
export type ProductListUrlQueryParams = BulkAction &
Dialog<ProductListUrlDialog> &
ProductListUrlFilters &
ProductListUrlSort &
Pagination &
ActiveTab;
export const productListUrl = (params?: ProductListUrlQueryParams): string =>

View file

@ -22,6 +22,7 @@ import usePaginator, {
import useShop from "@saleor/hooks/useShop";
import { commonMessages } from "@saleor/intl";
import { getMutationState, maybe } from "@saleor/misc";
import { ProductListVariables } from "@saleor/products/types/ProductList";
import { ListViews } from "@saleor/types";
import ProductListPage from "../../components/ProductListPage";
import {
@ -52,6 +53,7 @@ import {
getFilterVariables,
saveFilterTab
} from "./filters";
import { getSortUrlVariables, getSortQueryVariables } from "./sort";
interface ProductListProps {
params: ProductListUrlQueryParams;
@ -152,10 +154,13 @@ export const ProductList: React.StatelessComponent<ProductListProps> = ({
const paginationState = createPaginationState(settings.rowNumber, params);
const currencySymbol = maybe(() => shop.defaultCurrency, "USD");
const queryVariables = React.useMemo(
const filter = getFilterVariables(params);
const sort = getSortQueryVariables(params);
const queryVariables = React.useMemo<ProductListVariables>(
() => ({
...paginationState,
filter: getFilterVariables(params)
filter,
sort
}),
[params, settings.rowNumber]
);
@ -224,6 +229,19 @@ export const ProductList: React.StatelessComponent<ProductListProps> = ({
return (
<>
<ProductListPage
sort={sort}
onSort={field =>
navigate(
productListUrl({
...params,
...getSortUrlVariables(
field,
sort.field,
params
)
})
)
}
availableInGridAttributes={maybe(
() =>
attributes.data.availableInGrid.edges.map(

View file

@ -0,0 +1,66 @@
import { getOrderDirection } from "@saleor/misc";
import {
ProductListUrlQueryParams,
ProductListUrlSortFields as ProductListUrlSortField
} from "@saleor/products/urls";
import { Sort } from "@saleor/types";
import { ProductOrderField } from "@saleor/types/globalTypes";
export function getSortQueryVariables(params: ProductListUrlQueryParams) {
return {
direction: getOrderDirection(params.asc),
field: getSortQueryField(params.sort)
};
}
export function getSortQueryField(
sort: ProductListUrlSortField
): ProductOrderField {
switch (sort) {
case ProductListUrlSortField.name:
return ProductOrderField.NAME;
case ProductListUrlSortField.price:
return ProductOrderField.PRICE;
case ProductListUrlSortField.productType:
return ProductOrderField.TYPE;
case ProductListUrlSortField.status:
return ProductOrderField.PUBLISHED;
default:
return ProductOrderField.NAME;
}
}
export function getSortUrlField(
sort: ProductOrderField
): ProductListUrlSortField {
switch (sort) {
case ProductOrderField.NAME:
return ProductListUrlSortField.name;
case ProductOrderField.PRICE:
return ProductListUrlSortField.price;
case ProductOrderField.TYPE:
return ProductListUrlSortField.productType;
case ProductOrderField.PUBLISHED:
return ProductListUrlSortField.status;
default:
return ProductListUrlSortField.name;
}
}
export function getSortUrlVariables(
field: ProductOrderField,
selectedField: ProductOrderField,
params: Sort<ProductListUrlSortField>
): Sort<ProductListUrlSortField> {
if (field === selectedField) {
return {
asc: !params.asc,
sort: getSortUrlField(field)
};
}
return {
asc: true,
sort: getSortUrlField(field)
};
}

View file

@ -47,6 +47,10 @@ export interface ListProps<TColumns extends string = string> {
) => void;
onListSettingsReset?: () => void;
}
export interface SortPage<TSortKey extends string> {
onSort: (field: TSortKey) => void;
}
export interface ListActionsWithoutToolbar {
toggle: (id: string) => void;
toggleAll: (items: React.ReactNodeArray, selected: number) => void;
@ -131,6 +135,10 @@ export type FiltersWithMultipleValues<TFilters extends string> = Partial<
export type SingleAction = Partial<{
id: string;
}>;
export type Sort<TSort extends string = string> = Partial<{
asc: boolean;
sort: TSort;
}>;
export type BulkAction = Partial<{
ids: string[];
}>;

View file

@ -112,6 +112,11 @@ export enum OrderAction {
VOID = "VOID",
}
export enum OrderDirection {
ASC = "ASC",
DESC = "DESC",
}
export enum OrderEventsEmailsEnum {
DIGITAL_LINKS = "DIGITAL_LINKS",
FULFILLMENT_CONFIRMATION = "FULFILLMENT_CONFIRMATION",
@ -187,6 +192,15 @@ export enum PermissionEnum {
MANAGE_USERS = "MANAGE_USERS",
}
export enum ProductOrderField {
DATE = "DATE",
MINIMAL_PRICE = "MINIMAL_PRICE",
NAME = "NAME",
PRICE = "PRICE",
PUBLISHED = "PUBLISHED",
TYPE = "TYPE",
}
export enum ProductTypeConfigurable {
CONFIGURABLE = "CONFIGURABLE",
SIMPLE = "SIMPLE",
@ -574,6 +588,11 @@ export interface ProductFilterInput {
minimalPrice?: PriceRangeInput | null;
}
export interface ProductOrder {
field: ProductOrderField;
direction: OrderDirection;
}
export interface ProductTypeFilterInput {
search?: string | null;
configurable?: ProductTypeConfigurable | null;

View file

@ -12,12 +12,12 @@ const DisplayColumn: React.FC<DisplayColumnProps> = ({
children,
column
}) => {
const displayColumn = React.useCallback(
(column: string) => isSelected(column, displayColumns, (a, b) => a === b),
[displayColumns]
const display = React.useMemo(
() => isSelected(column, displayColumns, (a, b) => a === b),
[column, displayColumns]
);
return <>{displayColumn(column) && children}</>;
return <>{display && children}</>;
};
DisplayColumn.displayName = "DisplayColumn";