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

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

View file

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

View file

@ -61,6 +61,9 @@ const useStyles = makeStyles((theme: Theme) => ({
gridColumnEnd: "span 3",
height: theme.spacing.unit * 3,
justifyContent: "center"
},
root: {
boxShadow: "0px 4px 4px rgba(0, 0, 0, 0.25)"
}
}));
@ -87,7 +90,7 @@ const ColumnPickerContent: React.FC<ColumnPickerContentProps> = props => {
: false;
return (
<Card>
<Card className={classes.root}>
<CardContent>
<Typography color="textSecondary">
<FormattedMessage

View file

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

View file

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

View file

@ -134,6 +134,5 @@ export const filters: Filter[] = [
export const fetchMoreProps: FetchMoreProps = {
hasMore: true,
loading: false,
onFetch: () => 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 { FormattedMessage, useIntl } from "react-intl";
import { CategoryDetails_category_products_edges_node } from "@saleor/categories/types/CategoryDetails";
import ColumnPicker, {
ColumnPickerChoice
} from "@saleor/components/ColumnPicker";
import Container from "@saleor/components/Container";
import PageHeader from "@saleor/components/PageHeader";
import ProductList from "@saleor/components/ProductList";
import { ProductListColumns } from "@saleor/config";
import useStateFromProps from "@saleor/hooks/useStateFromProps";
import { sectionNames } from "@saleor/intl";
import { AvailableInGridAttributes_attributes_edges_node } from "@saleor/products/types/AvailableInGridAttributes";
import {
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 { toggle } from "@saleor/utils/lists";
import { ProductListUrlFilters } from "../../urls";
import ProductList from "../ProductList";
import ProductListFilter from "../ProductListFilter";
export interface ProductListPageProps
@ -32,10 +32,11 @@ export interface ProductListPageProps
ListActions,
FilterPageProps<ProductListUrlFilters>,
FetchMoreProps {
availableInGridAttributes: AvailableInGridAttributes_availableInGrid_edges_node[];
currencySymbol: string;
gridAttributes: AvailableInGridAttributes_attributes_edges_node[];
gridAttributes: AvailableInGridAttributes_grid_edges_node[];
totalGridAttributes: number;
products: CategoryDetails_category_products_edges_node[];
products: ProductList_products_edges_node[];
}
const useStyles = makeStyles((theme: Theme) => ({
@ -52,6 +53,7 @@ export const ProductListPage: React.FC<ProductListPageProps> = props => {
filtersList,
filterTabs,
gridAttributes,
availableInGridAttributes,
hasMore,
initialSearch,
loading,
@ -70,23 +72,9 @@ export const ProductListPage: React.FC<ProductListPageProps> = props => {
} = props;
const intl = useIntl();
const classes = useStyles(props);
const [selectedColumns, setSelectedColumns] = useStateFromProps(
settings.columns
);
const handleCancel = React.useCallback(
() => setSelectedColumns(settings.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 handleSave = (columns: ProductListColumns[]) =>
onUpdateListSettings("columns", columns);
const columns: ColumnPickerChoice[] = [
{
@ -110,7 +98,7 @@ export const ProductListPage: React.FC<ProductListPageProps> = props => {
}),
value: "productType" as ProductListColumns
},
...gridAttributes.map(attribute => ({
...availableInGridAttributes.map(attribute => ({
label: attribute.name,
value: `attribute:${attribute.id}`
}))
@ -122,14 +110,16 @@ export const ProductListPage: React.FC<ProductListPageProps> = props => {
<ColumnPicker
className={classes.columnPicker}
columns={columns}
defaultColumns={defaultSettings.columns}
hasMore={hasMore}
loading={loading}
selectedColumns={selectedColumns}
total={columns.length + totalGridAttributes}
onColumnToggle={handleColumnToggle}
onCancel={handleCancel}
initialColumns={settings.columns}
total={
columns.length -
availableInGridAttributes.length +
totalGridAttributes
}
onFetchMore={onFetchMore}
onReset={handleReset}
onSave={handleSave}
/>
<Button
@ -168,7 +158,8 @@ export const ProductListPage: React.FC<ProductListPageProps> = props => {
/>
<ProductList
{...listProps}
settings={{ ...settings, columns: selectedColumns }}
gridAttributes={gridAttributes}
settings={settings}
onUpdateListSettings={onUpdateListSettings}
/>
</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`
${pageInfoFragment}
query AvailableInGridAttributes($first: Int!, $after: String) {
attributes(
query GridAttributes($first: Int!, $after: String, $ids: [ID!]!) {
availableInGrid: attributes(
first: $first
after: $after
filter: { availableInGrid: true }
filter: { availableInGrid: true, isVariantOnly: false }
) {
edges {
node {
@ -394,6 +394,15 @@ const availableInGridAttributes = gql`
}
totalCount
}
grid: attributes(first: 25, filter: { ids: $ids }) {
edges {
node {
id
name
}
}
}
}
`;
export const AvailableInGridAttributesQuery = TypedQuery<

View file

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

@ -73,6 +73,18 @@ export const ProductList: React.StatelessComponent<ProductListProps> = ({
);
const intl = useIntl();
React.useEffect(
() =>
navigate(
productListUrl({
...params,
after: undefined,
before: undefined
})
),
[settings.rowNumber]
);
const tabs = getFilterTabs();
const currentTab =
@ -149,8 +161,10 @@ export const ProductList: React.StatelessComponent<ProductListProps> = ({
);
return (
<AvailableInGridAttributesQuery variables={{ first: 6 }}>
{gridAttributes => (
<AvailableInGridAttributesQuery
variables={{ first: 6, ids: settings.columns }}
>
{attributes => (
<TypedProductListQuery displayLoader variables={queryVariables}>
{({ data, loading, refetch }) => {
const { loadNextPage, loadPreviousPage, pageInfo } = paginate(
@ -210,6 +224,13 @@ export const ProductList: React.StatelessComponent<ProductListProps> = ({
return (
<>
<ProductListPage
availableInGridAttributes={maybe(
() =>
attributes.data.availableInGrid.edges.map(
edge => edge.node
),
[]
)}
currencySymbol={currencySymbol}
currentTab={currentTab}
defaultSettings={
@ -217,20 +238,20 @@ export const ProductList: React.StatelessComponent<ProductListProps> = ({
}
gridAttributes={maybe(
() =>
gridAttributes.data.attributes.edges.map(
attributes.data.grid.edges.map(
edge => edge.node
),
[]
)}
totalGridAttributes={maybe(
() => gridAttributes.data.attributes.totalCount,
() => attributes.data.availableInGrid.totalCount,
0
)}
settings={settings}
loading={gridAttributes.loading}
loading={attributes.loading}
hasMore={maybe(
() =>
gridAttributes.data.attributes.pageInfo
attributes.data.availableInGrid.pageInfo
.hasNextPage,
false
)}
@ -249,29 +270,29 @@ export const ProductList: React.StatelessComponent<ProductListProps> = ({
data.products.edges.map(edge => edge.node)
)}
onFetchMore={() =>
gridAttributes.loadMore(
attributes.loadMore(
(prev, next) => {
if (
prev.attributes.pageInfo.endCursor ===
next.attributes.pageInfo.endCursor
prev.availableInGrid.pageInfo.endCursor ===
next.availableInGrid.pageInfo.endCursor
) {
return prev;
}
return {
...prev,
attributes: {
...prev.attributes,
availableInGrid: {
...prev.availableInGrid,
edges: [
...prev.attributes.edges,
...next.attributes.edges
...prev.availableInGrid.edges,
...next.availableInGrid.edges
],
pageInfo: next.attributes.pageInfo
pageInfo: next.availableInGrid.pageInfo
}
};
},
{
after:
gridAttributes.data.attributes.pageInfo
attributes.data.availableInGrid.pageInfo
.endCursor
}
)
@ -301,7 +322,9 @@ export const ProductList: React.StatelessComponent<ProductListProps> = ({
</Button>
<Button
color="primary"
onClick={() => openModal("publish", listElements)}
onClick={() =>
openModal("publish", listElements)
}
>
<FormattedMessage
defaultMessage="Publish"
@ -310,7 +333,9 @@ export const ProductList: React.StatelessComponent<ProductListProps> = ({
</Button>
<IconButton
color="primary"
onClick={() => openModal("delete", listElements)}
onClick={() =>
openModal("delete", listElements)
}
>
<DeleteIcon />
</IconButton>
@ -320,7 +345,9 @@ export const ProductList: React.StatelessComponent<ProductListProps> = ({
selected={listElements.length}
toggle={toggle}
toggleAll={toggleAll}
onSearchChange={query => changeFilterField({ query })}
onSearchChange={query =>
changeFilterField({ query })
}
onFilterAdd={filter =>
changeFilterField(createFilter(filter))
}
@ -444,7 +471,10 @@ export const ProductList: React.StatelessComponent<ProductListProps> = ({
confirmButtonState="default"
onClose={closeModal}
onSubmit={handleFilterTabDelete}
tabName={maybe(() => tabs[currentTab - 1].name, "...")}
tabName={maybe(
() => tabs[currentTab - 1].name,
"..."
)}
/>
</>
);
@ -455,6 +485,8 @@ export const ProductList: React.StatelessComponent<ProductListProps> = ({
);
}}
</TypedProductListQuery>
)}
</AvailableInGridAttributesQuery>
);
};
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",
errors: [],
onClose: () => undefined,
onFetch: () => undefined,
onOpen: () => undefined,
onSubmit: () => undefined,
onToggle: () => undefined,

View file

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

View file

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