Merge pull request #136 from mirumee/add/attribute-columns

Add attributes to column picker
This commit is contained in:
Dominik Żegleń 2019-09-02 12:41:52 +02:00 committed by GitHub
commit 94eeca32f2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
35 changed files with 4404 additions and 2983 deletions

View file

@ -12,3 +12,4 @@ All notable, unreleased changes to this project will be documented in this file.
- Use react-intl - #105 by @dominik-zeglen - Use react-intl - #105 by @dominik-zeglen
- Add dynamic dashboard settings - #135 by @benekex2 - Add dynamic dashboard settings - #135 by @benekex2
- Fix plugins page translations - #141 by @benekex2 - Fix plugins page translations - #141 by @benekex2
- Add attributes to column picker - #136 by @dominik-zeglen

View file

@ -0,0 +1,243 @@
import {
createStyles,
Theme,
withStyles,
WithStyles
} from "@material-ui/core/styles";
import Table from "@material-ui/core/Table";
import TableBody from "@material-ui/core/TableBody";
import TableCell from "@material-ui/core/TableCell";
import TableFooter from "@material-ui/core/TableFooter";
import TableRow from "@material-ui/core/TableRow";
import Checkbox from "@saleor/components/Checkbox";
import Money from "@saleor/components/Money";
import Skeleton from "@saleor/components/Skeleton";
import StatusLabel from "@saleor/components/StatusLabel";
import { FormattedMessage, useIntl } from "react-intl";
import TableCellAvatar, {
AVATAR_MARGIN
} from "@saleor/components/TableCellAvatar";
import TableHead from "@saleor/components/TableHead";
import TablePagination from "@saleor/components/TablePagination";
import { maybe, renderCollection } from "@saleor/misc";
import { ListActions, ListProps } from "@saleor/types";
import React from "react";
import { CategoryDetails_category_products_edges_node } from "../../types/CategoryDetails";
const styles = (theme: Theme) =>
createStyles({
[theme.breakpoints.up("lg")]: {
colName: {
width: "auto"
},
colPrice: {
width: 200
},
colPublished: {
width: 200
},
colType: {
width: 200
}
},
colFill: {
padding: 0,
width: "100%"
},
colName: {},
colNameHeader: {
marginLeft: AVATAR_MARGIN
},
colPrice: {
textAlign: "right"
},
colPublished: {},
colType: {},
link: {
cursor: "pointer"
},
table: {
tableLayout: "fixed"
},
tableContainer: {
overflowX: "scroll"
},
textLeft: {
textAlign: "left"
},
textRight: {
textAlign: "right"
}
});
interface CategoryProductListProps
extends ListProps,
ListActions,
WithStyles<typeof styles> {
products: CategoryDetails_category_products_edges_node[];
}
export const CategoryProductList = withStyles(styles, {
name: "CategoryProductList"
})(
({
classes,
disabled,
isChecked,
pageInfo,
products,
selected,
toggle,
toggleAll,
toolbar,
onNextPage,
onPreviousPage,
onRowClick
}: CategoryProductListProps) => {
const intl = useIntl();
const numberOfColumns = 5;
return (
<div className={classes.tableContainer}>
<Table className={classes.table}>
<colgroup>
<col />
<col className={classes.colName} />
<col className={classes.colType} />
<col className={classes.colPublished} />
<col className={classes.colPrice} />
</colgroup>
<TableHead
colSpan={numberOfColumns}
selected={selected}
disabled={disabled}
items={products}
toggleAll={toggleAll}
toolbar={toolbar}
>
<TableCell className={classes.colName}>
<span className={classes.colNameHeader}>
<FormattedMessage defaultMessage="Name" description="product" />
</span>
</TableCell>
<TableCell className={classes.colType}>
<FormattedMessage
defaultMessage="Type"
description="product type"
/>
</TableCell>
<TableCell className={classes.colPublished}>
<FormattedMessage
defaultMessage="Published"
description="product status"
/>
</TableCell>
<TableCell className={classes.colPrice}>
<FormattedMessage
defaultMessage="Price"
description="product price"
/>
</TableCell>
</TableHead>
<TableFooter>
<TableRow>
<TablePagination
colSpan={numberOfColumns}
hasNextPage={
pageInfo && !disabled ? pageInfo.hasNextPage : false
}
onNextPage={onNextPage}
hasPreviousPage={
pageInfo && !disabled ? pageInfo.hasPreviousPage : false
}
onPreviousPage={onPreviousPage}
/>
</TableRow>
</TableFooter>
<TableBody>
{renderCollection(
products,
product => {
const isSelected = product ? isChecked(product.id) : false;
return (
<TableRow
selected={isSelected}
hover={!!product}
key={product ? product.id : "skeleton"}
onClick={product && onRowClick(product.id)}
className={classes.link}
>
<TableCell padding="checkbox">
<Checkbox
checked={isSelected}
disabled={disabled}
disableClickPropagation
onChange={() => toggle(product.id)}
/>
</TableCell>
<TableCellAvatar
className={classes.colName}
thumbnail={maybe(() => product.thumbnail.url)}
>
{product ? product.name : <Skeleton />}
</TableCellAvatar>
<TableCell className={classes.colType}>
{product && product.productType ? (
product.productType.name
) : (
<Skeleton />
)}
</TableCell>
<TableCell className={classes.colPublished}>
{product &&
maybe(() => product.isAvailable !== undefined) ? (
<StatusLabel
label={
product.isAvailable
? intl.formatMessage({
defaultMessage: "Published",
description: "product",
id: "productStatusLabel"
})
: intl.formatMessage({
defaultMessage: "Not published",
description: "product"
})
}
status={product.isAvailable ? "success" : "error"}
/>
) : (
<Skeleton />
)}
</TableCell>
<TableCell className={classes.colPrice}>
{maybe(() => product.basePrice) &&
maybe(() => product.basePrice.amount) !== undefined &&
maybe(() => product.basePrice.currency) !== undefined ? (
<Money money={product.basePrice} />
) : (
<Skeleton />
)}
</TableCell>
</TableRow>
);
},
() => (
<TableRow>
<TableCell colSpan={numberOfColumns}>
<FormattedMessage defaultMessage="No products found" />
</TableCell>
</TableRow>
)
)}
</TableBody>
</Table>
</div>
);
}
);
CategoryProductList.displayName = "CategoryProductList";
export default CategoryProductList;

View file

@ -0,0 +1,2 @@
export { default } from "./CategoryProductList";
export * from "./CategoryProductList";

View file

@ -1,156 +1,72 @@
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 {
createStyles,
Theme,
withStyles,
WithStyles
} from "@material-ui/core/styles";
import Table from "@material-ui/core/Table";
import TableBody from "@material-ui/core/TableBody";
import TableCell from "@material-ui/core/TableCell";
import TableFooter from "@material-ui/core/TableFooter";
import TableHead from "@material-ui/core/TableHead";
import TableRow from "@material-ui/core/TableRow";
import React from "react"; import React from "react";
import { FormattedMessage, useIntl } from "react-intl"; import { FormattedMessage, useIntl } from "react-intl";
import CardTitle from "@saleor/components/CardTitle"; import CardTitle from "@saleor/components/CardTitle";
import Skeleton from "@saleor/components/Skeleton"; import { ListActions, PageListProps } from "../../../types";
import TableCellAvatar from "@saleor/components/TableCellAvatar"; import { CategoryDetails_category_products_edges_node } from "../../types/CategoryDetails";
import TablePagination from "@saleor/components/TablePagination"; import CategoryProductList from "../CategoryProductList";
import { maybe, renderCollection } from "@saleor/misc";
const styles = (theme: Theme) => interface CategoryProductsProps extends PageListProps, ListActions {
createStyles({ products: CategoryDetails_category_products_edges_node[];
link: { categoryName: string;
color: theme.palette.primary.main,
cursor: "pointer"
},
textLeft: {
textAlign: "left"
}
});
interface ProductListProps extends WithStyles<typeof styles> {
hasNextPage?: boolean;
hasPreviousPage?: boolean;
products?: Array<{
id: string;
name: string;
productType: {
name: string;
};
thumbnail: {
url: string;
};
}>;
onAddProduct?();
onNextPage?();
onPreviousPage?();
onRowClick?(id: string): () => void;
} }
export const ProductList = withStyles(styles, { name: "ProductList" })( export const CategoryProducts: React.StatelessComponent<
({ CategoryProductsProps
classes, > = ({
hasNextPage, products,
hasPreviousPage, disabled,
products, pageInfo,
onAddProduct, onAdd,
onNextPage, onNextPage,
onPreviousPage, onPreviousPage,
onRowClick onRowClick,
}: ProductListProps) => { categoryName,
const intl = useIntl(); isChecked,
selected,
toggle,
toggleAll,
toolbar
}) => {
const intl = useIntl();
return ( return (
<Card> <Card>
<CardTitle <CardTitle
title={intl.formatMessage({ title={intl.formatMessage(
defaultMessage: "Products", {
description: "section header" defaultMessage: "Products in {categoryName}",
})} description: "header"
toolbar={ },
<Button variant="text" color="primary" onClick={onAddProduct}> { categoryName }
<FormattedMessage )}
defaultMessage="Add product" toolbar={
description="button" <Button color="primary" variant="text" onClick={onAdd}>
/> <FormattedMessage
</Button> defaultMessage="Add product"
} description="button"
/> />
<Table> </Button>
<TableHead> }
<TableRow> />
{(products === undefined || products.length > 0) && <TableCell />} <CategoryProductList
<TableCell className={classes.textLeft}> products={products}
<FormattedMessage disabled={disabled}
defaultMessage="Name" pageInfo={pageInfo}
description="product name" onNextPage={onNextPage}
/> onPreviousPage={onPreviousPage}
</TableCell> onRowClick={onRowClick}
<TableCell> selected={selected}
<FormattedMessage isChecked={isChecked}
defaultMessage="Type" toggle={toggle}
description="product type" toggleAll={toggleAll}
/> toolbar={toolbar}
</TableCell> />
</TableRow> </Card>
</TableHead> );
<TableFooter> };
<TableRow>
<TablePagination CategoryProducts.displayName = "CategoryProducts";
colSpan={3} export default CategoryProducts;
hasNextPage={hasNextPage}
onNextPage={onNextPage}
hasPreviousPage={hasPreviousPage}
onPreviousPage={onPreviousPage}
/>
</TableRow>
</TableFooter>
<TableBody>
{renderCollection(
products,
product => (
<TableRow key={product ? product.id : "skeleton"}>
<TableCellAvatar
thumbnail={maybe(() => product.thumbnail.url)}
/>
<TableCell className={classes.textLeft}>
{product ? (
<span
onClick={onRowClick && onRowClick(product.id)}
className={classes.link}
>
{product.name}
</span>
) : (
<Skeleton />
)}
</TableCell>
<TableCell>
{product && product.productType ? (
product.productType.name
) : (
<Skeleton />
)}
</TableCell>
</TableRow>
),
() => (
<TableRow>
<TableCell colSpan={3}>
<FormattedMessage defaultMessage="No products found" />
</TableCell>
</TableRow>
)
)}
</TableBody>
</Table>
</Card>
);
}
);
ProductList.displayName = "CategoryProductList";
export default ProductList;

View file

@ -1,78 +0,0 @@
import Button from "@material-ui/core/Button";
import Card from "@material-ui/core/Card";
import React from "react";
import { FormattedMessage, useIntl } from "react-intl";
import CardTitle from "@saleor/components/CardTitle";
import ProductList from "@saleor/components/ProductList";
import { ListActions, PageListProps } from "../../../types";
import { CategoryDetails_category_products_edges_node } from "../../types/CategoryDetails";
interface CategoryProductsCardProps extends PageListProps, ListActions {
products: CategoryDetails_category_products_edges_node[];
categoryName: string;
}
export const CategoryProductsCard: React.StatelessComponent<
CategoryProductsCardProps
> = ({
products,
disabled,
pageInfo,
onAdd,
onNextPage,
onPreviousPage,
onRowClick,
categoryName,
isChecked,
selected,
toggle,
toggleAll,
toolbar
}) => {
const intl = useIntl();
return (
<Card>
<CardTitle
title={intl.formatMessage(
{
defaultMessage: "Products in {categoryName}",
description: "section header"
},
{
categoryName
}
)}
toolbar={
<Button color="primary" variant="text" onClick={onAdd}>
<FormattedMessage
defaultMessage="Add product"
description="button"
/>
</Button>
}
/>
<ProductList
settings={{
columns: ["isPublished", "price", "productType"],
rowNumber: undefined
}}
products={products}
disabled={disabled}
pageInfo={pageInfo}
onNextPage={onNextPage}
onPreviousPage={onPreviousPage}
onRowClick={onRowClick}
selected={selected}
isChecked={isChecked}
toggle={toggle}
toggleAll={toggleAll}
toolbar={toolbar}
/>
</Card>
);
};
CategoryProductsCard.displayName = "CategoryProductsCard";
export default CategoryProductsCard;

View file

@ -1,2 +0,0 @@
export { default } from "./CategoryProductsCard";
export * from "./CategoryProductsCard";

View file

@ -22,7 +22,7 @@ import {
CategoryDetails_category_products_edges_node CategoryDetails_category_products_edges_node
} from "../../types/CategoryDetails"; } from "../../types/CategoryDetails";
import CategoryBackground from "../CategoryBackground"; import CategoryBackground from "../CategoryBackground";
import CategoryProductsCard from "../CategoryProductsCard"; import CategoryProducts from "../CategoryProducts";
export interface FormData { export interface FormData {
backgroundImageAlt: string; backgroundImageAlt: string;
@ -195,7 +195,7 @@ export const CategoryUpdatePage: React.StatelessComponent<
/> />
)} )}
{currentTab === CategoryPageTab.products && ( {currentTab === CategoryPageTab.products && (
<CategoryProductsCard <CategoryProducts
categoryName={maybe(() => category.name)} categoryName={maybe(() => category.name)}
products={products} products={products}
disabled={disabled} disabled={disabled}

View file

@ -204,6 +204,7 @@ const styles = (theme: Theme) =>
menuSmall: { menuSmall: {
background: theme.palette.background.paper, background: theme.palette.background.paper,
height: "100vh", height: "100vh",
overflow: "hidden",
padding: 25 padding: 25
}, },
popover: { popover: {

View file

@ -6,14 +6,23 @@ import { fade } from "@material-ui/core/styles/colorManipulator";
import makeStyles from "@material-ui/styles/makeStyles"; import makeStyles from "@material-ui/styles/makeStyles";
import React from "react"; import React from "react";
import useStateFromProps from "@saleor/hooks/useStateFromProps";
import { toggle } from "@saleor/utils/lists";
import ColumnPickerButton from "./ColumnPickerButton"; import ColumnPickerButton from "./ColumnPickerButton";
import ColumnPickerContent, { import ColumnPickerContent, {
ColumnPickerContentProps ColumnPickerContentProps
} from "./ColumnPickerContent"; } from "./ColumnPickerContent";
export interface ColumnPickerProps extends ColumnPickerContentProps { export interface ColumnPickerProps
extends Omit<
ColumnPickerContentProps,
"selectedColumns" | "onCancel" | "onColumnToggle" | "onReset" | "onSave"
> {
className?: string; className?: string;
initial?: boolean; defaultColumns: string[];
initialColumns: string[];
initialOpen?: boolean;
onSave: (columns: string[]) => void;
} }
const useStyles = makeStyles( const useStyles = makeStyles(
@ -33,29 +42,39 @@ const ColumnPicker: React.FC<ColumnPickerProps> = props => {
const { const {
className, className,
columns, columns,
initial = false, defaultColumns,
selectedColumns, hasMore,
onCancel, initialColumns,
onColumnToggle, initialOpen = false,
onReset, loading,
total,
onFetchMore,
onSave onSave
} = props; } = props;
const classes = useStyles(props); const classes = useStyles(props);
const anchor = React.useRef<HTMLDivElement>(); const anchor = React.useRef<HTMLDivElement>();
const [isExpanded, setExpansionState] = React.useState(false); const [isExpanded, setExpansionState] = React.useState(false);
const [selectedColumns, setSelectedColumns] = useStateFromProps(
initialColumns
);
React.useEffect(() => { React.useEffect(() => {
setTimeout(() => setExpansionState(initial), 100); setTimeout(() => setExpansionState(initialOpen), 100);
}, []); }, []);
const handleCancel = React.useCallback(() => { const handleCancel = React.useCallback(() => {
setExpansionState(false); setExpansionState(false);
onCancel(); setSelectedColumns(columns.map(column => column.value));
}, []); }, [columns]);
const handleColumnToggle = (column: string) =>
setSelectedColumns(toggle(column, selectedColumns, (a, b) => a === b));
const handleReset = () => setSelectedColumns(defaultColumns);
const handleSave = () => { const handleSave = () => {
setExpansionState(false); setExpansionState(false);
onSave(); onSave(selectedColumns);
}; };
return ( return (
@ -86,10 +105,14 @@ 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={handleColumnToggle}
onReset={onReset} onFetchMore={onFetchMore}
onReset={handleReset}
onSave={handleSave} onSave={handleSave}
/> />
</ClickAwayListener> </ClickAwayListener>

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,29 @@ 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"
},
root: {
boxShadow: "0px 4px 4px rgba(0, 0, 0, 0.25)"
} }
})); }));
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;
@ -72,7 +90,7 @@ const ColumnPickerContent: React.FC<ColumnPickerContentProps> = props => {
: false; : false;
return ( return (
<Card> <Card className={classes.root}>
<CardContent> <CardContent>
<Typography color="textSecondary"> <Typography color="textSecondary">
<FormattedMessage <FormattedMessage
@ -80,28 +98,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

@ -9,6 +9,7 @@ import TableBody from "@material-ui/core/TableBody";
import TableCell from "@material-ui/core/TableCell"; import TableCell from "@material-ui/core/TableCell";
import TableFooter from "@material-ui/core/TableFooter"; import TableFooter from "@material-ui/core/TableFooter";
import TableRow from "@material-ui/core/TableRow"; import TableRow from "@material-ui/core/TableRow";
import classNames from "classnames";
import React from "react"; import React from "react";
import { FormattedMessage, useIntl } from "react-intl"; import { FormattedMessage, useIntl } from "react-intl";
@ -17,9 +18,14 @@ import TableCellAvatar, {
} from "@saleor/components/TableCellAvatar"; } from "@saleor/components/TableCellAvatar";
import { ProductListColumns } from "@saleor/config"; import { ProductListColumns } from "@saleor/config";
import { maybe, renderCollection } from "@saleor/misc"; import { 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 { ListActions, ListProps } from "@saleor/types";
import { isSelected } from "@saleor/utils/lists"; import TDisplayColumn from "@saleor/utils/columns/DisplayColumn";
import { CategoryDetails_category_products_edges_node } from "../../categories/types/CategoryDetails";
import Checkbox from "../Checkbox"; import Checkbox from "../Checkbox";
import Money from "../Money"; import Money from "../Money";
import Skeleton from "../Skeleton"; import Skeleton from "../Skeleton";
@ -43,11 +49,19 @@ const styles = (theme: Theme) =>
width: 200 width: 200
} }
}, },
colAttribute: {
width: 150
},
colFill: { colFill: {
padding: 0, padding: 0,
width: "100%" width: "100%"
}, },
colName: {}, colName: {
"&$colNameFixed": {
width: 250
}
},
colNameFixed: {},
colNameHeader: { colNameHeader: {
marginLeft: AVATAR_MARGIN marginLeft: AVATAR_MARGIN
}, },
@ -77,7 +91,8 @@ interface ProductListProps
extends ListProps<ProductListColumns>, extends ListProps<ProductListColumns>,
ListActions, ListActions,
WithStyles<typeof styles> { WithStyles<typeof styles> {
products: CategoryDetails_category_products_edges_node[]; gridAttributes: AvailableInGridAttributes_grid_edges_node[];
products: ProductList_products_edges_node[];
} }
export const ProductList = withStyles(styles, { name: "ProductList" })( export const ProductList = withStyles(styles, { name: "ProductList" })(
@ -86,6 +101,7 @@ export const ProductList = withStyles(styles, { name: "ProductList" })(
settings, settings,
disabled, disabled,
isChecked, isChecked,
gridAttributes,
pageInfo, pageInfo,
products, products,
selected, selected,
@ -98,23 +114,35 @@ export const ProductList = withStyles(styles, { name: "ProductList" })(
onRowClick onRowClick
}: ProductListProps) => { }: ProductListProps) => {
const intl = useIntl(); const intl = useIntl();
const displayColumn = React.useCallback(
(column: ProductListColumns) => const DisplayColumn: React.FC<{ column: ProductListColumns }> = props => (
isSelected(column, settings.columns, (a, b) => a === b), <TDisplayColumn displayColumns={settings.columns} {...props} />
[settings.columns] );
const gridAttributesFromSettings = settings.columns.filter(
isAttributeColumnValue
); );
const numberOfColumns = 2 + settings.columns.length; const numberOfColumns = 2 + settings.columns.length;
return ( return (
<div className={classes.tableContainer}> <div className={classes.tableContainer}>
<Table className={classes.table}> <Table className={classes.table}>
<col /> <colgroup>
<col className={classes.colName} /> <col />
{displayColumn("productType") && <col className={classes.colType} />} <col className={classes.colName} />
{displayColumn("isPublished") && ( <DisplayColumn column="productType">
<col className={classes.colPublished} /> <col className={classes.colType} />
)} </DisplayColumn>
{displayColumn("price") && <col className={classes.colPrice} />} <DisplayColumn column="isPublished">
<col className={classes.colPublished} />
</DisplayColumn>
{gridAttributesFromSettings.map(gridAttribute => (
<col className={classes.colAttribute} key={gridAttribute} />
))}
<DisplayColumn column="price">
<col className={classes.colPrice} />
</DisplayColumn>
</colgroup>
<TableHead <TableHead
colSpan={numberOfColumns} colSpan={numberOfColumns}
selected={selected} selected={selected}
@ -123,32 +151,53 @@ export const ProductList = withStyles(styles, { name: "ProductList" })(
toggleAll={toggleAll} toggleAll={toggleAll}
toolbar={toolbar} toolbar={toolbar}
> >
<TableCell className={classes.colName}> <TableCell
className={classNames(classes.colName, {
[classes.colNameFixed]: settings.columns.length > 4
})}
>
<span className={classes.colNameHeader}> <span className={classes.colNameHeader}>
<FormattedMessage defaultMessage="Name" description="product" /> <FormattedMessage defaultMessage="Name" description="product" />
</span> </span>
</TableCell> </TableCell>
{displayColumn("productType") && ( <DisplayColumn column="productType">
<TableCell className={classes.colType}> <TableCell className={classes.colType}>
<FormattedMessage defaultMessage="Type" description="product" /> <FormattedMessage defaultMessage="Type" description="product" />
</TableCell> </TableCell>
)} </DisplayColumn>
{displayColumn("isPublished") && ( <DisplayColumn column="isPublished">
<TableCell className={classes.colPublished}> <TableCell className={classes.colPublished}>
<FormattedMessage <FormattedMessage
defaultMessage="Published" defaultMessage="Published"
description="product status" description="product status"
/> />
</TableCell> </TableCell>
)} </DisplayColumn>
{displayColumn("price") && ( {gridAttributesFromSettings.map(gridAttributeFromSettings => (
<TableCell
className={classes.colAttribute}
key={gridAttributeFromSettings}
>
{maybe<React.ReactNode>(
() =>
gridAttributes.find(
gridAttribute =>
getAttributeIdFromColumnValue(
gridAttributeFromSettings
) === gridAttribute.id
).name,
<Skeleton />
)}
</TableCell>
))}
<DisplayColumn column="price">
<TableCell className={classes.colPrice}> <TableCell className={classes.colPrice}>
<FormattedMessage <FormattedMessage
defaultMessage="Price" defaultMessage="Price"
description="product" description="product"
/> />
</TableCell> </TableCell>
)} </DisplayColumn>
</TableHead> </TableHead>
<TableFooter> <TableFooter>
<TableRow> <TableRow>
@ -194,9 +243,9 @@ export const ProductList = withStyles(styles, { name: "ProductList" })(
className={classes.colName} className={classes.colName}
thumbnail={maybe(() => product.thumbnail.url)} thumbnail={maybe(() => product.thumbnail.url)}
> >
{product ? product.name : <Skeleton />} {maybe<React.ReactNode>(() => product.name, <Skeleton />)}
</TableCellAvatar> </TableCellAvatar>
{displayColumn("productType") && ( <DisplayColumn column="productType">
<TableCell className={classes.colType}> <TableCell className={classes.colType}>
{product && product.productType ? ( {product && product.productType ? (
product.productType.name product.productType.name
@ -204,8 +253,8 @@ export const ProductList = withStyles(styles, { name: "ProductList" })(
<Skeleton /> <Skeleton />
)} )}
</TableCell> </TableCell>
)} </DisplayColumn>
{displayColumn("isPublished") && ( <DisplayColumn column="isPublished">
<TableCell className={classes.colPublished}> <TableCell className={classes.colPublished}>
{product && {product &&
maybe(() => product.isAvailable !== undefined) ? ( maybe(() => product.isAvailable !== undefined) ? (
@ -227,8 +276,28 @@ export const ProductList = withStyles(styles, { name: "ProductList" })(
<Skeleton /> <Skeleton />
)} )}
</TableCell> </TableCell>
)} </DisplayColumn>
{displayColumn("price") && ( {gridAttributesFromSettings.map(gridAttribute => (
<TableCell
className={classes.colAttribute}
key={gridAttribute}
>
{maybe<React.ReactNode>(() => {
const attribute = product.attributes.find(
attribute =>
attribute.attribute.id ===
getAttributeIdFromColumnValue(gridAttribute)
);
if (attribute) {
return attribute.values
.map(value => value.name)
.join(", ");
}
return "-";
}, <Skeleton />)}
</TableCell>
))}
<DisplayColumn column="price">
<TableCell className={classes.colPrice}> <TableCell className={classes.colPrice}>
{maybe(() => product.basePrice) && {maybe(() => product.basePrice) &&
maybe(() => product.basePrice.amount) !== undefined && maybe(() => product.basePrice.amount) !== undefined &&
@ -239,7 +308,7 @@ export const ProductList = withStyles(styles, { name: "ProductList" })(
<Skeleton /> <Skeleton />
)} )}
</TableCell> </TableCell>
)} </DisplayColumn>
</TableRow> </TableRow>
); );
}, },

View file

@ -26,7 +26,8 @@ const styles = (theme: Theme) =>
}, },
children: { children: {
alignSelf: "center", alignSelf: "center",
marginLeft: theme.spacing.unit * 2 marginLeft: theme.spacing.unit * 2,
width: "100%"
}, },
content: { content: {
alignItems: "center", alignItems: "center",
@ -69,7 +70,7 @@ const TableCellAvatar = withStyles(styles, { name: "TableCellAvatar" })(
src={thumbnail} src={thumbnail}
/> />
)} )}
<span className={classes.children}>{children}</span> <div className={classes.children}>{children}</div>
</div> </div>
</TableCell> </TableCell>
) )

View file

@ -101,26 +101,25 @@ const TableHead = withStyles(styles, {
})} })}
/> />
)} )}
{(items === undefined || items.length > 0) && {(items === undefined || items.length > 0) && (
(selected && ( <TableCell
<TableCell padding="checkbox"
padding="checkbox" className={classNames({
[classes.checkboxSelected]: selected,
[classes.dragRows]: dragRows
})}
>
<Checkbox
className={classNames({ className={classNames({
[classes.checkboxSelected]: selected, [classes.checkboxPartialSelect]:
[classes.dragRows]: dragRows items && items.length > selected && selected > 0
})} })}
> checked={selected === 0 ? false : true}
<Checkbox disabled={disabled}
className={classNames({ onChange={() => toggleAll(items, selected)}
[classes.checkboxPartialSelect]: />
items && items.length > selected && selected > 0 </TableCell>
})} )}
checked={selected === 0 ? false : true}
disabled={disabled}
onChange={() => toggleAll(items, selected)}
/>
</TableCell>
))}
{selected ? ( {selected ? (
<> <>
<TableCell <TableCell

View file

@ -134,6 +134,5 @@ export const filters: Filter[] = [
export const fetchMoreProps: FetchMoreProps = { export const fetchMoreProps: FetchMoreProps = {
hasMore: true, hasMore: true,
loading: false, loading: false,
onFetch: () => undefined,
onFetchMore: () => undefined onFetchMore: () => undefined
}; };

View file

@ -5,8 +5,8 @@ export const Plugins = createSvgIcon(
<> <>
<g> <g>
<path <path
fill-rule="evenodd" fillRule="evenodd"
clip-rule="evenodd" clipRule="evenodd"
d="M7.12891 6.10352e-05H12.9999V7.80006H18.7418V6.10352e-05H24.6128V7.80006H31.9999V14.6751H29.0967V29.7876H20.7418V33.6876H16.8708V40.0001H14.8708V33.6876H10.9999V29.7876H2.9031V14.6751H-0.00012207V7.80006H7.12891V6.10352e-05ZM9.12891 7.80006H10.9999V2.00006H9.12891V7.80006ZM4.9031 14.6751V27.7876H27.0967V14.6751H4.9031ZM12.9999 29.7876V31.6876H18.7418V29.7876H12.9999ZM22.6128 7.80006V2.00006H20.7418V7.80006H22.6128ZM1.99988 9.80006V12.6751H29.9999V9.80006H1.99988Z" d="M7.12891 6.10352e-05H12.9999V7.80006H18.7418V6.10352e-05H24.6128V7.80006H31.9999V14.6751H29.0967V29.7876H20.7418V33.6876H16.8708V40.0001H14.8708V33.6876H10.9999V29.7876H2.9031V14.6751H-0.00012207V7.80006H7.12891V6.10352e-05ZM9.12891 7.80006H10.9999V2.00006H9.12891V7.80006ZM4.9031 14.6751V27.7876H27.0967V14.6751H4.9031ZM12.9999 29.7876V31.6876H18.7418V29.7876H12.9999ZM22.6128 7.80006V2.00006H20.7418V7.80006H22.6128ZM1.99988 9.80006V12.6751H29.9999V9.80006H1.99988Z"
fill="#06847B" fill="#06847B"
/> />

View file

@ -81,6 +81,7 @@ interface OrderProductAddDialogProps extends FetchMoreProps {
open: boolean; open: boolean;
products: SearchOrderVariant_products_edges_node[]; products: SearchOrderVariant_products_edges_node[];
onClose: () => void; onClose: () => void;
onFetch: (query: string) => void;
onSubmit: (data: SearchOrderVariant_products_edges_node_variants[]) => void; onSubmit: (data: SearchOrderVariant_products_edges_node_variants[]) => void;
} }

View file

@ -2,7 +2,7 @@ import { WindowTitle } from "@saleor/components/WindowTitle";
import useNavigator from "@saleor/hooks/useNavigator"; import useNavigator from "@saleor/hooks/useNavigator";
import useNotifier from "@saleor/hooks/useNotifier"; import useNotifier from "@saleor/hooks/useNotifier";
import React from "react"; import React from "react";
import { FormattedMessage, useIntl } from "react-intl"; import { useIntl } from "react-intl";
import { getMutationState, maybe } from "../../misc"; import { getMutationState, maybe } from "../../misc";
import PluginsDetailsPage from "../components/PluginsDetailsPage"; import PluginsDetailsPage from "../components/PluginsDetailsPage";

View file

@ -54,6 +54,7 @@ export interface AssignAttributeDialogProps extends FetchMoreProps {
attributes: SearchAttributes_productType_availableAttributes_edges_node[]; attributes: SearchAttributes_productType_availableAttributes_edges_node[];
selected: string[]; selected: string[];
onClose: () => void; onClose: () => void;
onFetch: (query: string) => void;
onOpen: () => void; onOpen: () => void;
onSubmit: () => void; onSubmit: () => void;
onToggle: (id: string) => void; onToggle: (id: string) => void;

View file

@ -0,0 +1,332 @@
import {
createStyles,
Theme,
withStyles,
WithStyles
} from "@material-ui/core/styles";
import Table from "@material-ui/core/Table";
import TableBody from "@material-ui/core/TableBody";
import TableCell from "@material-ui/core/TableCell";
import TableFooter from "@material-ui/core/TableFooter";
import TableRow from "@material-ui/core/TableRow";
import classNames from "classnames";
import React from "react";
import { FormattedMessage, useIntl } from "react-intl";
import Checkbox from "@saleor/components/Checkbox";
import Money from "@saleor/components/Money";
import Skeleton from "@saleor/components/Skeleton";
import StatusLabel from "@saleor/components/StatusLabel";
import TableCellAvatar, {
AVATAR_MARGIN
} from "@saleor/components/TableCellAvatar";
import TableHead from "@saleor/components/TableHead";
import TablePagination from "@saleor/components/TablePagination";
import { ProductListColumns } from "@saleor/config";
import { 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";
const styles = (theme: Theme) =>
createStyles({
[theme.breakpoints.up("lg")]: {
colName: {
width: "auto"
},
colPrice: {
width: 200
},
colPublished: {
width: 200
},
colType: {
width: 200
}
},
colAttribute: {
width: 150
},
colFill: {
padding: 0,
width: "100%"
},
colName: {
"&$colNameFixed": {
width: 250
}
},
colNameFixed: {},
colNameHeader: {
marginLeft: AVATAR_MARGIN
},
colPrice: {
textAlign: "right"
},
colPublished: {},
colType: {},
link: {
cursor: "pointer"
},
table: {
tableLayout: "fixed"
},
tableContainer: {
overflowX: "scroll"
},
textLeft: {
textAlign: "left"
},
textRight: {
textAlign: "right"
}
});
interface ProductListProps
extends ListProps<ProductListColumns>,
ListActions,
WithStyles<typeof styles> {
gridAttributes: AvailableInGridAttributes_grid_edges_node[];
products: ProductList_products_edges_node[];
}
export const ProductList = withStyles(styles, { name: "ProductList" })(
({
classes,
settings,
disabled,
isChecked,
gridAttributes,
pageInfo,
products,
selected,
toggle,
toggleAll,
toolbar,
onNextPage,
onPreviousPage,
onUpdateListSettings,
onRowClick
}: ProductListProps) => {
const intl = useIntl();
const DisplayColumn: React.FC<{ column: ProductListColumns }> = props => (
<TDisplayColumn displayColumns={settings.columns} {...props} />
);
const gridAttributesFromSettings = settings.columns.filter(
isAttributeColumnValue
);
const numberOfColumns = 2 + settings.columns.length;
return (
<div className={classes.tableContainer}>
<Table className={classes.table}>
<colgroup>
<col />
<col className={classes.colName} />
<DisplayColumn column="productType">
<col className={classes.colType} />
</DisplayColumn>
<DisplayColumn column="isPublished">
<col className={classes.colPublished} />
</DisplayColumn>
{gridAttributesFromSettings.map(gridAttribute => (
<col className={classes.colAttribute} key={gridAttribute} />
))}
<DisplayColumn column="price">
<col className={classes.colPrice} />
</DisplayColumn>
</colgroup>
<TableHead
colSpan={numberOfColumns}
selected={selected}
disabled={disabled}
items={products}
toggleAll={toggleAll}
toolbar={toolbar}
>
<TableCell
className={classNames(classes.colName, {
[classes.colNameFixed]: settings.columns.length > 4
})}
>
<span className={classes.colNameHeader}>
<FormattedMessage defaultMessage="Name" description="product" />
</span>
</TableCell>
<DisplayColumn column="productType">
<TableCell className={classes.colType}>
<FormattedMessage
defaultMessage="Type"
description="product type"
/>
</TableCell>
</DisplayColumn>
<DisplayColumn column="isPublished">
<TableCell className={classes.colPublished}>
<FormattedMessage
defaultMessage="Published"
description="product status"
/>
</TableCell>
</DisplayColumn>
{gridAttributesFromSettings.map(gridAttributeFromSettings => (
<TableCell
className={classes.colAttribute}
key={gridAttributeFromSettings}
>
{maybe<React.ReactNode>(
() =>
gridAttributes.find(
gridAttribute =>
getAttributeIdFromColumnValue(
gridAttributeFromSettings
) === gridAttribute.id
).name,
<Skeleton />
)}
</TableCell>
))}
<DisplayColumn column="price">
<TableCell className={classes.colPrice}>
<FormattedMessage
defaultMessage="Price"
description="product price"
/>
</TableCell>
</DisplayColumn>
</TableHead>
<TableFooter>
<TableRow>
<TablePagination
colSpan={numberOfColumns}
settings={settings}
hasNextPage={
pageInfo && !disabled ? pageInfo.hasNextPage : false
}
onNextPage={onNextPage}
onUpdateListSettings={onUpdateListSettings}
hasPreviousPage={
pageInfo && !disabled ? pageInfo.hasPreviousPage : false
}
onPreviousPage={onPreviousPage}
/>
</TableRow>
</TableFooter>
<TableBody>
{renderCollection(
products,
product => {
const isSelected = product ? isChecked(product.id) : false;
return (
<TableRow
selected={isSelected}
hover={!!product}
key={product ? product.id : "skeleton"}
onClick={product && onRowClick(product.id)}
className={classes.link}
>
<TableCell padding="checkbox">
<Checkbox
checked={isSelected}
disabled={disabled}
disableClickPropagation
onChange={() => toggle(product.id)}
/>
</TableCell>
<TableCellAvatar
className={classes.colName}
thumbnail={maybe(() => product.thumbnail.url)}
>
{maybe<React.ReactNode>(() => product.name, <Skeleton />)}
</TableCellAvatar>
<DisplayColumn column="productType">
<TableCell className={classes.colType}>
{product && product.productType ? (
product.productType.name
) : (
<Skeleton />
)}
</TableCell>
</DisplayColumn>
<DisplayColumn column="isPublished">
<TableCell className={classes.colPublished}>
{product &&
maybe(() => product.isAvailable !== undefined) ? (
<StatusLabel
label={
product.isAvailable
? intl.formatMessage({
defaultMessage: "Published",
description: "product",
id: "productStatusLabel"
})
: intl.formatMessage({
defaultMessage: "Not published",
description: "product"
})
}
status={product.isAvailable ? "success" : "error"}
/>
) : (
<Skeleton />
)}
</TableCell>
</DisplayColumn>
{gridAttributesFromSettings.map(gridAttribute => (
<TableCell
className={classes.colAttribute}
key={gridAttribute}
>
{maybe<React.ReactNode>(() => {
const attribute = product.attributes.find(
attribute =>
attribute.attribute.id ===
getAttributeIdFromColumnValue(gridAttribute)
);
if (attribute) {
return attribute.values
.map(value => value.name)
.join(", ");
}
return "-";
}, <Skeleton />)}
</TableCell>
))}
<DisplayColumn column="price">
<TableCell className={classes.colPrice}>
{maybe(() => product.basePrice) &&
maybe(() => product.basePrice.amount) !== undefined &&
maybe(() => product.basePrice.currency) !==
undefined ? (
<Money money={product.basePrice} />
) : (
<Skeleton />
)}
</TableCell>
</DisplayColumn>
</TableRow>
);
},
() => (
<TableRow>
<TableCell colSpan={numberOfColumns}>
<FormattedMessage defaultMessage="No products found" />
</TableCell>
</TableRow>
)
)}
</TableBody>
</Table>
</div>
);
}
);
ProductList.displayName = "ProductList";
export default ProductList;

View file

@ -0,0 +1 @@
export { default } from "./ProductList";

View file

@ -6,27 +6,38 @@ import makeStyles from "@material-ui/styles/makeStyles";
import React from "react"; import React from "react";
import { FormattedMessage, useIntl } from "react-intl"; import { FormattedMessage, useIntl } from "react-intl";
import { CategoryDetails_category_products_edges_node } from "@saleor/categories/types/CategoryDetails";
import ColumnPicker, { import ColumnPicker, {
ColumnPickerChoice ColumnPickerChoice
} from "@saleor/components/ColumnPicker"; } from "@saleor/components/ColumnPicker";
import Container from "@saleor/components/Container"; import Container from "@saleor/components/Container";
import PageHeader from "@saleor/components/PageHeader"; import PageHeader from "@saleor/components/PageHeader";
import ProductList from "@saleor/components/ProductList";
import { ProductListColumns } from "@saleor/config"; import { ProductListColumns } from "@saleor/config";
import useStateFromProps from "@saleor/hooks/useStateFromProps";
import { sectionNames } from "@saleor/intl"; import { sectionNames } from "@saleor/intl";
import { FilterPageProps, ListActions, PageListProps } from "@saleor/types"; import {
import { toggle } from "@saleor/utils/lists"; AvailableInGridAttributes_availableInGrid_edges_node,
AvailableInGridAttributes_grid_edges_node
} from "@saleor/products/types/AvailableInGridAttributes";
import { ProductList_products_edges_node } from "@saleor/products/types/ProductList";
import {
FetchMoreProps,
FilterPageProps,
ListActions,
PageListProps
} from "@saleor/types";
import { ProductListUrlFilters } from "../../urls"; import { ProductListUrlFilters } from "../../urls";
import ProductList from "../ProductList";
import ProductListFilter from "../ProductListFilter"; import ProductListFilter from "../ProductListFilter";
export interface ProductListPageProps export interface ProductListPageProps
extends PageListProps<ProductListColumns>, extends PageListProps<ProductListColumns>,
ListActions, ListActions,
FilterPageProps<ProductListUrlFilters> { FilterPageProps<ProductListUrlFilters>,
FetchMoreProps {
availableInGridAttributes: AvailableInGridAttributes_availableInGrid_edges_node[];
currencySymbol: string; currencySymbol: string;
products: CategoryDetails_category_products_edges_node[]; gridAttributes: AvailableInGridAttributes_grid_edges_node[];
totalGridAttributes: number;
products: ProductList_products_edges_node[];
} }
const useStyles = makeStyles((theme: Theme) => ({ const useStyles = makeStyles((theme: Theme) => ({
@ -42,10 +53,16 @@ export const ProductListPage: React.FC<ProductListPageProps> = props => {
defaultSettings, defaultSettings,
filtersList, filtersList,
filterTabs, filterTabs,
gridAttributes,
availableInGridAttributes,
hasMore,
initialSearch, initialSearch,
loading,
settings, settings,
totalGridAttributes,
onAdd, onAdd,
onAll, onAll,
onFetchMore,
onSearchChange, onSearchChange,
onFilterAdd, onFilterAdd,
onFilterSave, onFilterSave,
@ -56,23 +73,9 @@ export const ProductListPage: React.FC<ProductListPageProps> = props => {
} = props; } = props;
const intl = useIntl(); const intl = useIntl();
const classes = useStyles(props); const classes = useStyles(props);
const [selectedColumns, setSelectedColumns] = useStateFromProps(
settings.columns
);
const handleCancel = React.useCallback( const handleSave = (columns: ProductListColumns[]) =>
() => setSelectedColumns(settings.columns), onUpdateListSettings("columns", columns);
[settings.columns]
);
const handleColumnToggle = (column: ProductListColumns) =>
setSelectedColumns(prevSelectedColumns =>
toggle(column, prevSelectedColumns, (a, b) => a === b)
);
const handleReset = () => setSelectedColumns(defaultSettings.columns);
const handleSave = () => onUpdateListSettings("columns", selectedColumns);
const columns: ColumnPickerChoice[] = [ const columns: ColumnPickerChoice[] = [
{ {
@ -95,7 +98,11 @@ export const ProductListPage: React.FC<ProductListPageProps> = props => {
description: "product type" description: "product type"
}), }),
value: "productType" as ProductListColumns value: "productType" as ProductListColumns
} },
...availableInGridAttributes.map(attribute => ({
label: attribute.name,
value: `attribute:${attribute.id}`
}))
]; ];
return ( return (
@ -104,10 +111,16 @@ export const ProductListPage: React.FC<ProductListPageProps> = props => {
<ColumnPicker <ColumnPicker
className={classes.columnPicker} className={classes.columnPicker}
columns={columns} columns={columns}
selectedColumns={selectedColumns} defaultColumns={defaultSettings.columns}
onColumnToggle={handleColumnToggle} hasMore={hasMore}
onCancel={handleCancel} loading={loading}
onReset={handleReset} initialColumns={settings.columns}
total={
columns.length -
availableInGridAttributes.length +
totalGridAttributes
}
onFetchMore={onFetchMore}
onSave={handleSave} onSave={handleSave}
/> />
<Button <Button
@ -146,7 +159,8 @@ export const ProductListPage: React.FC<ProductListPageProps> = props => {
/> />
<ProductList <ProductList
{...listProps} {...listProps}
settings={{ ...settings, columns: selectedColumns }} gridAttributes={gridAttributes}
settings={settings}
onUpdateListSettings={onUpdateListSettings} onUpdateListSettings={onUpdateListSettings}
/> />
</Card> </Card>

View file

@ -0,0 +1,13 @@
const prefix = "attribute";
export function getAttributeColumnValue(id: string) {
return `${prefix}:${id}`;
}
export function isAttributeColumnValue(value: string) {
return value.includes(`${prefix}:`);
}
export function getAttributeIdFromColumnValue(value: string) {
return value.substr(prefix.length + 1);
}

File diff suppressed because it is too large Load diff

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,38 @@ export const TypedProductImageQuery = TypedQuery<
ProductImageById, ProductImageById,
ProductImageByIdVariables ProductImageByIdVariables
>(productImageQuery); >(productImageQuery);
const availableInGridAttributes = gql`
${pageInfoFragment}
query GridAttributes($first: Int!, $after: String, $ids: [ID!]!) {
availableInGrid: attributes(
first: $first
after: $after
filter: { availableInGrid: true, isVariantOnly: false }
) {
edges {
node {
id
name
}
}
pageInfo {
...PageInfoFragment
}
totalCount
}
grid: attributes(first: 25, filter: { ids: $ids }) {
edges {
node {
id
name
}
}
}
}
`;
export const AvailableInGridAttributesQuery = TypedQuery<
AvailableInGridAttributes,
AvailableInGridAttributesVariables
>(availableInGridAttributes);

View file

@ -0,0 +1,60 @@
/* tslint:disable */
/* eslint-disable */
// This file was automatically generated and should not be edited.
// ====================================================
// GraphQL query operation: AvailableInGridAttributes
// ====================================================
export interface AvailableInGridAttributes_availableInGrid_edges_node {
__typename: "Attribute";
id: string;
name: string | null;
}
export interface AvailableInGridAttributes_availableInGrid_edges {
__typename: "AttributeCountableEdge";
node: AvailableInGridAttributes_availableInGrid_edges_node;
}
export interface AvailableInGridAttributes_availableInGrid_pageInfo {
__typename: "PageInfo";
endCursor: string | null;
hasNextPage: boolean;
hasPreviousPage: boolean;
startCursor: string | null;
}
export interface AvailableInGridAttributes_availableInGrid {
__typename: "AttributeCountableConnection";
edges: AvailableInGridAttributes_availableInGrid_edges[];
pageInfo: AvailableInGridAttributes_availableInGrid_pageInfo;
totalCount: number | null;
}
export interface AvailableInGridAttributes_grid_edges_node {
__typename: "Attribute";
id: string;
name: string | null;
}
export interface AvailableInGridAttributes_grid_edges {
__typename: "AttributeCountableEdge";
node: AvailableInGridAttributes_grid_edges_node;
}
export interface AvailableInGridAttributes_grid {
__typename: "AttributeCountableConnection";
edges: AvailableInGridAttributes_grid_edges[];
}
export interface AvailableInGridAttributes {
availableInGrid: AvailableInGridAttributes_availableInGrid | null;
grid: AvailableInGridAttributes_grid | null;
}
export interface AvailableInGridAttributesVariables {
first: number;
after?: string | null;
ids: string[];
}

View file

@ -0,0 +1,60 @@
/* tslint:disable */
/* eslint-disable */
// This file was automatically generated and should not be edited.
// ====================================================
// GraphQL query operation: GridAttributes
// ====================================================
export interface GridAttributes_availableInGrid_edges_node {
__typename: "Attribute";
id: string;
name: string | null;
}
export interface GridAttributes_availableInGrid_edges {
__typename: "AttributeCountableEdge";
node: GridAttributes_availableInGrid_edges_node;
}
export interface GridAttributes_availableInGrid_pageInfo {
__typename: "PageInfo";
endCursor: string | null;
hasNextPage: boolean;
hasPreviousPage: boolean;
startCursor: string | null;
}
export interface GridAttributes_availableInGrid {
__typename: "AttributeCountableConnection";
edges: GridAttributes_availableInGrid_edges[];
pageInfo: GridAttributes_availableInGrid_pageInfo;
totalCount: number | null;
}
export interface GridAttributes_grid_edges_node {
__typename: "Attribute";
id: string;
name: string | null;
}
export interface GridAttributes_grid_edges {
__typename: "AttributeCountableEdge";
node: GridAttributes_grid_edges_node;
}
export interface GridAttributes_grid {
__typename: "AttributeCountableConnection";
edges: GridAttributes_grid_edges[];
}
export interface GridAttributes {
availableInGrid: GridAttributes_availableInGrid | null;
grid: GridAttributes_grid | null;
}
export interface GridAttributesVariables {
first: number;
after?: string | null;
ids: string[];
}

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 {
@ -69,6 +73,18 @@ export const ProductList: React.StatelessComponent<ProductListProps> = ({
); );
const intl = useIntl(); const intl = useIntl();
React.useEffect(
() =>
navigate(
productListUrl({
...params,
after: undefined,
before: undefined
})
),
[settings.rowNumber]
);
const tabs = getFilterTabs(); const tabs = getFilterTabs();
const currentTab = const currentTab =
@ -145,265 +161,332 @@ export const ProductList: React.StatelessComponent<ProductListProps> = ({
); );
return ( return (
<TypedProductListQuery displayLoader variables={queryVariables}> <AvailableInGridAttributesQuery
{({ data, loading, refetch }) => { variables={{ first: 6, ids: settings.columns }}
const { loadNextPage, loadPreviousPage, pageInfo } = paginate( >
maybe(() => data.products.pageInfo), {attributes => (
paginationState, <TypedProductListQuery displayLoader variables={queryVariables}>
params {({ data, loading, refetch }) => {
); const { loadNextPage, loadPreviousPage, pageInfo } = paginate(
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();
}); refetch();
reset(); }
refetch(); };
}
};
const handleBulkPublish = (data: productBulkPublish) => { const handleBulkPublish = (data: productBulkPublish) => {
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();
}) refetch();
}); }
reset(); };
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 (
<> <>
<ProductListPage <ProductListPage
currencySymbol={currencySymbol} availableInGridAttributes={maybe(
currentTab={currentTab} () =>
defaultSettings={ attributes.data.availableInGrid.edges.map(
defaultListSettings[ListViews.PRODUCT_LIST] edge => edge.node
} ),
settings={settings} []
filtersList={createFilterChips( )}
params, currencySymbol={currencySymbol}
{ currentTab={currentTab}
currencySymbol, defaultSettings={
locale defaultListSettings[ListViews.PRODUCT_LIST]
},
changeFilterField,
intl
)}
onAdd={() => navigate(productAddUrl)}
disabled={loading}
products={maybe(() =>
data.products.edges.map(edge => edge.node)
)}
onNextPage={loadNextPage}
onPreviousPage={loadPreviousPage}
onUpdateListSettings={updateListSettings}
pageInfo={pageInfo}
onRowClick={id => () => navigate(productUrl(id))}
onAll={() =>
changeFilters({
status: undefined
})
}
toolbar={
<>
<Button
color="primary"
onClick={() =>
openModal("unpublish", listElements)
}
>
<FormattedMessage
defaultMessage="Unpublish"
description="unpublish product, button"
/>
</Button>
<Button
color="primary"
onClick={() => openModal("publish", listElements)}
>
<FormattedMessage
defaultMessage="Publish"
description="publish product, button"
/>
</Button>
<IconButton
color="primary"
onClick={() => openModal("delete", listElements)}
>
<DeleteIcon />
</IconButton>
</>
}
isChecked={isSelected}
selected={listElements.length}
toggle={toggle}
toggleAll={toggleAll}
onSearchChange={query => changeFilterField({ query })}
onFilterAdd={filter =>
changeFilterField(createFilter(filter))
}
onFilterSave={() => openModal("save-search")}
onFilterDelete={() => openModal("delete-search")}
onTabChange={handleTabChange}
initialSearch={params.query || ""}
filterTabs={getFilterTabs()}
/>
<ActionDialog
open={params.action === "delete"}
confirmButtonState={bulkDeleteMutationState}
onClose={closeModal}
onConfirm={() =>
productBulkDelete({
variables: { ids: params.ids }
})
}
title={intl.formatMessage({
defaultMessage: "Delete Products",
description: "dialog header"
})}
variant="delete"
>
<DialogContentText>
<FormattedMessage
defaultMessage="Are you sure you want to delete {counter, plural,
one {this product}
other {{displayQuantity} products}
}?"
description="dialog content"
values={{
counter: maybe(() => params.ids.length),
displayQuantity: (
<strong>
{maybe(() => params.ids.length)}
</strong>
)
}}
/>
</DialogContentText>
</ActionDialog>
<ActionDialog
open={params.action === "publish"}
confirmButtonState={bulkPublishMutationState}
onClose={closeModal}
onConfirm={() =>
productBulkPublish({
variables: {
ids: params.ids,
isPublished: true
} }
}) gridAttributes={maybe(
} () =>
title={intl.formatMessage({ attributes.data.grid.edges.map(
defaultMessage: "Publish Products", edge => edge.node
description: "dialog header" ),
})} []
> )}
<DialogContentText> totalGridAttributes={maybe(
<FormattedMessage () => attributes.data.availableInGrid.totalCount,
defaultMessage="Are you sure you want to publish {counter, plural, 0
one {this product} )}
other {{displayQuantity} products} settings={settings}
}?" loading={attributes.loading}
description="dialog content" hasMore={maybe(
values={{ () =>
counter: maybe(() => params.ids.length), attributes.data.availableInGrid.pageInfo
displayQuantity: ( .hasNextPage,
<strong> false
{maybe(() => params.ids.length)} )}
</strong> filtersList={createFilterChips(
params,
{
currencySymbol,
locale
},
changeFilterField,
intl
)}
onAdd={() => navigate(productAddUrl)}
disabled={loading}
products={maybe(() =>
data.products.edges.map(edge => edge.node)
)}
onFetchMore={() =>
attributes.loadMore(
(prev, next) => {
if (
prev.availableInGrid.pageInfo.endCursor ===
next.availableInGrid.pageInfo.endCursor
) {
return prev;
}
return {
...prev,
availableInGrid: {
...prev.availableInGrid,
edges: [
...prev.availableInGrid.edges,
...next.availableInGrid.edges
],
pageInfo: next.availableInGrid.pageInfo
}
};
},
{
after:
attributes.data.availableInGrid.pageInfo
.endCursor
}
) )
}}
/>
</DialogContentText>
</ActionDialog>
<ActionDialog
open={params.action === "unpublish"}
confirmButtonState={bulkPublishMutationState}
onClose={closeModal}
onConfirm={() =>
productBulkPublish({
variables: {
ids: params.ids,
isPublished: false
} }
}) onNextPage={loadNextPage}
} onPreviousPage={loadPreviousPage}
title={intl.formatMessage({ onUpdateListSettings={updateListSettings}
defaultMessage: "Unpublish Products", pageInfo={pageInfo}
description: "dialog header" onRowClick={id => () => navigate(productUrl(id))}
})} onAll={() =>
> changeFilters({
<DialogContentText> status: undefined
<FormattedMessage })
defaultMessage="Are you sure you want to unpublish {counter, plural, }
toolbar={
<>
<Button
color="primary"
onClick={() =>
openModal("unpublish", listElements)
}
>
<FormattedMessage
defaultMessage="Unpublish"
description="unpublish product, button"
/>
</Button>
<Button
color="primary"
onClick={() =>
openModal("publish", listElements)
}
>
<FormattedMessage
defaultMessage="Publish"
description="publish product, button"
/>
</Button>
<IconButton
color="primary"
onClick={() =>
openModal("delete", listElements)
}
>
<DeleteIcon />
</IconButton>
</>
}
isChecked={isSelected}
selected={listElements.length}
toggle={toggle}
toggleAll={toggleAll}
onSearchChange={query =>
changeFilterField({ query })
}
onFilterAdd={filter =>
changeFilterField(createFilter(filter))
}
onFilterSave={() => openModal("save-search")}
onFilterDelete={() => openModal("delete-search")}
onTabChange={handleTabChange}
initialSearch={params.query || ""}
filterTabs={getFilterTabs()}
/>
<ActionDialog
open={params.action === "delete"}
confirmButtonState={bulkDeleteMutationState}
onClose={closeModal}
onConfirm={() =>
productBulkDelete({
variables: { ids: params.ids }
})
}
title={intl.formatMessage({
defaultMessage: "Delete Products",
description: "dialog header"
})}
variant="delete"
>
<DialogContentText>
<FormattedMessage
defaultMessage="Are you sure you want to delete {counter, plural,
one {this product} one {this product}
other {{displayQuantity} products} other {{displayQuantity} products}
}?" }?"
description="dialog content" description="dialog content"
values={{ values={{
counter: maybe(() => params.ids.length), counter: maybe(() => params.ids.length),
displayQuantity: ( displayQuantity: (
<strong> <strong>
{maybe(() => params.ids.length)} {maybe(() => params.ids.length)}
</strong> </strong>
) )
}} }}
/>
</DialogContentText>
</ActionDialog>
<ActionDialog
open={params.action === "publish"}
confirmButtonState={bulkPublishMutationState}
onClose={closeModal}
onConfirm={() =>
productBulkPublish({
variables: {
ids: params.ids,
isPublished: true
}
})
}
title={intl.formatMessage({
defaultMessage: "Publish Products",
description: "dialog header"
})}
>
<DialogContentText>
<FormattedMessage
defaultMessage="Are you sure you want to publish {counter, plural,
one {this product}
other {{displayQuantity} products}
}?"
description="dialog content"
values={{
counter: maybe(() => params.ids.length),
displayQuantity: (
<strong>
{maybe(() => params.ids.length)}
</strong>
)
}}
/>
</DialogContentText>
</ActionDialog>
<ActionDialog
open={params.action === "unpublish"}
confirmButtonState={bulkPublishMutationState}
onClose={closeModal}
onConfirm={() =>
productBulkPublish({
variables: {
ids: params.ids,
isPublished: false
}
})
}
title={intl.formatMessage({
defaultMessage: "Unpublish Products",
description: "dialog header"
})}
>
<DialogContentText>
<FormattedMessage
defaultMessage="Are you sure you want to unpublish {counter, plural,
one {this product}
other {{displayQuantity} products}
}?"
description="dialog content"
values={{
counter: maybe(() => params.ids.length),
displayQuantity: (
<strong>
{maybe(() => params.ids.length)}
</strong>
)
}}
/>
</DialogContentText>
</ActionDialog>
<SaveFilterTabDialog
open={params.action === "save-search"}
confirmButtonState="default"
onClose={closeModal}
onSubmit={handleFilterTabSave}
/> />
</DialogContentText> <DeleteFilterTabDialog
</ActionDialog> open={params.action === "delete-search"}
<SaveFilterTabDialog confirmButtonState="default"
open={params.action === "save-search"} onClose={closeModal}
confirmButtonState="default" onSubmit={handleFilterTabDelete}
onClose={closeModal} tabName={maybe(
onSubmit={handleFilterTabSave} () => tabs[currentTab - 1].name,
/> "..."
<DeleteFilterTabDialog )}
open={params.action === "delete-search"} />
confirmButtonState="default" </>
onClose={closeModal} );
onSubmit={handleFilterTabDelete} }}
tabName={maybe(() => tabs[currentTab - 1].name, "...")} </TypedProductBulkPublishMutation>
/> )}
</> </TypedProductBulkDeleteMutation>
); );
}} }}
</TypedProductBulkPublishMutation> </TypedProductListQuery>
)} )}
</TypedProductBulkDeleteMutation> </AvailableInGridAttributesQuery>
);
}}
</TypedProductListQuery>
); );
}; };
export default ProductList; export default ProductList;

File diff suppressed because it is too large Load diff

View file

@ -1,96 +0,0 @@
import { storiesOf } from "@storybook/react";
import React from "react";
import placeholder from "@assets/images/placeholder60x60.png";
import CategoryProducts from "../../../categories/components/CategoryProducts";
import Decorator from "../../Decorator";
const products = [
{
id: "UHJvZHVjdDox",
name: "Gardner, Graham and King",
productType: {
id: "1",
name: "T-Shirt"
},
thumbnail: {
url: placeholder
}
},
{
id: "UHJvZHVjdDoy",
name: "Gardner, Graham and King",
productType: {
id: "1",
name: "T-Shirt"
},
thumbnail: {
url: placeholder
}
},
{
id: "UHJvZHVjdDoz",
name: "Gardner, Graham and King",
productType: {
id: "1",
name: "T-Shirt"
},
thumbnail: {
url: placeholder
}
},
{
id: "UHJvZHVjdDoa",
name: "Gardner, Graham and King",
productType: {
id: "1",
name: "T-Shirt"
},
thumbnail: {
url: placeholder
}
}
];
storiesOf("Categories / CategoryProducts", module)
.addDecorator(Decorator)
.add("without initial data", () => (
<CategoryProducts
hasNextPage={true}
hasPreviousPage={false}
products={[]}
onAddProduct={undefined}
onNextPage={undefined}
onPreviousPage={undefined}
/>
))
.add("with initial data", () => (
<CategoryProducts
hasNextPage={true}
hasPreviousPage={false}
products={products}
onAddProduct={undefined}
onNextPage={undefined}
onPreviousPage={undefined}
/>
))
.add("with clickable rows", () => (
<CategoryProducts
hasNextPage={true}
hasPreviousPage={false}
products={products}
onAddProduct={undefined}
onNextPage={undefined}
onPreviousPage={undefined}
onRowClick={() => undefined}
/>
))
.add("when loading data", () => (
<CategoryProducts
hasNextPage={true}
hasPreviousPage={false}
onAddProduct={undefined}
onNextPage={undefined}
onPreviousPage={undefined}
/>
));

View file

@ -15,6 +15,7 @@ const props: AssignAttributeDialogProps = {
confirmButtonState: "default", confirmButtonState: "default",
errors: [], errors: [],
onClose: () => undefined, onClose: () => undefined,
onFetch: () => undefined,
onOpen: () => undefined, onOpen: () => undefined,
onSubmit: () => undefined, onSubmit: () => undefined,
onToggle: () => undefined, onToggle: () => undefined,

View file

@ -26,12 +26,10 @@ const columns: ColumnPickerChoice[] = [
const props: ColumnPickerProps = { const props: ColumnPickerProps = {
columns, columns,
initial: true, defaultColumns: [1, 3].map(index => columns[index].value),
onCancel: () => undefined, initialColumns: [1, 3, 4, 6].map(index => columns[index].value),
onColumnToggle: () => undefined, initialOpen: true,
onReset: () => undefined, onSave: () => undefined
onSave: () => undefined,
selectedColumns: [1, 3, 4, 6].map(index => columns[index].value)
}; };
storiesOf("Generics / Column picker", module) storiesOf("Generics / Column picker", module)
@ -42,4 +40,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}
/>
));

View file

@ -3,9 +3,11 @@ import React from "react";
import placeholderImage from "@assets/images/placeholder255x255.png"; import placeholderImage from "@assets/images/placeholder255x255.png";
import { defaultListSettings } from "@saleor/config"; import { defaultListSettings } from "@saleor/config";
import { products as productListFixture } from "@saleor/products/fixtures";
import { attributes } from "@saleor/productTypes/fixtures";
import { ListViews } from "@saleor/types"; import { ListViews } from "@saleor/types";
import { category as categoryFixture } from "../../../categories/fixtures";
import { import {
fetchMoreProps,
filterPageProps, filterPageProps,
filters, filters,
listActionsProps, listActionsProps,
@ -16,20 +18,22 @@ import ProductListPage, {
} from "../../../products/components/ProductListPage"; } from "../../../products/components/ProductListPage";
import Decorator from "../../Decorator"; import Decorator from "../../Decorator";
const products = categoryFixture(placeholderImage).products.edges.map( const products = productListFixture(placeholderImage);
edge => edge.node
);
const props: ProductListPageProps = { const props: ProductListPageProps = {
...listActionsProps, ...listActionsProps,
...pageListProps.default, ...pageListProps.default,
...filterPageProps, ...filterPageProps,
...fetchMoreProps,
availableInGridAttributes: attributes,
defaultSettings: defaultListSettings[ListViews.PRODUCT_LIST], defaultSettings: defaultListSettings[ListViews.PRODUCT_LIST],
gridAttributes: attributes,
products, products,
settings: { settings: {
...pageListProps.default.settings, ...pageListProps.default.settings,
columns: ["isPublished", "productType", "price"] columns: ["isPublished", "productType", "price"]
} },
totalGridAttributes: attributes.length
}; };
storiesOf("Views / Products / Product list", module) storiesOf("Views / Products / Product list", module)

View file

@ -125,9 +125,8 @@ export interface ReorderEvent {
} }
export type ReorderAction = (event: ReorderEvent) => void; export type ReorderAction = (event: ReorderEvent) => void;
export interface FetchMoreProps<TData = string> { export interface FetchMoreProps {
loading: boolean; loading: boolean;
hasMore: boolean; hasMore: boolean;
onFetch: (value: TData) => void;
onFetchMore: () => void; onFetchMore: () => void;
} }

View file

@ -0,0 +1,24 @@
import React from "react";
import { isSelected } from "../lists";
export interface DisplayColumnProps<TColumn extends string = string> {
displayColumns: TColumn[];
column: TColumn;
}
const DisplayColumn: React.FC<DisplayColumnProps> = ({
displayColumns,
children,
column
}) => {
const displayColumn = React.useCallback(
(column: string) => isSelected(column, displayColumns, (a, b) => a === b),
[displayColumns]
);
return <>{displayColumn(column) && children}</>;
};
DisplayColumn.displayName = "DisplayColumn";
export default DisplayColumn;