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

View file

@ -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<FetchMoreProps> {
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<ColumnPickerContentProps> = props => {
const {
columns,
hasMore,
loading,
selectedColumns,
total,
onCancel,
onColumnToggle,
onFetchMore,
onReset,
onSave
} = props;
@ -80,28 +95,61 @@ const ColumnPickerContent: React.FC<ColumnPickerContentProps> = props => {
description="pick columns to display"
values={{
numberOfSelected: selectedColumns.length,
numberOfTotal: columns.length
numberOfTotal: total || columns.length
}}
/>
</Typography>
</CardContent>
<Hr />
<CardContent className={classes.contentContainer}>
<div className={classes.content} ref={anchor}>
{columns.map(column => (
<ControlledCheckbox
checked={isSelected(
column.value,
selectedColumns,
(a, b) => a === b
{hasMore && onFetchMore ? (
<InfiniteScroll
pageStart={0}
loadMore={onFetchMore}
hasMore={hasMore}
useWindow={false}
threshold={100}
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}
label={column.label}
onChange={() => onColumnToggle(column.value)}
/>
))}
</div>
</CardContent>
</div>
</CardContent>
</InfiniteScroll>
) : (
<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)}
/>
))}
</div>
</CardContent>
)}
<Hr />
<CardContent
className={classNames(classes.actionBarContainer, {

View file

@ -16,7 +16,13 @@ import ProductList from "@saleor/components/ProductList";
import { ProductListColumns } from "@saleor/config";
import useStateFromProps from "@saleor/hooks/useStateFromProps";
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 { ProductListUrlFilters } from "../../urls";
import ProductListFilter from "../ProductListFilter";
@ -24,8 +30,11 @@ import ProductListFilter from "../ProductListFilter";
export interface ProductListPageProps
extends PageListProps<ProductListColumns>,
ListActions,
FilterPageProps<ProductListUrlFilters> {
FilterPageProps<ProductListUrlFilters>,
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<ProductListPageProps> = 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<ProductListPageProps> = 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<ProductListPageProps> = props => {
<ColumnPicker
className={classes.columnPicker}
columns={columns}
hasMore={hasMore}
loading={loading}
selectedColumns={selectedColumns}
total={columns.length + totalGridAttributes}
onColumnToggle={handleColumnToggle}
onCancel={handleCancel}
onFetchMore={onFetchMore}
onReset={handleReset}
onSave={handleSave}
/>

View file

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

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

View file

@ -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<ProductListProps> = ({
);
return (
<TypedProductListQuery displayLoader variables={queryVariables}>
{({ data, loading, refetch }) => {
const { loadNextPage, loadPreviousPage, pageInfo } = paginate(
maybe(() => data.products.pageInfo),
paginationState,
params
);
<AvailableInGridAttributesQuery variables={{ first: 6 }}>
{gridAttributes => (
<TypedProductListQuery displayLoader variables={queryVariables}>
{({ 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<ProductListProps> = ({
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 (
<TypedProductBulkDeleteMutation onCompleted={handleBulkDelete}>
{(productBulkDelete, productBulkDeleteOpts) => (
<TypedProductBulkPublishMutation onCompleted={handleBulkPublish}>
{(productBulkPublish, productBulkPublishOpts) => {
const bulkDeleteMutationState = getMutationState(
productBulkDeleteOpts.called,
productBulkDeleteOpts.loading,
maybe(
() => productBulkDeleteOpts.data.productBulkDelete.errors
)
);
return (
<TypedProductBulkDeleteMutation onCompleted={handleBulkDelete}>
{(productBulkDelete, productBulkDeleteOpts) => (
<TypedProductBulkPublishMutation
onCompleted={handleBulkPublish}
>
{(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<ProductListProps> = ({
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<ProductListProps> = ({
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}

View file

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