This commit is contained in:
dominik-zeglen 2019-08-13 11:04:52 +02:00
parent 0dcf4b2c5f
commit bc2c6a9c71
8 changed files with 293 additions and 57 deletions

View file

@ -33,10 +33,14 @@ const ColumnPicker: React.FC<ColumnPickerProps> = props => {
const { const {
className, className,
columns, columns,
hasMore,
initial = false, initial = false,
loading,
selectedColumns, selectedColumns,
total,
onCancel, onCancel,
onColumnToggle, onColumnToggle,
onFetchMore,
onReset, onReset,
onSave onSave
} = props; } = props;
@ -86,9 +90,13 @@ const ColumnPicker: React.FC<ColumnPickerProps> = props => {
> >
<ColumnPickerContent <ColumnPickerContent
columns={columns} columns={columns}
hasMore={hasMore}
loading={loading}
selectedColumns={selectedColumns} selectedColumns={selectedColumns}
total={total}
onCancel={handleCancel} onCancel={handleCancel}
onColumnToggle={onColumnToggle} onColumnToggle={onColumnToggle}
onFetchMore={onFetchMore}
onReset={onReset} onReset={onReset}
onSave={handleSave} onSave={handleSave}
/> />

View file

@ -1,15 +1,18 @@
import Button from "@material-ui/core/Button"; import Button from "@material-ui/core/Button";
import Card from "@material-ui/core/Card"; import Card from "@material-ui/core/Card";
import CardContent from "@material-ui/core/CardContent"; import CardContent from "@material-ui/core/CardContent";
import CircularProgress from "@material-ui/core/CircularProgress";
import { Theme } from "@material-ui/core/styles"; import { Theme } from "@material-ui/core/styles";
import Typography from "@material-ui/core/Typography"; import Typography from "@material-ui/core/Typography";
import makeStyles from "@material-ui/styles/makeStyles"; import makeStyles from "@material-ui/styles/makeStyles";
import classNames from "classnames"; import classNames from "classnames";
import React from "react"; import React from "react";
import InfiniteScroll from "react-infinite-scroller";
import { FormattedMessage } from "react-intl"; import { FormattedMessage } from "react-intl";
import useElementScroll from "@saleor/hooks/useElementScroll"; import useElementScroll from "@saleor/hooks/useElementScroll";
import { buttonMessages } from "@saleor/intl"; import { buttonMessages } from "@saleor/intl";
import { FetchMoreProps } from "@saleor/types";
import { isSelected } from "@saleor/utils/lists"; import { isSelected } from "@saleor/utils/lists";
import ControlledCheckbox from "../ControlledCheckbox"; import ControlledCheckbox from "../ControlledCheckbox";
import Hr from "../Hr"; import Hr from "../Hr";
@ -18,9 +21,10 @@ export interface ColumnPickerChoice {
label: string; label: string;
value: string; value: string;
} }
export interface ColumnPickerContentProps { export interface ColumnPickerContentProps extends Partial<FetchMoreProps> {
columns: ColumnPickerChoice[]; columns: ColumnPickerChoice[];
selectedColumns: string[]; selectedColumns: string[];
total?: number;
onCancel: () => void; onCancel: () => void;
onColumnToggle: (column: string) => void; onColumnToggle: (column: string) => void;
onReset: () => void; onReset: () => void;
@ -50,15 +54,26 @@ const useStyles = makeStyles((theme: Theme) => ({
}, },
dropShadow: { dropShadow: {
boxShadow: `0px -5px 10px 0px ${theme.overrides.MuiCard.root.borderColor}` 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<ColumnPickerContentProps> = props => { const ColumnPickerContent: React.FC<ColumnPickerContentProps> = props => {
const { const {
columns, columns,
hasMore,
loading,
selectedColumns, selectedColumns,
total,
onCancel, onCancel,
onColumnToggle, onColumnToggle,
onFetchMore,
onReset, onReset,
onSave onSave
} = props; } = props;
@ -80,28 +95,61 @@ const ColumnPickerContent: React.FC<ColumnPickerContentProps> = props => {
description="pick columns to display" description="pick columns to display"
values={{ values={{
numberOfSelected: selectedColumns.length, numberOfSelected: selectedColumns.length,
numberOfTotal: columns.length numberOfTotal: total || columns.length
}} }}
/> />
</Typography> </Typography>
</CardContent> </CardContent>
<Hr /> <Hr />
<CardContent className={classes.contentContainer}> {hasMore && onFetchMore ? (
<div className={classes.content} ref={anchor}> <InfiniteScroll
{columns.map(column => ( pageStart={0}
<ControlledCheckbox loadMore={onFetchMore}
checked={isSelected( hasMore={hasMore}
column.value, useWindow={false}
selectedColumns, threshold={100}
(a, b) => a === b key="infinite-scroll"
>
<CardContent className={classes.contentContainer}>
<div className={classes.content} ref={anchor}>
{columns.map(column => (
<ControlledCheckbox
checked={isSelected(
column.value,
selectedColumns,
(a, b) => a === b
)}
name={column.value}
label={column.label}
onChange={() => onColumnToggle(column.value)}
/>
))}
{loading && (
<div className={classes.loadMoreLoaderContainer}>
<CircularProgress size={16} />
</div>
)} )}
name={column.value} </div>
label={column.label} </CardContent>
onChange={() => onColumnToggle(column.value)} </InfiniteScroll>
/> ) : (
))} <CardContent className={classes.contentContainer}>
</div> <div className={classes.content} ref={anchor}>
</CardContent> {columns.map(column => (
<ControlledCheckbox
checked={isSelected(
column.value,
selectedColumns,
(a, b) => a === b
)}
name={column.value}
label={column.label}
onChange={() => onColumnToggle(column.value)}
/>
))}
</div>
</CardContent>
)}
<Hr /> <Hr />
<CardContent <CardContent
className={classNames(classes.actionBarContainer, { className={classNames(classes.actionBarContainer, {

View file

@ -16,7 +16,13 @@ import ProductList from "@saleor/components/ProductList";
import { ProductListColumns } from "@saleor/config"; import { ProductListColumns } from "@saleor/config";
import useStateFromProps from "@saleor/hooks/useStateFromProps"; import useStateFromProps from "@saleor/hooks/useStateFromProps";
import { sectionNames } from "@saleor/intl"; import { sectionNames } from "@saleor/intl";
import { FilterPageProps, ListActions, PageListProps } from "@saleor/types"; import { AvailableInGridAttributes_attributes_edges_node } from "@saleor/products/types/AvailableInGridAttributes";
import {
FetchMoreProps,
FilterPageProps,
ListActions,
PageListProps
} from "@saleor/types";
import { toggle } from "@saleor/utils/lists"; import { toggle } from "@saleor/utils/lists";
import { ProductListUrlFilters } from "../../urls"; import { ProductListUrlFilters } from "../../urls";
import ProductListFilter from "../ProductListFilter"; import ProductListFilter from "../ProductListFilter";
@ -24,8 +30,11 @@ import ProductListFilter from "../ProductListFilter";
export interface ProductListPageProps export interface ProductListPageProps
extends PageListProps<ProductListColumns>, extends PageListProps<ProductListColumns>,
ListActions, ListActions,
FilterPageProps<ProductListUrlFilters> { FilterPageProps<ProductListUrlFilters>,
FetchMoreProps {
currencySymbol: string; currencySymbol: string;
gridAttributes: AvailableInGridAttributes_attributes_edges_node[];
totalGridAttributes: number;
products: CategoryDetails_category_products_edges_node[]; products: CategoryDetails_category_products_edges_node[];
} }
@ -42,10 +51,15 @@ export const ProductListPage: React.FC<ProductListPageProps> = props => {
defaultSettings, defaultSettings,
filtersList, filtersList,
filterTabs, filterTabs,
gridAttributes,
hasMore,
initialSearch, initialSearch,
loading,
settings, settings,
totalGridAttributes,
onAdd, onAdd,
onAll, onAll,
onFetchMore,
onSearchChange, onSearchChange,
onFilterAdd, onFilterAdd,
onFilterSave, onFilterSave,
@ -95,7 +109,11 @@ export const ProductListPage: React.FC<ProductListPageProps> = props => {
description: "product type" description: "product type"
}), }),
value: "productType" as ProductListColumns value: "productType" as ProductListColumns
} },
...gridAttributes.map(attribute => ({
label: attribute.name,
value: `attribute:${attribute.id}`
}))
]; ];
return ( return (
@ -104,9 +122,13 @@ export const ProductListPage: React.FC<ProductListPageProps> = props => {
<ColumnPicker <ColumnPicker
className={classes.columnPicker} className={classes.columnPicker}
columns={columns} columns={columns}
hasMore={hasMore}
loading={loading}
selectedColumns={selectedColumns} selectedColumns={selectedColumns}
total={columns.length + totalGridAttributes}
onColumnToggle={handleColumnToggle} onColumnToggle={handleColumnToggle}
onCancel={handleCancel} onCancel={handleCancel}
onFetchMore={onFetchMore}
onReset={handleReset} onReset={handleReset}
onSave={handleSave} onSave={handleSave}
/> />

View file

@ -1,6 +1,10 @@
import gql from "graphql-tag"; 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 { ProductCreateData } from "./types/ProductCreateData";
import { import {
ProductDetails, ProductDetails,
@ -225,6 +229,15 @@ const productListQuery = gql`
edges { edges {
node { node {
...ProductFragment ...ProductFragment
attributes {
attribute {
id
}
values {
id
name
}
}
} }
} }
pageInfo { pageInfo {
@ -361,3 +374,29 @@ export const TypedProductImageQuery = TypedQuery<
ProductImageById, ProductImageById,
ProductImageByIdVariables ProductImageByIdVariables
>(productImageQuery); >(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);

View file

@ -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;
}

View file

@ -25,6 +25,23 @@ export interface ProductList_products_edges_node_productType {
name: string; 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 { export interface ProductList_products_edges_node {
__typename: "Product"; __typename: "Product";
id: string; id: string;
@ -33,6 +50,7 @@ export interface ProductList_products_edges_node {
isAvailable: boolean | null; isAvailable: boolean | null;
basePrice: ProductList_products_edges_node_basePrice | null; basePrice: ProductList_products_edges_node_basePrice | null;
productType: ProductList_products_edges_node_productType; productType: ProductList_products_edges_node_productType;
attributes: ProductList_products_edges_node_attributes[];
} }
export interface ProductList_products_edges { export interface ProductList_products_edges {

View file

@ -20,6 +20,7 @@ import usePaginator, {
createPaginationState createPaginationState
} from "@saleor/hooks/usePaginator"; } from "@saleor/hooks/usePaginator";
import useShop from "@saleor/hooks/useShop"; import useShop from "@saleor/hooks/useShop";
import { commonMessages } from "@saleor/intl";
import { getMutationState, maybe } from "@saleor/misc"; import { getMutationState, maybe } from "@saleor/misc";
import { ListViews } from "@saleor/types"; import { ListViews } from "@saleor/types";
import ProductListPage from "../../components/ProductListPage"; import ProductListPage from "../../components/ProductListPage";
@ -27,7 +28,10 @@ import {
TypedProductBulkDeleteMutation, TypedProductBulkDeleteMutation,
TypedProductBulkPublishMutation TypedProductBulkPublishMutation
} from "../../mutations"; } from "../../mutations";
import { TypedProductListQuery } from "../../queries"; import {
AvailableInGridAttributesQuery,
TypedProductListQuery
} from "../../queries";
import { productBulkDelete } from "../../types/productBulkDelete"; import { productBulkDelete } from "../../types/productBulkDelete";
import { productBulkPublish } from "../../types/productBulkPublish"; import { productBulkPublish } from "../../types/productBulkPublish";
import { import {
@ -145,21 +149,21 @@ export const ProductList: React.StatelessComponent<ProductListProps> = ({
); );
return ( return (
<TypedProductListQuery displayLoader variables={queryVariables}> <AvailableInGridAttributesQuery variables={{ first: 6 }}>
{({ data, loading, refetch }) => { {gridAttributes => (
const { loadNextPage, loadPreviousPage, pageInfo } = paginate( <TypedProductListQuery displayLoader variables={queryVariables}>
maybe(() => data.products.pageInfo), {({ data, loading, refetch }) => {
paginationState, const { loadNextPage, loadPreviousPage, pageInfo } = paginate(
params maybe(() => data.products.pageInfo),
); paginationState,
params
);
const handleBulkDelete = (data: productBulkDelete) => { const handleBulkDelete = (data: productBulkDelete) => {
if (data.productBulkDelete.errors.length === 0) { if (data.productBulkDelete.errors.length === 0) {
closeModal(); closeModal();
notify({ notify({
text: intl.formatMessage({ text: intl.formatMessage(commonMessages.savedChanges)
defaultMessage: "Products removed"
})
}); });
reset(); reset();
refetch(); refetch();
@ -170,37 +174,38 @@ export const ProductList: React.StatelessComponent<ProductListProps> = ({
if (data.productBulkPublish.errors.length === 0) { if (data.productBulkPublish.errors.length === 0) {
closeModal(); closeModal();
notify({ notify({
text: intl.formatMessage({ text: intl.formatMessage(commonMessages.savedChanges)
defaultMessage: "Changed publication status",
description: "product status update notification"
})
}); });
reset(); reset();
refetch(); refetch();
} }
}; };
return ( return (
<TypedProductBulkDeleteMutation onCompleted={handleBulkDelete}> <TypedProductBulkDeleteMutation onCompleted={handleBulkDelete}>
{(productBulkDelete, productBulkDeleteOpts) => ( {(productBulkDelete, productBulkDeleteOpts) => (
<TypedProductBulkPublishMutation onCompleted={handleBulkPublish}> <TypedProductBulkPublishMutation
{(productBulkPublish, productBulkPublishOpts) => { onCompleted={handleBulkPublish}
const bulkDeleteMutationState = getMutationState( >
productBulkDeleteOpts.called, {(productBulkPublish, productBulkPublishOpts) => {
productBulkDeleteOpts.loading, const bulkDeleteMutationState = getMutationState(
maybe( productBulkDeleteOpts.called,
() => productBulkDeleteOpts.data.productBulkDelete.errors productBulkDeleteOpts.loading,
) maybe(
); () =>
productBulkDeleteOpts.data.productBulkDelete.errors
)
);
const bulkPublishMutationState = getMutationState( const bulkPublishMutationState = getMutationState(
productBulkPublishOpts.called, productBulkPublishOpts.called,
productBulkPublishOpts.loading, productBulkPublishOpts.loading,
maybe( maybe(
() => () =>
productBulkPublishOpts.data.productBulkPublish.errors productBulkPublishOpts.data.productBulkPublish
) .errors
); )
);
return ( return (
<> <>
@ -210,7 +215,25 @@ export const ProductList: React.StatelessComponent<ProductListProps> = ({
defaultSettings={ defaultSettings={
defaultListSettings[ListViews.PRODUCT_LIST] defaultListSettings[ListViews.PRODUCT_LIST]
} }
gridAttributes={maybe(
() =>
gridAttributes.data.attributes.edges.map(
edge => edge.node
),
[]
)}
totalGridAttributes={maybe(
() => gridAttributes.data.attributes.totalCount,
0
)}
settings={settings} settings={settings}
loading={gridAttributes.loading}
hasMore={maybe(
() =>
gridAttributes.data.attributes.pageInfo
.hasNextPage,
false
)}
filtersList={createFilterChips( filtersList={createFilterChips(
params, params,
{ {
@ -225,6 +248,34 @@ export const ProductList: React.StatelessComponent<ProductListProps> = ({
products={maybe(() => products={maybe(() =>
data.products.edges.map(edge => edge.node) 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} onNextPage={loadNextPage}
onPreviousPage={loadPreviousPage} onPreviousPage={loadPreviousPage}
onUpdateListSettings={updateListSettings} onUpdateListSettings={updateListSettings}

View file

@ -42,4 +42,12 @@ storiesOf("Generics / Column picker", module)
)) ))
.addDecorator(CardDecorator) .addDecorator(CardDecorator)
.addDecorator(Decorator) .addDecorator(Decorator)
.add("default", () => <ColumnPicker {...props} />); .add("default", () => <ColumnPicker {...props} />)
.add("loading", () => (
<ColumnPicker
{...props}
loading={true}
hasMore={true}
onFetchMore={() => undefined}
/>
));