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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,15 +1,18 @@
import Button from "@material-ui/core/Button"; import Button from "@material-ui/core/Button";
import Card from "@material-ui/core/Card"; import Card from "@material-ui/core/Card";
import CardContent from "@material-ui/core/CardContent"; import CardContent from "@material-ui/core/CardContent";
import CircularProgress from "@material-ui/core/CircularProgress";
import { Theme } from "@material-ui/core/styles"; import { Theme } from "@material-ui/core/styles";
import Typography from "@material-ui/core/Typography"; import Typography from "@material-ui/core/Typography";
import makeStyles from "@material-ui/styles/makeStyles"; import makeStyles from "@material-ui/styles/makeStyles";
import classNames from "classnames"; import classNames from "classnames";
import React from "react"; import React from "react";
import InfiniteScroll from "react-infinite-scroller";
import { FormattedMessage } from "react-intl"; import { FormattedMessage } from "react-intl";
import useElementScroll from "@saleor/hooks/useElementScroll"; import useElementScroll from "@saleor/hooks/useElementScroll";
import { buttonMessages } from "@saleor/intl"; import { buttonMessages } from "@saleor/intl";
import { FetchMoreProps } from "@saleor/types";
import { isSelected } from "@saleor/utils/lists"; import { isSelected } from "@saleor/utils/lists";
import ControlledCheckbox from "../ControlledCheckbox"; import ControlledCheckbox from "../ControlledCheckbox";
import Hr from "../Hr"; import Hr from "../Hr";
@ -18,9 +21,10 @@ export interface ColumnPickerChoice {
label: string; label: string;
value: string; value: string;
} }
export interface ColumnPickerContentProps { export interface ColumnPickerContentProps extends Partial<FetchMoreProps> {
columns: ColumnPickerChoice[]; columns: ColumnPickerChoice[];
selectedColumns: string[]; selectedColumns: string[];
total?: number;
onCancel: () => void; onCancel: () => void;
onColumnToggle: (column: string) => void; onColumnToggle: (column: string) => void;
onReset: () => void; onReset: () => void;
@ -50,15 +54,29 @@ const useStyles = makeStyles((theme: Theme) => ({
}, },
dropShadow: { dropShadow: {
boxShadow: `0px -5px 10px 0px ${theme.overrides.MuiCard.root.borderColor}` boxShadow: `0px -5px 10px 0px ${theme.overrides.MuiCard.root.borderColor}`
},
loadMoreLoaderContainer: {
alignItems: "center",
display: "flex",
gridColumnEnd: "span 3",
height: theme.spacing.unit * 3,
justifyContent: "center"
},
root: {
boxShadow: "0px 4px 4px rgba(0, 0, 0, 0.25)"
} }
})); }));
const ColumnPickerContent: React.FC<ColumnPickerContentProps> = props => { const ColumnPickerContent: React.FC<ColumnPickerContentProps> = props => {
const { const {
columns, columns,
hasMore,
loading,
selectedColumns, selectedColumns,
total,
onCancel, onCancel,
onColumnToggle, onColumnToggle,
onFetchMore,
onReset, onReset,
onSave onSave
} = props; } = props;
@ -72,7 +90,7 @@ const ColumnPickerContent: React.FC<ColumnPickerContentProps> = props => {
: false; : false;
return ( return (
<Card> <Card className={classes.root}>
<CardContent> <CardContent>
<Typography color="textSecondary"> <Typography color="textSecondary">
<FormattedMessage <FormattedMessage
@ -80,12 +98,44 @@ const ColumnPickerContent: React.FC<ColumnPickerContentProps> = props => {
description="pick columns to display" description="pick columns to display"
values={{ values={{
numberOfSelected: selectedColumns.length, numberOfSelected: selectedColumns.length,
numberOfTotal: columns.length numberOfTotal: total || columns.length
}} }}
/> />
</Typography> </Typography>
</CardContent> </CardContent>
<Hr /> <Hr />
{hasMore && onFetchMore ? (
<InfiniteScroll
pageStart={0}
loadMore={onFetchMore}
hasMore={hasMore}
useWindow={false}
threshold={100}
key="infinite-scroll"
>
<CardContent className={classes.contentContainer}>
<div className={classes.content} ref={anchor}>
{columns.map(column => (
<ControlledCheckbox
checked={isSelected(
column.value,
selectedColumns,
(a, b) => a === b
)}
name={column.value}
label={column.label}
onChange={() => onColumnToggle(column.value)}
/>
))}
{loading && (
<div className={classes.loadMoreLoaderContainer}>
<CircularProgress size={16} />
</div>
)}
</div>
</CardContent>
</InfiniteScroll>
) : (
<CardContent className={classes.contentContainer}> <CardContent className={classes.contentContainer}>
<div className={classes.content} ref={anchor}> <div className={classes.content} ref={anchor}>
{columns.map(column => ( {columns.map(column => (
@ -102,6 +152,7 @@ const ColumnPickerContent: React.FC<ColumnPickerContentProps> = props => {
))} ))}
</div> </div>
</CardContent> </CardContent>
)}
<Hr /> <Hr />
<CardContent <CardContent
className={classNames(classes.actionBarContainer, { className={classNames(classes.actionBarContainer, {

View file

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

View file

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

View file

@ -101,8 +101,7 @@ const TableHead = withStyles(styles, {
})} })}
/> />
)} )}
{(items === undefined || items.length > 0) && {(items === undefined || items.length > 0) && (
(selected && (
<TableCell <TableCell
padding="checkbox" padding="checkbox"
className={classNames({ className={classNames({
@ -120,7 +119,7 @@ const TableHead = withStyles(styles, {
onChange={() => toggleAll(items, selected)} onChange={() => toggleAll(items, selected)}
/> />
</TableCell> </TableCell>
))} )}
{selected ? ( {selected ? (
<> <>
<TableCell <TableCell

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,10 @@
import gql from "graphql-tag"; import gql from "graphql-tag";
import { TypedQuery } from "../queries"; import { pageInfoFragment, TypedQuery } from "../queries";
import {
AvailableInGridAttributes,
AvailableInGridAttributesVariables
} from "./types/AvailableInGridAttributes";
import { ProductCreateData } from "./types/ProductCreateData"; import { ProductCreateData } from "./types/ProductCreateData";
import { import {
ProductDetails, ProductDetails,
@ -225,6 +229,15 @@ const productListQuery = gql`
edges { edges {
node { node {
...ProductFragment ...ProductFragment
attributes {
attribute {
id
}
values {
id
name
}
}
} }
} }
pageInfo { pageInfo {
@ -361,3 +374,38 @@ export const TypedProductImageQuery = TypedQuery<
ProductImageById, ProductImageById,
ProductImageByIdVariables ProductImageByIdVariables
>(productImageQuery); >(productImageQuery);
const availableInGridAttributes = gql`
${pageInfoFragment}
query GridAttributes($first: Int!, $after: String, $ids: [ID!]!) {
availableInGrid: attributes(
first: $first
after: $after
filter: { availableInGrid: true, isVariantOnly: false }
) {
edges {
node {
id
name
}
}
pageInfo {
...PageInfoFragment
}
totalCount
}
grid: attributes(first: 25, filter: { ids: $ids }) {
edges {
node {
id
name
}
}
}
}
`;
export const AvailableInGridAttributesQuery = TypedQuery<
AvailableInGridAttributes,
AvailableInGridAttributesVariables
>(availableInGridAttributes);

View file

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

View file

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

View file

@ -25,6 +25,23 @@ export interface ProductList_products_edges_node_productType {
name: string; name: string;
} }
export interface ProductList_products_edges_node_attributes_attribute {
__typename: "Attribute";
id: string;
}
export interface ProductList_products_edges_node_attributes_values {
__typename: "AttributeValue";
id: string;
name: string | null;
}
export interface ProductList_products_edges_node_attributes {
__typename: "SelectedAttribute";
attribute: ProductList_products_edges_node_attributes_attribute;
values: (ProductList_products_edges_node_attributes_values | null)[];
}
export interface ProductList_products_edges_node { export interface ProductList_products_edges_node {
__typename: "Product"; __typename: "Product";
id: string; id: string;
@ -33,6 +50,7 @@ export interface ProductList_products_edges_node {
isAvailable: boolean | null; isAvailable: boolean | null;
basePrice: ProductList_products_edges_node_basePrice | null; basePrice: ProductList_products_edges_node_basePrice | null;
productType: ProductList_products_edges_node_productType; productType: ProductList_products_edges_node_productType;
attributes: ProductList_products_edges_node_attributes[];
} }
export interface ProductList_products_edges { export interface ProductList_products_edges {

View file

@ -20,6 +20,7 @@ import usePaginator, {
createPaginationState createPaginationState
} from "@saleor/hooks/usePaginator"; } from "@saleor/hooks/usePaginator";
import useShop from "@saleor/hooks/useShop"; import useShop from "@saleor/hooks/useShop";
import { commonMessages } from "@saleor/intl";
import { getMutationState, maybe } from "@saleor/misc"; import { getMutationState, maybe } from "@saleor/misc";
import { ListViews } from "@saleor/types"; import { ListViews } from "@saleor/types";
import ProductListPage from "../../components/ProductListPage"; import ProductListPage from "../../components/ProductListPage";
@ -27,7 +28,10 @@ import {
TypedProductBulkDeleteMutation, TypedProductBulkDeleteMutation,
TypedProductBulkPublishMutation TypedProductBulkPublishMutation
} from "../../mutations"; } from "../../mutations";
import { TypedProductListQuery } from "../../queries"; import {
AvailableInGridAttributesQuery,
TypedProductListQuery
} from "../../queries";
import { productBulkDelete } from "../../types/productBulkDelete"; import { productBulkDelete } from "../../types/productBulkDelete";
import { productBulkPublish } from "../../types/productBulkPublish"; import { productBulkPublish } from "../../types/productBulkPublish";
import { import {
@ -69,6 +73,18 @@ export const ProductList: React.StatelessComponent<ProductListProps> = ({
); );
const intl = useIntl(); const intl = useIntl();
React.useEffect(
() =>
navigate(
productListUrl({
...params,
after: undefined,
before: undefined
})
),
[settings.rowNumber]
);
const tabs = getFilterTabs(); const tabs = getFilterTabs();
const currentTab = const currentTab =
@ -145,6 +161,10 @@ export const ProductList: React.StatelessComponent<ProductListProps> = ({
); );
return ( return (
<AvailableInGridAttributesQuery
variables={{ first: 6, ids: settings.columns }}
>
{attributes => (
<TypedProductListQuery displayLoader variables={queryVariables}> <TypedProductListQuery displayLoader variables={queryVariables}>
{({ data, loading, refetch }) => { {({ data, loading, refetch }) => {
const { loadNextPage, loadPreviousPage, pageInfo } = paginate( const { loadNextPage, loadPreviousPage, pageInfo } = paginate(
@ -157,9 +177,7 @@ export const ProductList: React.StatelessComponent<ProductListProps> = ({
if (data.productBulkDelete.errors.length === 0) { if (data.productBulkDelete.errors.length === 0) {
closeModal(); closeModal();
notify({ notify({
text: intl.formatMessage({ text: intl.formatMessage(commonMessages.savedChanges)
defaultMessage: "Products removed"
})
}); });
reset(); reset();
refetch(); refetch();
@ -170,10 +188,7 @@ export const ProductList: React.StatelessComponent<ProductListProps> = ({
if (data.productBulkPublish.errors.length === 0) { if (data.productBulkPublish.errors.length === 0) {
closeModal(); closeModal();
notify({ notify({
text: intl.formatMessage({ text: intl.formatMessage(commonMessages.savedChanges)
defaultMessage: "Changed publication status",
description: "product status update notification"
})
}); });
reset(); reset();
refetch(); refetch();
@ -183,13 +198,16 @@ export const ProductList: React.StatelessComponent<ProductListProps> = ({
return ( return (
<TypedProductBulkDeleteMutation onCompleted={handleBulkDelete}> <TypedProductBulkDeleteMutation onCompleted={handleBulkDelete}>
{(productBulkDelete, productBulkDeleteOpts) => ( {(productBulkDelete, productBulkDeleteOpts) => (
<TypedProductBulkPublishMutation onCompleted={handleBulkPublish}> <TypedProductBulkPublishMutation
onCompleted={handleBulkPublish}
>
{(productBulkPublish, productBulkPublishOpts) => { {(productBulkPublish, productBulkPublishOpts) => {
const bulkDeleteMutationState = getMutationState( const bulkDeleteMutationState = getMutationState(
productBulkDeleteOpts.called, productBulkDeleteOpts.called,
productBulkDeleteOpts.loading, productBulkDeleteOpts.loading,
maybe( maybe(
() => productBulkDeleteOpts.data.productBulkDelete.errors () =>
productBulkDeleteOpts.data.productBulkDelete.errors
) )
); );
@ -198,19 +216,45 @@ export const ProductList: React.StatelessComponent<ProductListProps> = ({
productBulkPublishOpts.loading, productBulkPublishOpts.loading,
maybe( maybe(
() => () =>
productBulkPublishOpts.data.productBulkPublish.errors productBulkPublishOpts.data.productBulkPublish
.errors
) )
); );
return ( return (
<> <>
<ProductListPage <ProductListPage
availableInGridAttributes={maybe(
() =>
attributes.data.availableInGrid.edges.map(
edge => edge.node
),
[]
)}
currencySymbol={currencySymbol} currencySymbol={currencySymbol}
currentTab={currentTab} currentTab={currentTab}
defaultSettings={ defaultSettings={
defaultListSettings[ListViews.PRODUCT_LIST] defaultListSettings[ListViews.PRODUCT_LIST]
} }
gridAttributes={maybe(
() =>
attributes.data.grid.edges.map(
edge => edge.node
),
[]
)}
totalGridAttributes={maybe(
() => attributes.data.availableInGrid.totalCount,
0
)}
settings={settings} settings={settings}
loading={attributes.loading}
hasMore={maybe(
() =>
attributes.data.availableInGrid.pageInfo
.hasNextPage,
false
)}
filtersList={createFilterChips( filtersList={createFilterChips(
params, params,
{ {
@ -225,6 +269,34 @@ export const ProductList: React.StatelessComponent<ProductListProps> = ({
products={maybe(() => products={maybe(() =>
data.products.edges.map(edge => edge.node) data.products.edges.map(edge => edge.node)
)} )}
onFetchMore={() =>
attributes.loadMore(
(prev, next) => {
if (
prev.availableInGrid.pageInfo.endCursor ===
next.availableInGrid.pageInfo.endCursor
) {
return prev;
}
return {
...prev,
availableInGrid: {
...prev.availableInGrid,
edges: [
...prev.availableInGrid.edges,
...next.availableInGrid.edges
],
pageInfo: next.availableInGrid.pageInfo
}
};
},
{
after:
attributes.data.availableInGrid.pageInfo
.endCursor
}
)
}
onNextPage={loadNextPage} onNextPage={loadNextPage}
onPreviousPage={loadPreviousPage} onPreviousPage={loadPreviousPage}
onUpdateListSettings={updateListSettings} onUpdateListSettings={updateListSettings}
@ -250,7 +322,9 @@ export const ProductList: React.StatelessComponent<ProductListProps> = ({
</Button> </Button>
<Button <Button
color="primary" color="primary"
onClick={() => openModal("publish", listElements)} onClick={() =>
openModal("publish", listElements)
}
> >
<FormattedMessage <FormattedMessage
defaultMessage="Publish" defaultMessage="Publish"
@ -259,7 +333,9 @@ export const ProductList: React.StatelessComponent<ProductListProps> = ({
</Button> </Button>
<IconButton <IconButton
color="primary" color="primary"
onClick={() => openModal("delete", listElements)} onClick={() =>
openModal("delete", listElements)
}
> >
<DeleteIcon /> <DeleteIcon />
</IconButton> </IconButton>
@ -269,7 +345,9 @@ export const ProductList: React.StatelessComponent<ProductListProps> = ({
selected={listElements.length} selected={listElements.length}
toggle={toggle} toggle={toggle}
toggleAll={toggleAll} toggleAll={toggleAll}
onSearchChange={query => changeFilterField({ query })} onSearchChange={query =>
changeFilterField({ query })
}
onFilterAdd={filter => onFilterAdd={filter =>
changeFilterField(createFilter(filter)) changeFilterField(createFilter(filter))
} }
@ -393,7 +471,10 @@ export const ProductList: React.StatelessComponent<ProductListProps> = ({
confirmButtonState="default" confirmButtonState="default"
onClose={closeModal} onClose={closeModal}
onSubmit={handleFilterTabDelete} onSubmit={handleFilterTabDelete}
tabName={maybe(() => tabs[currentTab - 1].name, "...")} tabName={maybe(
() => tabs[currentTab - 1].name,
"..."
)}
/> />
</> </>
); );
@ -404,6 +485,8 @@ export const ProductList: React.StatelessComponent<ProductListProps> = ({
); );
}} }}
</TypedProductListQuery> </TypedProductListQuery>
)}
</AvailableInGridAttributesQuery>
); );
}; };
export default ProductList; export default ProductList;

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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