Add attributes as columns in product list view
This commit is contained in:
parent
bc2c6a9c71
commit
85cd2e875e
25 changed files with 1831 additions and 2569 deletions
|
@ -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;
|
0
src/categories/components/CategoryProductList/index.ts
Normal file
0
src/categories/components/CategoryProductList/index.ts
Normal 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;
|
||||
|
|
|
@ -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;
|
|
@ -1,2 +0,0 @@
|
|||
export { default } from "./CategoryProductsCard";
|
||||
export * from "./CategoryProductsCard";
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
|
|
|
@ -134,6 +134,5 @@ export const filters: Filter[] = [
|
|||
export const fetchMoreProps: FetchMoreProps = {
|
||||
hasMore: true,
|
||||
loading: false,
|
||||
onFetch: () => undefined,
|
||||
onFetchMore: () => undefined
|
||||
};
|
||||
|
|
318
src/products/components/ProductList/ProductList.tsx
Normal file
318
src/products/components/ProductList/ProductList.tsx
Normal 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;
|
1
src/products/components/ProductList/index.ts
Normal file
1
src/products/components/ProductList/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export { default } from "./ProductList";
|
|
@ -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>
|
||||
|
|
13
src/products/components/ProductListPage/utils.ts
Normal file
13
src/products/components/ProductListPage/utils.ts
Normal 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
|
@ -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<
|
||||
|
|
|
@ -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[];
|
||||
}
|
||||
|
|
60
src/products/types/GridAttributes.ts
Normal file
60
src/products/types/GridAttributes.ts
Normal 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[];
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
));
|
|
@ -15,6 +15,7 @@ const props: AssignAttributeDialogProps = {
|
|||
confirmButtonState: "default",
|
||||
errors: [],
|
||||
onClose: () => undefined,
|
||||
onFetch: () => undefined,
|
||||
onOpen: () => undefined,
|
||||
onSubmit: () => undefined,
|
||||
onToggle: () => undefined,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
24
src/utils/columns/DisplayColumn.tsx
Normal file
24
src/utils/columns/DisplayColumn.tsx
Normal 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;
|
Loading…
Reference in a new issue