wip
This commit is contained in:
parent
0dcf4b2c5f
commit
bc2c6a9c71
8 changed files with 293 additions and 57 deletions
|
@ -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}
|
||||
/>
|
||||
|
|
|
@ -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, {
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
@ -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);
|
||||
|
|
42
src/products/types/AvailableInGridAttributes.ts
Normal file
42
src/products/types/AvailableInGridAttributes.ts
Normal 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;
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
));
|
||||
|
|
Loading…
Reference in a new issue