Add attributes as columns in product list view

This commit is contained in:
dominik-zeglen 2019-08-28 16:41:17 +02:00
parent bc2c6a9c71
commit 85cd2e875e
25 changed files with 1831 additions and 2569 deletions

View file

@ -0,0 +1,229 @@
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 TableCellAvatar, {
AVATAR_MARGIN
} from "@saleor/components/TableCellAvatar";
import TableHead from "@saleor/components/TableHead";
import TablePagination from "@saleor/components/TablePagination";
import i18n from "@saleor/i18n";
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 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}>
{i18n.t("Name", { context: "object" })}
</span>
</TableCell>
<TableCell className={classes.colType}>
{i18n.t("Type", { context: "object" })}
</TableCell>
<TableCell className={classes.colPublished}>
{i18n.t("Published", { context: "object" })}
</TableCell>
<TableCell className={classes.colPrice}>
{i18n.t("Price", { context: "object" })}
</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
? i18n.t("Published", {
context: "product status"
})
: i18n.t("Not published", {
context: "product status"
})
}
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}>
{i18n.t("No products found")}
</TableCell>
</TableRow>
)
)}
</TableBody>
</Table>
</div>
);
}
);
CategoryProductList.displayName = "CategoryProductList";
export default 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

@ -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,33 +42,39 @@ const ColumnPicker: React.FC<ColumnPickerProps> = props => {
const { const {
className, className,
columns, columns,
defaultColumns,
hasMore, hasMore,
initial = false, initialColumns,
initialOpen = false,
loading, loading,
selectedColumns,
total, total,
onCancel,
onColumnToggle,
onFetchMore, onFetchMore,
onReset,
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 (
@ -95,9 +110,9 @@ const ColumnPicker: React.FC<ColumnPickerProps> = props => {
selectedColumns={selectedColumns} selectedColumns={selectedColumns}
total={total} total={total}
onCancel={handleCancel} onCancel={handleCancel}
onColumnToggle={onColumnToggle} onColumnToggle={handleColumnToggle}
onFetchMore={onFetchMore} onFetchMore={onFetchMore}
onReset={onReset} onReset={handleReset}
onSave={handleSave} onSave={handleSave}
/> />
</ClickAwayListener> </ClickAwayListener>

View file

@ -61,6 +61,9 @@ const useStyles = makeStyles((theme: Theme) => ({
gridColumnEnd: "span 3", gridColumnEnd: "span 3",
height: theme.spacing.unit * 3, height: theme.spacing.unit * 3,
justifyContent: "center" justifyContent: "center"
},
root: {
boxShadow: "0px 4px 4px rgba(0, 0, 0, 0.25)"
} }
})); }));
@ -87,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

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

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

@ -0,0 +1,318 @@
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 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 i18n from "@saleor/i18n";
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 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}>
{i18n.t("Name", { context: "object" })}
</span>
</TableCell>
<DisplayColumn column="productType">
<TableCell className={classes.colType}>
{i18n.t("Type", { context: "object" })}
</TableCell>
</DisplayColumn>
<DisplayColumn column="isPublished">
<TableCell className={classes.colPublished}>
{i18n.t("Published", { context: "object" })}
</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}>
{i18n.t("Price", { context: "object" })}
</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
? i18n.t("Published", {
context: "product status"
})
: i18n.t("Not published", {
context: "product status"
})
}
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}>
{i18n.t("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,25 +6,25 @@ 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 {
import { sectionNames } from "@saleor/intl"; AvailableInGridAttributes_availableInGrid_edges_node,
import { AvailableInGridAttributes_attributes_edges_node } from "@saleor/products/types/AvailableInGridAttributes"; AvailableInGridAttributes_grid_edges_node
} from "@saleor/products/types/AvailableInGridAttributes";
import { ProductList_products_edges_node } from "@saleor/products/types/ProductList";
import { import {
FetchMoreProps, FetchMoreProps,
FilterPageProps, FilterPageProps,
ListActions, ListActions,
PageListProps PageListProps
} from "@saleor/types"; } from "@saleor/types";
import { toggle } from "@saleor/utils/lists";
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
@ -32,10 +32,11 @@ export interface ProductListPageProps
ListActions, ListActions,
FilterPageProps<ProductListUrlFilters>, FilterPageProps<ProductListUrlFilters>,
FetchMoreProps { FetchMoreProps {
availableInGridAttributes: AvailableInGridAttributes_availableInGrid_edges_node[];
currencySymbol: string; currencySymbol: string;
gridAttributes: AvailableInGridAttributes_attributes_edges_node[]; gridAttributes: AvailableInGridAttributes_grid_edges_node[];
totalGridAttributes: number; totalGridAttributes: number;
products: CategoryDetails_category_products_edges_node[]; products: ProductList_products_edges_node[];
} }
const useStyles = makeStyles((theme: Theme) => ({ const useStyles = makeStyles((theme: Theme) => ({
@ -52,6 +53,7 @@ export const ProductListPage: React.FC<ProductListPageProps> = props => {
filtersList, filtersList,
filterTabs, filterTabs,
gridAttributes, gridAttributes,
availableInGridAttributes,
hasMore, hasMore,
initialSearch, initialSearch,
loading, loading,
@ -70,23 +72,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[] = [
{ {
@ -110,7 +98,7 @@ export const ProductListPage: React.FC<ProductListPageProps> = props => {
}), }),
value: "productType" as ProductListColumns value: "productType" as ProductListColumns
}, },
...gridAttributes.map(attribute => ({ ...availableInGridAttributes.map(attribute => ({
label: attribute.name, label: attribute.name,
value: `attribute:${attribute.id}` value: `attribute:${attribute.id}`
})) }))
@ -122,14 +110,16 @@ export const ProductListPage: React.FC<ProductListPageProps> = props => {
<ColumnPicker <ColumnPicker
className={classes.columnPicker} className={classes.columnPicker}
columns={columns} columns={columns}
defaultColumns={defaultSettings.columns}
hasMore={hasMore} hasMore={hasMore}
loading={loading} loading={loading}
selectedColumns={selectedColumns} initialColumns={settings.columns}
total={columns.length + totalGridAttributes} total={
onColumnToggle={handleColumnToggle} columns.length -
onCancel={handleCancel} availableInGridAttributes.length +
totalGridAttributes
}
onFetchMore={onFetchMore} onFetchMore={onFetchMore}
onReset={handleReset}
onSave={handleSave} onSave={handleSave}
/> />
<Button <Button
@ -168,7 +158,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

@ -377,11 +377,11 @@ export const TypedProductImageQuery = TypedQuery<
const availableInGridAttributes = gql` const availableInGridAttributes = gql`
${pageInfoFragment} ${pageInfoFragment}
query AvailableInGridAttributes($first: Int!, $after: String) { query GridAttributes($first: Int!, $after: String, $ids: [ID!]!) {
attributes( availableInGrid: attributes(
first: $first first: $first
after: $after after: $after
filter: { availableInGrid: true } filter: { availableInGrid: true, isVariantOnly: false }
) { ) {
edges { edges {
node { node {
@ -394,6 +394,15 @@ const availableInGridAttributes = gql`
} }
totalCount totalCount
} }
grid: attributes(first: 25, filter: { ids: $ids }) {
edges {
node {
id
name
}
}
}
} }
`; `;
export const AvailableInGridAttributesQuery = TypedQuery< export const AvailableInGridAttributesQuery = TypedQuery<

View file

@ -6,18 +6,18 @@
// GraphQL query operation: AvailableInGridAttributes // GraphQL query operation: AvailableInGridAttributes
// ==================================================== // ====================================================
export interface AvailableInGridAttributes_attributes_edges_node { export interface AvailableInGridAttributes_availableInGrid_edges_node {
__typename: "Attribute"; __typename: "Attribute";
id: string; id: string;
name: string | null; name: string | null;
} }
export interface AvailableInGridAttributes_attributes_edges { export interface AvailableInGridAttributes_availableInGrid_edges {
__typename: "AttributeCountableEdge"; __typename: "AttributeCountableEdge";
node: AvailableInGridAttributes_attributes_edges_node; node: AvailableInGridAttributes_availableInGrid_edges_node;
} }
export interface AvailableInGridAttributes_attributes_pageInfo { export interface AvailableInGridAttributes_availableInGrid_pageInfo {
__typename: "PageInfo"; __typename: "PageInfo";
endCursor: string | null; endCursor: string | null;
hasNextPage: boolean; hasNextPage: boolean;
@ -25,18 +25,36 @@ export interface AvailableInGridAttributes_attributes_pageInfo {
startCursor: string | null; startCursor: string | null;
} }
export interface AvailableInGridAttributes_attributes { export interface AvailableInGridAttributes_availableInGrid {
__typename: "AttributeCountableConnection"; __typename: "AttributeCountableConnection";
edges: AvailableInGridAttributes_attributes_edges[]; edges: AvailableInGridAttributes_availableInGrid_edges[];
pageInfo: AvailableInGridAttributes_attributes_pageInfo; pageInfo: AvailableInGridAttributes_availableInGrid_pageInfo;
totalCount: number | null; 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 { export interface AvailableInGridAttributes {
attributes: AvailableInGridAttributes_attributes | null; availableInGrid: AvailableInGridAttributes_availableInGrid | null;
grid: AvailableInGridAttributes_grid | null;
} }
export interface AvailableInGridAttributesVariables { export interface AvailableInGridAttributesVariables {
first: number; first: number;
after?: string | null; 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

@ -73,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 =
@ -149,8 +161,10 @@ export const ProductList: React.StatelessComponent<ProductListProps> = ({
); );
return ( return (
<AvailableInGridAttributesQuery variables={{ first: 6 }}> <AvailableInGridAttributesQuery
{gridAttributes => ( variables={{ first: 6, ids: settings.columns }}
>
{attributes => (
<TypedProductListQuery displayLoader variables={queryVariables}> <TypedProductListQuery displayLoader variables={queryVariables}>
{({ data, loading, refetch }) => { {({ data, loading, refetch }) => {
const { loadNextPage, loadPreviousPage, pageInfo } = paginate( const { loadNextPage, loadPreviousPage, pageInfo } = paginate(
@ -159,27 +173,27 @@ export const ProductList: React.StatelessComponent<ProductListProps> = ({
params 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(commonMessages.savedChanges) text: intl.formatMessage(commonMessages.savedChanges)
}); });
reset(); reset();
refetch(); 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(commonMessages.savedChanges) text: intl.formatMessage(commonMessages.savedChanges)
}); });
reset(); reset();
refetch(); refetch();
} }
}; };
return ( return (
<TypedProductBulkDeleteMutation onCompleted={handleBulkDelete}> <TypedProductBulkDeleteMutation onCompleted={handleBulkDelete}>
@ -207,254 +221,272 @@ export const ProductList: React.StatelessComponent<ProductListProps> = ({
) )
); );
return ( return (
<> <>
<ProductListPage <ProductListPage
currencySymbol={currencySymbol} availableInGridAttributes={maybe(
currentTab={currentTab} () =>
defaultSettings={ attributes.data.availableInGrid.edges.map(
defaultListSettings[ListViews.PRODUCT_LIST] edge => edge.node
} ),
gridAttributes={maybe( []
() => )}
gridAttributes.data.attributes.edges.map( currencySymbol={currencySymbol}
edge => edge.node currentTab={currentTab}
), defaultSettings={
[] defaultListSettings[ListViews.PRODUCT_LIST]
)} }
totalGridAttributes={maybe( gridAttributes={maybe(
() => gridAttributes.data.attributes.totalCount, () =>
0 attributes.data.grid.edges.map(
)} edge => edge.node
settings={settings} ),
loading={gridAttributes.loading} []
)}
totalGridAttributes={maybe(
() => attributes.data.availableInGrid.totalCount,
0
)}
settings={settings}
loading={attributes.loading}
hasMore={maybe( hasMore={maybe(
() => () =>
gridAttributes.data.attributes.pageInfo attributes.data.availableInGrid.pageInfo
.hasNextPage, .hasNextPage,
false false
)} )}
filtersList={createFilterChips( filtersList={createFilterChips(
params, params,
{ {
currencySymbol, currencySymbol,
locale locale
}, },
changeFilterField, changeFilterField,
intl intl
)} )}
onAdd={() => navigate(productAddUrl)} onAdd={() => navigate(productAddUrl)}
disabled={loading} disabled={loading}
products={maybe(() => products={maybe(() =>
data.products.edges.map(edge => edge.node) data.products.edges.map(edge => edge.node)
)} )}
onFetchMore={() => onFetchMore={() =>
gridAttributes.loadMore( attributes.loadMore(
(prev, next) => { (prev, next) => {
if ( if (
prev.attributes.pageInfo.endCursor === prev.availableInGrid.pageInfo.endCursor ===
next.attributes.pageInfo.endCursor next.availableInGrid.pageInfo.endCursor
) { ) {
return prev; return prev;
} }
return { return {
...prev, ...prev,
attributes: { availableInGrid: {
...prev.attributes, ...prev.availableInGrid,
edges: [ edges: [
...prev.attributes.edges, ...prev.availableInGrid.edges,
...next.attributes.edges ...next.availableInGrid.edges
], ],
pageInfo: next.attributes.pageInfo pageInfo: next.availableInGrid.pageInfo
}
};
},
{
after:
attributes.data.availableInGrid.pageInfo
.endCursor
} }
}; )
},
{
after:
gridAttributes.data.attributes.pageInfo
.endCursor
} }
) onNextPage={loadNextPage}
} onPreviousPage={loadPreviousPage}
onNextPage={loadNextPage} onUpdateListSettings={updateListSettings}
onPreviousPage={loadPreviousPage} pageInfo={pageInfo}
onUpdateListSettings={updateListSettings} onRowClick={id => () => navigate(productUrl(id))}
pageInfo={pageInfo} onAll={() =>
onRowClick={id => () => navigate(productUrl(id))} changeFilters({
onAll={() => status: undefined
changeFilters({ })
status: undefined }
}) toolbar={
} <>
toolbar={ <Button
<> color="primary"
<Button onClick={() =>
color="primary" openModal("unpublish", listElements)
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 <FormattedMessage
defaultMessage="Unpublish" defaultMessage="Are you sure you want to delete {counter, plural,
description="unpublish product, button" one {this product}
other {{displayQuantity} products}
}?"
description="dialog content"
values={{
counter: maybe(() => params.ids.length),
displayQuantity: (
<strong>
{maybe(() => params.ids.length)}
</strong>
)
}}
/> />
</Button> </DialogContentText>
<Button </ActionDialog>
color="primary" <ActionDialog
onClick={() => openModal("publish", listElements)} 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 <FormattedMessage
defaultMessage="Publish" defaultMessage="Are you sure you want to publish {counter, plural,
description="publish product, button" one {this product}
other {{displayQuantity} products}
}?"
description="dialog content"
values={{
counter: maybe(() => params.ids.length),
displayQuantity: (
<strong>
{maybe(() => params.ids.length)}
</strong>
)
}}
/> />
</Button> </DialogContentText>
<IconButton </ActionDialog>
color="primary" <ActionDialog
onClick={() => openModal("delete", listElements)} open={params.action === "unpublish"}
> confirmButtonState={bulkPublishMutationState}
<DeleteIcon /> onClose={closeModal}
</IconButton> onConfirm={() =>
</> productBulkPublish({
} variables: {
isChecked={isSelected} ids: params.ids,
selected={listElements.length} isPublished: false
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
} }
}) title={intl.formatMessage({
} defaultMessage: "Unpublish Products",
title={intl.formatMessage({ description: "dialog header"
defaultMessage: "Publish Products", })}
description: "dialog header" >
})} <DialogContentText>
> <FormattedMessage
<DialogContentText> defaultMessage="Are you sure you want to unpublish {counter, plural,
<FormattedMessage
defaultMessage="Are you sure you want to publish {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>
<SaveFilterTabDialog
open={params.action === "save-search"}
confirmButtonState="default"
onClose={closeModal}
onSubmit={handleFilterTabSave}
/> />
</DialogContentText> <DeleteFilterTabDialog
</ActionDialog> open={params.action === "delete-search"}
<ActionDialog confirmButtonState="default"
open={params.action === "unpublish"} onClose={closeModal}
confirmButtonState={bulkPublishMutationState} onSubmit={handleFilterTabDelete}
onClose={closeModal} tabName={maybe(
onConfirm={() => () => tabs[currentTab - 1].name,
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"} </TypedProductBulkPublishMutation>
confirmButtonState="default" )}
onClose={closeModal} </TypedProductBulkDeleteMutation>
onSubmit={handleFilterTabSave} );
/> }}
<DeleteFilterTabDialog </TypedProductListQuery>
open={params.action === "delete-search"} )}
confirmButtonState="default" </AvailableInGridAttributesQuery>
onClose={closeModal}
onSubmit={handleFilterTabDelete}
tabName={maybe(() => tabs[currentTab - 1].name, "...")}
/>
</>
);
}}
</TypedProductBulkPublishMutation>
)}
</TypedProductBulkDeleteMutation>
);
}}
</TypedProductListQuery>
); );
}; };
export default ProductList; export default ProductList;

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)

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

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