diff --git a/src/components/TableCellHeader/TableCellHeader.tsx b/src/components/TableCellHeader/TableCellHeader.tsx index e6011b30b..e557c9196 100644 --- a/src/components/TableCellHeader/TableCellHeader.tsx +++ b/src/components/TableCellHeader/TableCellHeader.tsx @@ -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 = props => { +const TableCellHeader: React.FC = props => { const classes = useStyles(props); - const { arrowPosition, children, className, direction, ...rest } = props; + const { + arrowPosition, + children, + className, + direction, + textAlign, + ...rest + } = props; return ( - -
+ +
{!!direction && arrowPosition === "left" && ( = props => { TableCellHeader.displayName = "TableCellHeader"; TableCellHeader.defaultProps = { - arrowPosition: "left" + arrowPosition: "left", + textAlign: "left" }; export default TableCellHeader; diff --git a/src/misc.ts b/src/misc.ts index 517e1a00e..6cbfafde9 100644 --- a/src/misc.ts +++ b/src/misc.ts @@ -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( 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"; +} diff --git a/src/products/components/ProductList/ProductList.tsx b/src/products/components/ProductList/ProductList.tsx index 88508d18a..cee340599 100644 --- a/src/products/components/ProductList/ProductList.tsx +++ b/src/products/components/ProductList/ProductList.tsx @@ -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 +>; + interface ProductListProps extends ListProps, ListActions, + SortPage, WithStyles { 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 => ( - - ); const gridAttributesFromSettings = settings.columns.filter( isAttributeColumnValue ); @@ -129,16 +138,22 @@ export const ProductList = withStyles(styles, { name: "ProductList" })( - + - + {gridAttributesFromSettings.map(gridAttribute => ( ))} - + @@ -150,30 +165,59 @@ export const ProductList = withStyles(styles, { name: "ProductList" })( toggleAll={toggleAll} toolbar={toolbar} > - 4 })} + direction={ + sort.field === ProductOrderField.NAME + ? getArrowDirection(sort.direction) + : undefined + } + onClick={() => onSort(ProductOrderField.NAME)} > - - - + + + onSort(ProductOrderField.TYPE)} + > - + - - + + onSort(ProductOrderField.PUBLISHED)} + > - + {gridAttributesFromSettings.map(gridAttributeFromSettings => ( ))} - - + + onSort(ProductOrderField.PRICE)} + > - + @@ -246,7 +299,10 @@ export const ProductList = withStyles(styles, { name: "ProductList" })( > {maybe(() => product.name, )} - + {product && product.productType ? ( product.productType.name @@ -255,7 +311,10 @@ export const ProductList = withStyles(styles, { name: "ProductList" })( )} - + {product && maybe(() => product.isAvailable !== undefined) ? ( @@ -299,7 +358,10 @@ export const ProductList = withStyles(styles, { name: "ProductList" })( }, )} ))} - + {maybe(() => product.basePrice) && maybe(() => product.basePrice.amount) !== undefined && diff --git a/src/products/components/ProductListPage/ProductListPage.tsx b/src/products/components/ProductListPage/ProductListPage.tsx index 7f89ff158..97a4b72d1 100644 --- a/src/products/components/ProductListPage/ProductListPage.tsx +++ b/src/products/components/ProductListPage/ProductListPage.tsx @@ -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, ListActions, FilterPageProps, - FetchMoreProps { + FetchMoreProps, + SortPage { 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) => ({ diff --git a/src/products/index.tsx b/src/products/index.tsx index 0a8a1e767..85d080971 100644 --- a/src/products/index.tsx +++ b/src/products/index.tsx @@ -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> = ({ location }) => { const qs = parseQs(location.search.substr(1)); - const params: ProductListUrlQueryParams = qs; + const params: ProductListUrlQueryParams = { + ...qs, + asc: parseBoolean(qs.asc) + }; return ; }; diff --git a/src/products/queries.ts b/src/products/queries.ts index 5d5086ec4..65c5948d7 100644 --- a/src/products/queries.ts +++ b/src/products/queries.ts @@ -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 { diff --git a/src/products/types/ProductList.ts b/src/products/types/ProductList.ts index ae5de0235..ab392aa3e 100644 --- a/src/products/types/ProductList.ts +++ b/src/products/types/ProductList.ts @@ -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; } diff --git a/src/products/urls.ts b/src/products/urls.ts index 7c0348d3f..4250470f4 100644 --- a/src/products/urls.ts +++ b/src/products/urls.ts @@ -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; +export enum ProductListUrlSortFields { + name = "name", + productType = "type", + status = "status", + price = "price" +} +export type ProductListUrlSort = Sort; export type ProductListUrlQueryParams = BulkAction & Dialog & ProductListUrlFilters & + ProductListUrlSort & Pagination & ActiveTab; export const productListUrl = (params?: ProductListUrlQueryParams): string => diff --git a/src/products/views/ProductList/ProductList.tsx b/src/products/views/ProductList/ProductList.tsx index a15798fa7..054e565ff 100644 --- a/src/products/views/ProductList/ProductList.tsx +++ b/src/products/views/ProductList/ProductList.tsx @@ -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 = ({ 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( () => ({ ...paginationState, - filter: getFilterVariables(params) + filter, + sort }), [params, settings.rowNumber] ); @@ -224,6 +229,19 @@ export const ProductList: React.StatelessComponent = ({ return ( <> + navigate( + productListUrl({ + ...params, + ...getSortUrlVariables( + field, + sort.field, + params + ) + }) + ) + } availableInGridAttributes={maybe( () => attributes.data.availableInGrid.edges.map( diff --git a/src/products/views/ProductList/sort.ts b/src/products/views/ProductList/sort.ts new file mode 100644 index 000000000..5ef794e36 --- /dev/null +++ b/src/products/views/ProductList/sort.ts @@ -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 +): Sort { + if (field === selectedField) { + return { + asc: !params.asc, + sort: getSortUrlField(field) + }; + } + + return { + asc: true, + sort: getSortUrlField(field) + }; +} diff --git a/src/types.ts b/src/types.ts index f47628fa7..bebdb22c1 100644 --- a/src/types.ts +++ b/src/types.ts @@ -47,6 +47,10 @@ export interface ListProps { ) => void; onListSettingsReset?: () => void; } + +export interface SortPage { + 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 = Partial< export type SingleAction = Partial<{ id: string; }>; +export type Sort = Partial<{ + asc: boolean; + sort: TSort; +}>; export type BulkAction = Partial<{ ids: string[]; }>; diff --git a/src/types/globalTypes.ts b/src/types/globalTypes.ts index 420441a4f..e4d1e84ac 100644 --- a/src/types/globalTypes.ts +++ b/src/types/globalTypes.ts @@ -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; diff --git a/src/utils/columns/DisplayColumn.tsx b/src/utils/columns/DisplayColumn.tsx index 72dd32898..af48b0f93 100644 --- a/src/utils/columns/DisplayColumn.tsx +++ b/src/utils/columns/DisplayColumn.tsx @@ -12,12 +12,12 @@ const DisplayColumn: React.FC = ({ 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";