diff --git a/src/components/ColumnPicker/ColumnPicker.tsx b/src/components/ColumnPicker/ColumnPicker.tsx index a0d72cb09..b2255e8b3 100644 --- a/src/components/ColumnPicker/ColumnPicker.tsx +++ b/src/components/ColumnPicker/ColumnPicker.tsx @@ -33,10 +33,14 @@ const ColumnPicker: React.FC = props => { const { className, columns, + hasMore, initial = false, + loading, selectedColumns, + total, onCancel, onColumnToggle, + onFetchMore, onReset, onSave } = props; @@ -86,9 +90,13 @@ const ColumnPicker: React.FC = props => { > diff --git a/src/components/ColumnPicker/ColumnPickerContent.tsx b/src/components/ColumnPicker/ColumnPickerContent.tsx index 52473159d..8fbc42525 100644 --- a/src/components/ColumnPicker/ColumnPickerContent.tsx +++ b/src/components/ColumnPicker/ColumnPickerContent.tsx @@ -1,15 +1,18 @@ import Button from "@material-ui/core/Button"; import Card from "@material-ui/core/Card"; import CardContent from "@material-ui/core/CardContent"; +import CircularProgress from "@material-ui/core/CircularProgress"; import { Theme } from "@material-ui/core/styles"; import Typography from "@material-ui/core/Typography"; import makeStyles from "@material-ui/styles/makeStyles"; import classNames from "classnames"; import React from "react"; +import InfiniteScroll from "react-infinite-scroller"; import { FormattedMessage } from "react-intl"; import useElementScroll from "@saleor/hooks/useElementScroll"; import { buttonMessages } from "@saleor/intl"; +import { FetchMoreProps } from "@saleor/types"; import { isSelected } from "@saleor/utils/lists"; import ControlledCheckbox from "../ControlledCheckbox"; import Hr from "../Hr"; @@ -18,9 +21,10 @@ export interface ColumnPickerChoice { label: string; value: string; } -export interface ColumnPickerContentProps { +export interface ColumnPickerContentProps extends Partial { columns: ColumnPickerChoice[]; selectedColumns: string[]; + total?: number; onCancel: () => void; onColumnToggle: (column: string) => void; onReset: () => void; @@ -50,15 +54,26 @@ const useStyles = makeStyles((theme: Theme) => ({ }, dropShadow: { boxShadow: `0px -5px 10px 0px ${theme.overrides.MuiCard.root.borderColor}` + }, + loadMoreLoaderContainer: { + alignItems: "center", + display: "flex", + gridColumnEnd: "span 3", + height: theme.spacing.unit * 3, + justifyContent: "center" } })); const ColumnPickerContent: React.FC = props => { const { columns, + hasMore, + loading, selectedColumns, + total, onCancel, onColumnToggle, + onFetchMore, onReset, onSave } = props; @@ -80,28 +95,61 @@ const ColumnPickerContent: React.FC = props => { description="pick columns to display" values={{ numberOfSelected: selectedColumns.length, - numberOfTotal: columns.length + numberOfTotal: total || columns.length }} />
- -
- {columns.map(column => ( - a === b + {hasMore && onFetchMore ? ( + + +
+ {columns.map(column => ( + a === b + )} + name={column.value} + label={column.label} + onChange={() => onColumnToggle(column.value)} + /> + ))} + {loading && ( +
+ +
)} - name={column.value} - label={column.label} - onChange={() => onColumnToggle(column.value)} - /> - ))} -
-
+
+
+ + ) : ( + +
+ {columns.map(column => ( + a === b + )} + name={column.value} + label={column.label} + onChange={() => onColumnToggle(column.value)} + /> + ))} +
+
+ )}
, ListActions, - FilterPageProps { + FilterPageProps, + FetchMoreProps { currencySymbol: string; + gridAttributes: AvailableInGridAttributes_attributes_edges_node[]; + totalGridAttributes: number; products: CategoryDetails_category_products_edges_node[]; } @@ -42,10 +51,15 @@ export const ProductListPage: React.FC = props => { defaultSettings, filtersList, filterTabs, + gridAttributes, + hasMore, initialSearch, + loading, settings, + totalGridAttributes, onAdd, onAll, + onFetchMore, onSearchChange, onFilterAdd, onFilterSave, @@ -95,7 +109,11 @@ export const ProductListPage: React.FC = props => { description: "product type" }), value: "productType" as ProductListColumns - } + }, + ...gridAttributes.map(attribute => ({ + label: attribute.name, + value: `attribute:${attribute.id}` + })) ]; return ( @@ -104,9 +122,13 @@ export const ProductListPage: React.FC = props => { diff --git a/src/products/queries.ts b/src/products/queries.ts index 6bf4516f7..e460582fc 100644 --- a/src/products/queries.ts +++ b/src/products/queries.ts @@ -1,6 +1,10 @@ import gql from "graphql-tag"; -import { TypedQuery } from "../queries"; +import { pageInfoFragment, TypedQuery } from "../queries"; +import { + AvailableInGridAttributes, + AvailableInGridAttributesVariables +} from "./types/AvailableInGridAttributes"; import { ProductCreateData } from "./types/ProductCreateData"; import { ProductDetails, @@ -225,6 +229,15 @@ const productListQuery = gql` edges { node { ...ProductFragment + attributes { + attribute { + id + } + values { + id + name + } + } } } pageInfo { @@ -361,3 +374,29 @@ export const TypedProductImageQuery = TypedQuery< ProductImageById, ProductImageByIdVariables >(productImageQuery); + +const availableInGridAttributes = gql` + ${pageInfoFragment} + query AvailableInGridAttributes($first: Int!, $after: String) { + attributes( + first: $first + after: $after + filter: { availableInGrid: true } + ) { + edges { + node { + id + name + } + } + pageInfo { + ...PageInfoFragment + } + totalCount + } + } +`; +export const AvailableInGridAttributesQuery = TypedQuery< + AvailableInGridAttributes, + AvailableInGridAttributesVariables +>(availableInGridAttributes); diff --git a/src/products/types/AvailableInGridAttributes.ts b/src/products/types/AvailableInGridAttributes.ts new file mode 100644 index 000000000..d18d4d94f --- /dev/null +++ b/src/products/types/AvailableInGridAttributes.ts @@ -0,0 +1,42 @@ +/* tslint:disable */ +/* eslint-disable */ +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL query operation: AvailableInGridAttributes +// ==================================================== + +export interface AvailableInGridAttributes_attributes_edges_node { + __typename: "Attribute"; + id: string; + name: string | null; +} + +export interface AvailableInGridAttributes_attributes_edges { + __typename: "AttributeCountableEdge"; + node: AvailableInGridAttributes_attributes_edges_node; +} + +export interface AvailableInGridAttributes_attributes_pageInfo { + __typename: "PageInfo"; + endCursor: string | null; + hasNextPage: boolean; + hasPreviousPage: boolean; + startCursor: string | null; +} + +export interface AvailableInGridAttributes_attributes { + __typename: "AttributeCountableConnection"; + edges: AvailableInGridAttributes_attributes_edges[]; + pageInfo: AvailableInGridAttributes_attributes_pageInfo; + totalCount: number | null; +} + +export interface AvailableInGridAttributes { + attributes: AvailableInGridAttributes_attributes | null; +} + +export interface AvailableInGridAttributesVariables { + first: number; + after?: string | null; +} diff --git a/src/products/types/ProductList.ts b/src/products/types/ProductList.ts index 075889875..ae5de0235 100644 --- a/src/products/types/ProductList.ts +++ b/src/products/types/ProductList.ts @@ -25,6 +25,23 @@ export interface ProductList_products_edges_node_productType { name: string; } +export interface ProductList_products_edges_node_attributes_attribute { + __typename: "Attribute"; + id: string; +} + +export interface ProductList_products_edges_node_attributes_values { + __typename: "AttributeValue"; + id: string; + name: string | null; +} + +export interface ProductList_products_edges_node_attributes { + __typename: "SelectedAttribute"; + attribute: ProductList_products_edges_node_attributes_attribute; + values: (ProductList_products_edges_node_attributes_values | null)[]; +} + export interface ProductList_products_edges_node { __typename: "Product"; id: string; @@ -33,6 +50,7 @@ export interface ProductList_products_edges_node { isAvailable: boolean | null; basePrice: ProductList_products_edges_node_basePrice | null; productType: ProductList_products_edges_node_productType; + attributes: ProductList_products_edges_node_attributes[]; } export interface ProductList_products_edges { diff --git a/src/products/views/ProductList/ProductList.tsx b/src/products/views/ProductList/ProductList.tsx index 39db0c7e6..0c08b1d5a 100644 --- a/src/products/views/ProductList/ProductList.tsx +++ b/src/products/views/ProductList/ProductList.tsx @@ -20,6 +20,7 @@ import usePaginator, { createPaginationState } from "@saleor/hooks/usePaginator"; import useShop from "@saleor/hooks/useShop"; +import { commonMessages } from "@saleor/intl"; import { getMutationState, maybe } from "@saleor/misc"; import { ListViews } from "@saleor/types"; import ProductListPage from "../../components/ProductListPage"; @@ -27,7 +28,10 @@ import { TypedProductBulkDeleteMutation, TypedProductBulkPublishMutation } from "../../mutations"; -import { TypedProductListQuery } from "../../queries"; +import { + AvailableInGridAttributesQuery, + TypedProductListQuery +} from "../../queries"; import { productBulkDelete } from "../../types/productBulkDelete"; import { productBulkPublish } from "../../types/productBulkPublish"; import { @@ -145,21 +149,21 @@ export const ProductList: React.StatelessComponent = ({ ); return ( - - {({ data, loading, refetch }) => { - const { loadNextPage, loadPreviousPage, pageInfo } = paginate( - maybe(() => data.products.pageInfo), - paginationState, - params - ); + + {gridAttributes => ( + + {({ data, loading, refetch }) => { + const { loadNextPage, loadPreviousPage, pageInfo } = paginate( + maybe(() => data.products.pageInfo), + paginationState, + params + ); const handleBulkDelete = (data: productBulkDelete) => { if (data.productBulkDelete.errors.length === 0) { closeModal(); notify({ - text: intl.formatMessage({ - defaultMessage: "Products removed" - }) + text: intl.formatMessage(commonMessages.savedChanges) }); reset(); refetch(); @@ -170,37 +174,38 @@ export const ProductList: React.StatelessComponent = ({ if (data.productBulkPublish.errors.length === 0) { closeModal(); notify({ - text: intl.formatMessage({ - defaultMessage: "Changed publication status", - description: "product status update notification" - }) + text: intl.formatMessage(commonMessages.savedChanges) }); reset(); refetch(); } }; - return ( - - {(productBulkDelete, productBulkDeleteOpts) => ( - - {(productBulkPublish, productBulkPublishOpts) => { - const bulkDeleteMutationState = getMutationState( - productBulkDeleteOpts.called, - productBulkDeleteOpts.loading, - maybe( - () => productBulkDeleteOpts.data.productBulkDelete.errors - ) - ); + return ( + + {(productBulkDelete, productBulkDeleteOpts) => ( + + {(productBulkPublish, productBulkPublishOpts) => { + const bulkDeleteMutationState = getMutationState( + productBulkDeleteOpts.called, + productBulkDeleteOpts.loading, + maybe( + () => + productBulkDeleteOpts.data.productBulkDelete.errors + ) + ); - const bulkPublishMutationState = getMutationState( - productBulkPublishOpts.called, - productBulkPublishOpts.loading, - maybe( - () => - productBulkPublishOpts.data.productBulkPublish.errors - ) - ); + const bulkPublishMutationState = getMutationState( + productBulkPublishOpts.called, + productBulkPublishOpts.loading, + maybe( + () => + productBulkPublishOpts.data.productBulkPublish + .errors + ) + ); return ( <> @@ -210,7 +215,25 @@ export const ProductList: React.StatelessComponent = ({ defaultSettings={ defaultListSettings[ListViews.PRODUCT_LIST] } + gridAttributes={maybe( + () => + gridAttributes.data.attributes.edges.map( + edge => edge.node + ), + [] + )} + totalGridAttributes={maybe( + () => gridAttributes.data.attributes.totalCount, + 0 + )} settings={settings} + loading={gridAttributes.loading} + hasMore={maybe( + () => + gridAttributes.data.attributes.pageInfo + .hasNextPage, + false + )} filtersList={createFilterChips( params, { @@ -225,6 +248,34 @@ export const ProductList: React.StatelessComponent = ({ products={maybe(() => data.products.edges.map(edge => edge.node) )} + onFetchMore={() => + gridAttributes.loadMore( + (prev, next) => { + if ( + prev.attributes.pageInfo.endCursor === + next.attributes.pageInfo.endCursor + ) { + return prev; + } + return { + ...prev, + attributes: { + ...prev.attributes, + edges: [ + ...prev.attributes.edges, + ...next.attributes.edges + ], + pageInfo: next.attributes.pageInfo + } + }; + }, + { + after: + gridAttributes.data.attributes.pageInfo + .endCursor + } + ) + } onNextPage={loadNextPage} onPreviousPage={loadPreviousPage} onUpdateListSettings={updateListSettings} diff --git a/src/storybook/stories/components/ColumnPicker.tsx b/src/storybook/stories/components/ColumnPicker.tsx index 9da2bc9cc..f23ec4824 100644 --- a/src/storybook/stories/components/ColumnPicker.tsx +++ b/src/storybook/stories/components/ColumnPicker.tsx @@ -42,4 +42,12 @@ storiesOf("Generics / Column picker", module) )) .addDecorator(CardDecorator) .addDecorator(Decorator) - .add("default", () => ); + .add("default", () => ) + .add("loading", () => ( + undefined} + /> + ));