Update from saleor/master

This commit is contained in:
dominik-zeglen 2019-08-09 13:14:35 +02:00
parent 9551b50351
commit bd211a6720
262 changed files with 35477 additions and 13429 deletions

View file

@ -10,14 +10,14 @@ import Typography from "@material-ui/core/Typography";
import React from "react"; import React from "react";
import SVG from "react-inlinesvg"; import SVG from "react-inlinesvg";
import * as backgroundArt from "@assets/images/login-background.svg"; import backgroundArt from "@assets/images/login-background.svg";
import * as saleorDarkLogo from "@assets/images/logo-dark.svg"; import saleorDarkLogo from "@assets/images/logo-dark.svg";
import * as saleorLightLogo from "@assets/images/logo-light.svg"; import saleorLightLogo from "@assets/images/logo-light.svg";
import { ControlledCheckbox } from "@saleor/components/ControlledCheckbox"; import { ControlledCheckbox } from "@saleor/components/ControlledCheckbox";
import Form from "@saleor/components/Form"; import Form from "@saleor/components/Form";
import { FormSpacer } from "@saleor/components/FormSpacer"; import { FormSpacer } from "@saleor/components/FormSpacer";
import useTheme from "@saleor/hooks/useTheme"; import useTheme from "@saleor/hooks/useTheme";
import i18n from "../../../i18n"; import i18n from "@saleor/i18n";
export interface FormData { export interface FormData {
email: string; email: string;

View file

@ -16,6 +16,9 @@ export const fragmentUser = gql`
code code
name name
} }
avatar {
url
}
} }
`; `;

View file

@ -20,6 +20,11 @@ export interface TokenAuth_tokenCreate_user_permissions {
name: string; name: string;
} }
export interface TokenAuth_tokenCreate_user_avatar {
__typename: "Image";
url: string;
}
export interface TokenAuth_tokenCreate_user { export interface TokenAuth_tokenCreate_user {
__typename: "User"; __typename: "User";
id: string; id: string;
@ -29,6 +34,7 @@ export interface TokenAuth_tokenCreate_user {
isStaff: boolean; isStaff: boolean;
note: string | null; note: string | null;
permissions: (TokenAuth_tokenCreate_user_permissions | null)[] | null; permissions: (TokenAuth_tokenCreate_user_permissions | null)[] | null;
avatar: TokenAuth_tokenCreate_user_avatar | null;
} }
export interface TokenAuth_tokenCreate { export interface TokenAuth_tokenCreate {

View file

@ -14,6 +14,11 @@ export interface User_permissions {
name: string; name: string;
} }
export interface User_avatar {
__typename: "Image";
url: string;
}
export interface User { export interface User {
__typename: "User"; __typename: "User";
id: string; id: string;
@ -23,4 +28,5 @@ export interface User {
isStaff: boolean; isStaff: boolean;
note: string | null; note: string | null;
permissions: (User_permissions | null)[] | null; permissions: (User_permissions | null)[] | null;
avatar: User_avatar | null;
} }

View file

@ -14,6 +14,11 @@ export interface VerifyToken_tokenVerify_user_permissions {
name: string; name: string;
} }
export interface VerifyToken_tokenVerify_user_avatar {
__typename: "Image";
url: string;
}
export interface VerifyToken_tokenVerify_user { export interface VerifyToken_tokenVerify_user {
__typename: "User"; __typename: "User";
id: string; id: string;
@ -23,6 +28,7 @@ export interface VerifyToken_tokenVerify_user {
isStaff: boolean; isStaff: boolean;
note: string | null; note: string | null;
permissions: (VerifyToken_tokenVerify_user_permissions | null)[] | null; permissions: (VerifyToken_tokenVerify_user_permissions | null)[] | null;
avatar: VerifyToken_tokenVerify_user_avatar | null;
} }
export interface VerifyToken_tokenVerify { export interface VerifyToken_tokenVerify {

View file

@ -18,9 +18,9 @@ import Checkbox from "@saleor/components/Checkbox";
import Skeleton from "@saleor/components/Skeleton"; import Skeleton from "@saleor/components/Skeleton";
import TableHead from "@saleor/components/TableHead"; import TableHead from "@saleor/components/TableHead";
import TablePagination from "@saleor/components/TablePagination"; import TablePagination from "@saleor/components/TablePagination";
import i18n from "../../../i18n"; import i18n from "@saleor/i18n";
import { renderCollection } from "../../../misc"; import { renderCollection } from "@saleor/misc";
import { ListActions, ListProps } from "../../../types"; import { ListActions, ListProps } from "@saleor/types";
const styles = (theme: Theme) => const styles = (theme: Theme) =>
createStyles({ createStyles({
@ -67,11 +67,14 @@ interface CategoryListProps
onAdd?(); onAdd?();
} }
const numberOfColumns = 4;
const CategoryList = withStyles(styles, { name: "CategoryList" })( const CategoryList = withStyles(styles, { name: "CategoryList" })(
({ ({
categories, categories,
classes, classes,
disabled, disabled,
settings,
isRoot, isRoot,
pageInfo, pageInfo,
isChecked, isChecked,
@ -82,6 +85,7 @@ const CategoryList = withStyles(styles, { name: "CategoryList" })(
onAdd, onAdd,
onNextPage, onNextPage,
onPreviousPage, onPreviousPage,
onUpdateListSettings,
onRowClick onRowClick
}: CategoryListProps) => ( }: CategoryListProps) => (
<Card> <Card>
@ -97,6 +101,7 @@ const CategoryList = withStyles(styles, { name: "CategoryList" })(
)} )}
<Table> <Table>
<TableHead <TableHead
colSpan={numberOfColumns}
selected={selected} selected={selected}
disabled={disabled} disabled={disabled}
items={categories} items={categories}
@ -116,9 +121,11 @@ const CategoryList = withStyles(styles, { name: "CategoryList" })(
<TableFooter> <TableFooter>
<TableRow> <TableRow>
<TablePagination <TablePagination
colSpan={4} colSpan={numberOfColumns}
settings={settings}
hasNextPage={pageInfo && !disabled ? pageInfo.hasNextPage : false} hasNextPage={pageInfo && !disabled ? pageInfo.hasNextPage : false}
onNextPage={onNextPage} onNextPage={onNextPage}
onUpdateListSettings={onUpdateListSettings}
hasPreviousPage={ hasPreviousPage={
pageInfo && !disabled ? pageInfo.hasPreviousPage : false pageInfo && !disabled ? pageInfo.hasPreviousPage : false
} }
@ -144,6 +151,7 @@ const CategoryList = withStyles(styles, { name: "CategoryList" })(
<Checkbox <Checkbox
checked={isSelected} checked={isSelected}
disabled={disabled} disabled={disabled}
disableClickPropagation
onChange={() => toggle(category.id)} onChange={() => toggle(category.id)}
/> />
</TableCell> </TableCell>
@ -173,7 +181,7 @@ const CategoryList = withStyles(styles, { name: "CategoryList" })(
}, },
() => ( () => (
<TableRow> <TableRow>
<TableCell colSpan={4}> <TableCell colSpan={numberOfColumns}>
{isRoot {isRoot
? i18n.t("No categories found") ? i18n.t("No categories found")
: i18n.t("No subcategories found")} : i18n.t("No subcategories found")}

View file

@ -4,8 +4,8 @@ import React from "react";
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 i18n from "../../../i18n"; import i18n from "@saleor/i18n";
import { ListActions, PageListProps } from "../../../types"; import { ListActions, PageListProps } from "@saleor/types";
import CategoryList from "../CategoryList"; import CategoryList from "../CategoryList";
export interface CategoryTableProps extends PageListProps, ListActions { export interface CategoryTableProps extends PageListProps, ListActions {
@ -24,9 +24,11 @@ export interface CategoryTableProps extends PageListProps, ListActions {
export const CategoryListPage: React.StatelessComponent<CategoryTableProps> = ({ export const CategoryListPage: React.StatelessComponent<CategoryTableProps> = ({
categories, categories,
disabled, disabled,
settings,
onAdd, onAdd,
onNextPage, onNextPage,
onPreviousPage, onPreviousPage,
onUpdateListSettings,
onRowClick, onRowClick,
pageInfo, pageInfo,
isChecked, isChecked,
@ -46,9 +48,11 @@ export const CategoryListPage: React.StatelessComponent<CategoryTableProps> = ({
onAdd={onAdd} onAdd={onAdd}
onRowClick={onRowClick} onRowClick={onRowClick}
disabled={disabled} disabled={disabled}
settings={settings}
isRoot={true} isRoot={true}
onNextPage={onNextPage} onNextPage={onNextPage}
onPreviousPage={onPreviousPage} onPreviousPage={onPreviousPage}
onUpdateListSettings={onUpdateListSettings}
pageInfo={pageInfo} pageInfo={pageInfo}
isChecked={isChecked} isChecked={isChecked}
selected={selected} selected={selected}

View file

@ -40,6 +40,10 @@ export const CategoryProductsCard: React.StatelessComponent<
} }
/> />
<ProductList <ProductList
settings={{
columns: ["isPublished", "price", "productType"],
rowNumber: undefined
}}
products={products} products={products}
disabled={disabled} disabled={disabled}
pageInfo={pageInfo} pageInfo={pageInfo}

View file

@ -5,13 +5,14 @@ import React from "react";
import ActionDialog from "@saleor/components/ActionDialog"; import ActionDialog from "@saleor/components/ActionDialog";
import useBulkActions from "@saleor/hooks/useBulkActions"; import useBulkActions from "@saleor/hooks/useBulkActions";
import useListSettings from "@saleor/hooks/useListSettings";
import useNavigator from "@saleor/hooks/useNavigator"; import useNavigator from "@saleor/hooks/useNavigator";
import usePaginator, { import usePaginator, {
createPaginationState createPaginationState
} from "@saleor/hooks/usePaginator"; } from "@saleor/hooks/usePaginator";
import { PAGINATE_BY } from "../../config"; import i18n from "@saleor/i18n";
import i18n from "../../i18n"; import { getMutationState, maybe } from "@saleor/misc";
import { getMutationState, maybe } from "../../misc"; import { ListViews } from "@saleor/types";
import { CategoryListPage } from "../components/CategoryListPage/CategoryListPage"; import { CategoryListPage } from "../components/CategoryListPage/CategoryListPage";
import { TypedCategoryBulkDeleteMutation } from "../mutations"; import { TypedCategoryBulkDeleteMutation } from "../mutations";
import { TypedRootCategoriesQuery } from "../queries"; import { TypedRootCategoriesQuery } from "../queries";
@ -35,8 +36,10 @@ export const CategoryList: React.StatelessComponent<CategoryListProps> = ({
const { isSelected, listElements, toggle, toggleAll, reset } = useBulkActions( const { isSelected, listElements, toggle, toggleAll, reset } = useBulkActions(
params.ids params.ids
); );
const { updateListSettings, settings } = useListSettings(
const paginationState = createPaginationState(PAGINATE_BY, params); ListViews.CATEGORY_LIST
);
const paginationState = createPaginationState(settings.rowNumber, params);
return ( return (
<TypedRootCategoriesQuery displayLoader variables={paginationState}> <TypedRootCategoriesQuery displayLoader variables={paginationState}>
{({ data, loading, refetch }) => { {({ data, loading, refetch }) => {
@ -73,11 +76,13 @@ export const CategoryList: React.StatelessComponent<CategoryListProps> = ({
() => data.categories.edges.map(edge => edge.node), () => data.categories.edges.map(edge => edge.node),
[] []
)} )}
settings={settings}
onAdd={() => navigate(categoryAddUrl())} onAdd={() => navigate(categoryAddUrl())}
onRowClick={id => () => navigate(categoryUrl(id))} onRowClick={id => () => navigate(categoryUrl(id))}
disabled={loading} disabled={loading}
onNextPage={loadNextPage} onNextPage={loadNextPage}
onPreviousPage={loadPreviousPage} onPreviousPage={loadPreviousPage}
onUpdateListSettings={updateListSettings}
pageInfo={pageInfo} pageInfo={pageInfo}
isChecked={isSelected} isChecked={isSelected}
selected={listElements.length} selected={listElements.length}

View file

@ -17,9 +17,9 @@ import Skeleton from "@saleor/components/Skeleton";
import StatusLabel from "@saleor/components/StatusLabel"; import StatusLabel from "@saleor/components/StatusLabel";
import TableHead from "@saleor/components/TableHead"; import TableHead from "@saleor/components/TableHead";
import TablePagination from "@saleor/components/TablePagination"; import TablePagination from "@saleor/components/TablePagination";
import i18n from "../../../i18n"; import i18n from "@saleor/i18n";
import { maybe, renderCollection } from "../../../misc"; import { maybe, renderCollection } from "@saleor/misc";
import { ListActions, ListProps } from "../../../types"; import { ListActions, ListProps } from "@saleor/types";
import { CollectionList_collections_edges_node } from "../../types/CollectionList"; import { CollectionList_collections_edges_node } from "../../types/CollectionList";
const styles = (theme: Theme) => const styles = (theme: Theme) =>
@ -50,13 +50,17 @@ interface CollectionListProps
collections: CollectionList_collections_edges_node[]; collections: CollectionList_collections_edges_node[];
} }
const numberOfColumns = 5;
const CollectionList = withStyles(styles, { name: "CollectionList" })( const CollectionList = withStyles(styles, { name: "CollectionList" })(
({ ({
classes, classes,
collections, collections,
disabled, disabled,
settings,
onNextPage, onNextPage,
onPreviousPage, onPreviousPage,
onUpdateListSettings,
onRowClick, onRowClick,
pageInfo, pageInfo,
isChecked, isChecked,
@ -68,6 +72,7 @@ const CollectionList = withStyles(styles, { name: "CollectionList" })(
<Card> <Card>
<Table> <Table>
<TableHead <TableHead
colSpan={numberOfColumns}
selected={selected} selected={selected}
disabled={disabled} disabled={disabled}
items={collections} items={collections}
@ -89,9 +94,11 @@ const CollectionList = withStyles(styles, { name: "CollectionList" })(
<TableFooter> <TableFooter>
<TableRow> <TableRow>
<TablePagination <TablePagination
colSpan={5} colSpan={numberOfColumns}
settings={settings}
hasNextPage={pageInfo && !disabled ? pageInfo.hasNextPage : false} hasNextPage={pageInfo && !disabled ? pageInfo.hasNextPage : false}
onNextPage={onNextPage} onNextPage={onNextPage}
onUpdateListSettings={onUpdateListSettings}
hasPreviousPage={ hasPreviousPage={
pageInfo && !disabled ? pageInfo.hasPreviousPage : false pageInfo && !disabled ? pageInfo.hasPreviousPage : false
} }
@ -116,6 +123,7 @@ const CollectionList = withStyles(styles, { name: "CollectionList" })(
<Checkbox <Checkbox
checked={isSelected} checked={isSelected}
disabled={disabled} disabled={disabled}
disableClickPropagation
onChange={() => toggle(collection.id)} onChange={() => toggle(collection.id)}
/> />
</TableCell> </TableCell>
@ -151,7 +159,7 @@ const CollectionList = withStyles(styles, { name: "CollectionList" })(
}, },
() => ( () => (
<TableRow> <TableRow>
<TableCell colSpan={3}> <TableCell colSpan={numberOfColumns}>
{i18n.t("No collections found")} {i18n.t("No collections found")}
</TableCell> </TableCell>
</TableRow> </TableRow>

View file

@ -4,8 +4,8 @@ import React from "react";
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 i18n from "../../../i18n"; import i18n from "@saleor/i18n";
import { ListActions, PageListProps } from "../../../types"; import { ListActions, PageListProps } from "@saleor/types";
import { CollectionList_collections_edges_node } from "../../types/CollectionList"; import { CollectionList_collections_edges_node } from "../../types/CollectionList";
import CollectionList from "../CollectionList/CollectionList"; import CollectionList from "../CollectionList/CollectionList";

View file

@ -19,7 +19,9 @@ import CardTitle from "@saleor/components/CardTitle";
import Checkbox from "@saleor/components/Checkbox"; import Checkbox from "@saleor/components/Checkbox";
import Skeleton from "@saleor/components/Skeleton"; import Skeleton from "@saleor/components/Skeleton";
import StatusLabel from "@saleor/components/StatusLabel"; import StatusLabel from "@saleor/components/StatusLabel";
import TableCellAvatar from "@saleor/components/TableCellAvatar"; import TableCellAvatar, {
AVATAR_MARGIN
} from "@saleor/components/TableCellAvatar";
import TableHead from "@saleor/components/TableHead"; import TableHead from "@saleor/components/TableHead";
import TablePagination from "@saleor/components/TablePagination"; import TablePagination from "@saleor/components/TablePagination";
import i18n from "../../../i18n"; import i18n from "../../../i18n";
@ -29,12 +31,27 @@ import { CollectionDetails_collection } from "../../types/CollectionDetails";
const styles = (theme: Theme) => const styles = (theme: Theme) =>
createStyles({ createStyles({
iconCell: { colActions: {
"&:last-child": { "&:last-child": {
paddingRight: 0 paddingRight: 0
}, },
width: 48 + theme.spacing.unit / 2 width: 48 + theme.spacing.unit / 2
}, },
colName: {
width: "auto"
},
colNameLabel: {
marginLeft: AVATAR_MARGIN
},
colPublished: {
width: 200
},
colType: {
width: 200
},
table: {
tableLayout: "fixed"
},
tableRow: { tableRow: {
cursor: "pointer" cursor: "pointer"
} }
@ -48,6 +65,8 @@ export interface CollectionProductsProps
onProductUnassign: (id: string, event: React.MouseEvent<any>) => void; onProductUnassign: (id: string, event: React.MouseEvent<any>) => void;
} }
const numberOfColumns = 5;
const CollectionProducts = withStyles(styles, { name: "CollectionProducts" })( const CollectionProducts = withStyles(styles, { name: "CollectionProducts" })(
({ ({
classes, classes,
@ -89,24 +108,32 @@ const CollectionProducts = withStyles(styles, { name: "CollectionProducts" })(
</Button> </Button>
} }
/> />
<Table> <Table className={classes.table}>
<TableHead <TableHead
colSpan={numberOfColumns}
selected={selected} selected={selected}
disabled={disabled} disabled={disabled}
items={maybe(() => collection.products.edges.map(edge => edge.node))} items={maybe(() => collection.products.edges.map(edge => edge.node))}
toggleAll={toggleAll} toggleAll={toggleAll}
toolbar={toolbar} toolbar={toolbar}
> >
<TableCell>{i18n.t("Name", { context: "table header" })}</TableCell> <TableCell className={classes.colName}>
<TableCell>{i18n.t("Type", { context: "table header" })}</TableCell> <span className={classes.colNameLabel}>
<TableCell> {i18n.t("Name", { context: "table header" })}
</span>
</TableCell>
<TableCell className={classes.colType}>
{i18n.t("Type", { context: "table header" })}
</TableCell>
<TableCell className={classes.colPublished}>
{i18n.t("Published", { context: "table header" })} {i18n.t("Published", { context: "table header" })}
</TableCell> </TableCell>
<TableCell className={classes.colActions} />
</TableHead> </TableHead>
<TableFooter> <TableFooter>
<TableRow> <TableRow>
<TablePagination <TablePagination
colSpan={6} colSpan={numberOfColumns}
hasNextPage={maybe(() => pageInfo.hasNextPage)} hasNextPage={maybe(() => pageInfo.hasNextPage)}
onNextPage={onNextPage} onNextPage={onNextPage}
hasPreviousPage={maybe(() => pageInfo.hasPreviousPage)} hasPreviousPage={maybe(() => pageInfo.hasPreviousPage)}
@ -132,22 +159,23 @@ const CollectionProducts = withStyles(styles, { name: "CollectionProducts" })(
<Checkbox <Checkbox
checked={isSelected} checked={isSelected}
disabled={disabled} disabled={disabled}
disableClickPropagation
onChange={() => toggle(product.id)} onChange={() => toggle(product.id)}
/> />
</TableCell> </TableCell>
<TableCellAvatar <TableCellAvatar
className={classes.colName}
thumbnail={maybe(() => product.thumbnail.url)} thumbnail={maybe(() => product.thumbnail.url)}
/> >
<TableCell>
{maybe<React.ReactNode>(() => product.name, <Skeleton />)} {maybe<React.ReactNode>(() => product.name, <Skeleton />)}
</TableCell> </TableCellAvatar>
<TableCell> <TableCell className={classes.colType}>
{maybe<React.ReactNode>( {maybe<React.ReactNode>(
() => product.productType.name, () => product.productType.name,
<Skeleton /> <Skeleton />
)} )}
</TableCell> </TableCell>
<TableCell> <TableCell className={classes.colPublished}>
{maybe( {maybe(
() => ( () => (
<StatusLabel <StatusLabel
@ -162,7 +190,7 @@ const CollectionProducts = withStyles(styles, { name: "CollectionProducts" })(
<Skeleton /> <Skeleton />
)} )}
</TableCell> </TableCell>
<TableCell className={classes.iconCell}> <TableCell className={classes.colActions}>
<IconButton <IconButton
disabled={!product} disabled={!product}
onClick={event => onProductUnassign(product.id, event)} onClick={event => onProductUnassign(product.id, event)}
@ -176,7 +204,9 @@ const CollectionProducts = withStyles(styles, { name: "CollectionProducts" })(
() => ( () => (
<TableRow> <TableRow>
<TableCell /> <TableCell />
<TableCell colSpan={6}>{i18n.t("No products found")}</TableCell> <TableCell colSpan={numberOfColumns}>
{i18n.t("No products found")}
</TableCell>
</TableRow> </TableRow>
) )
)} )}

View file

@ -22,6 +22,46 @@ export const collections: CollectionList_collections_edges_node[] = [
__typename: "ProductCountableConnection", __typename: "ProductCountableConnection",
totalCount: 4 totalCount: 4
} }
},
{
__typename: "Collection",
id: "Q29sbGVjdGlvbjoz",
isPublished: true,
name: "Vintage vibes",
products: {
__typename: "ProductCountableConnection",
totalCount: 4
}
},
{
__typename: "Collection",
id: "Q29sbGVjdGlvbjoa",
isPublished: true,
name: "Merry Christmas",
products: {
__typename: "ProductCountableConnection",
totalCount: 4
}
},
{
__typename: "Collection",
id: "Q29sbGVjdGlvbjob",
isPublished: true,
name: "80s Miami",
products: {
__typename: "ProductCountableConnection",
totalCount: 4
}
},
{
__typename: "Collection",
id: "Q29sbGVjdGlvbjoc",
isPublished: true,
name: "Yellow Submarine 2019",
products: {
__typename: "ProductCountableConnection",
totalCount: 4
}
} }
]; ];
export const collection: ( export const collection: (

View file

@ -287,14 +287,12 @@ export const CollectionDetails: React.StatelessComponent<
open={params.action === "assign"} open={params.action === "assign"}
onFetch={search} onFetch={search}
loading={result.loading} loading={result.loading}
onClose={() => navigate(collectionUrl(id), true, true)} onClose={closeModal}
onSubmit={formData => onSubmit={products =>
assignProduct.mutate({ assignProduct.mutate({
...paginationState, ...paginationState,
collectionId: id, collectionId: id,
productIds: formData.products.map( productIds: products.map(product => product.id)
product => product.id
)
}) })
} }
products={maybe(() => products={maybe(() =>

View file

@ -6,14 +6,15 @@ import React from "react";
import ActionDialog from "@saleor/components/ActionDialog"; import ActionDialog from "@saleor/components/ActionDialog";
import useBulkActions from "@saleor/hooks/useBulkActions"; import useBulkActions from "@saleor/hooks/useBulkActions";
import useListSettings from "@saleor/hooks/useListSettings";
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 usePaginator, { import usePaginator, {
createPaginationState createPaginationState
} from "@saleor/hooks/usePaginator"; } from "@saleor/hooks/usePaginator";
import { PAGINATE_BY } from "../../config"; import i18n from "@saleor/i18n";
import i18n from "../../i18n"; import { getMutationState, maybe } from "@saleor/misc";
import { getMutationState, maybe } from "../../misc"; import { ListViews } from "@saleor/types";
import CollectionListPage from "../components/CollectionListPage/CollectionListPage"; import CollectionListPage from "../components/CollectionListPage/CollectionListPage";
import { import {
TypedCollectionBulkDelete, TypedCollectionBulkDelete,
@ -43,6 +44,9 @@ export const CollectionList: React.StatelessComponent<CollectionListProps> = ({
const { isSelected, listElements, reset, toggle, toggleAll } = useBulkActions( const { isSelected, listElements, reset, toggle, toggleAll } = useBulkActions(
params.ids params.ids
); );
const { updateListSettings, settings } = useListSettings(
ListViews.COLLECTION_LIST
);
const closeModal = () => const closeModal = () =>
navigate( navigate(
@ -62,7 +66,7 @@ export const CollectionList: React.StatelessComponent<CollectionListProps> = ({
}) })
); );
const paginationState = createPaginationState(PAGINATE_BY, params); const paginationState = createPaginationState(settings.rowNumber, params);
return ( return (
<TypedCollectionListQuery displayLoader variables={paginationState}> <TypedCollectionListQuery displayLoader variables={paginationState}>
{({ data, loading, refetch }) => { {({ data, loading, refetch }) => {
@ -129,8 +133,10 @@ export const CollectionList: React.StatelessComponent<CollectionListProps> = ({
collections={maybe(() => collections={maybe(() =>
data.collections.edges.map(edge => edge.node) data.collections.edges.map(edge => edge.node)
)} )}
settings={settings}
onNextPage={loadNextPage} onNextPage={loadNextPage}
onPreviousPage={loadPreviousPage} onPreviousPage={loadPreviousPage}
onUpdateListSettings={updateListSettings}
pageInfo={pageInfo} pageInfo={pageInfo}
onRowClick={id => () => navigate(collectionUrl(id))} onRowClick={id => () => navigate(collectionUrl(id))}
toolbar={ toolbar={

View file

@ -9,7 +9,7 @@ import {
withStyles, withStyles,
WithStyles WithStyles
} from "@material-ui/core/styles"; } from "@material-ui/core/styles";
import * as classNames from "classnames"; import classNames from "classnames";
import React from "react"; import React from "react";
import i18n from "../../i18n"; import i18n from "../../i18n";

View file

@ -7,12 +7,13 @@ import {
import TextField from "@material-ui/core/TextField"; import TextField from "@material-ui/core/TextField";
import React from "react"; import React from "react";
import { AddressTypeInput } from "../../customers/types"; import { AddressTypeInput } from "@saleor/customers/types";
import i18n from "../../i18n"; import i18n from "@saleor/i18n";
import { maybe } from "../../misc"; import { FormErrors } from "@saleor/types";
import { FormErrors } from "../../types";
import FormSpacer from "../FormSpacer"; import FormSpacer from "../FormSpacer";
import SingleAutocompleteSelectField from "../SingleAutocompleteSelectField"; import SingleAutocompleteSelectField, {
SingleAutocompleteChoiceType
} from "../SingleAutocompleteSelectField";
const styles = (theme: Theme) => const styles = (theme: Theme) =>
createStyles({ createStyles({
@ -24,24 +25,25 @@ const styles = (theme: Theme) =>
}); });
interface AddressEditProps extends WithStyles<typeof styles> { interface AddressEditProps extends WithStyles<typeof styles> {
countries?: Array<{ countries: SingleAutocompleteChoiceType[];
code: string; countryDisplayValue: string;
label: string;
}>;
data: AddressTypeInput; data: AddressTypeInput;
disabled?: boolean; disabled?: boolean;
errors: FormErrors<keyof AddressTypeInput>; errors: FormErrors<keyof AddressTypeInput>;
onChange(event: React.ChangeEvent<any>); onChange(event: React.ChangeEvent<any>);
onCountryChange(event: React.ChangeEvent<any>);
} }
const AddressEdit = withStyles(styles, { name: "AddressEdit" })( const AddressEdit = withStyles(styles, { name: "AddressEdit" })(
({ ({
classes, classes,
countries, countries,
countryDisplayValue,
data, data,
disabled, disabled,
errors, errors,
onChange onChange,
onCountryChange
}: AddressEditProps) => ( }: AddressEditProps) => (
<> <>
<div className={classes.root}> <div className={classes.root}>
@ -152,16 +154,14 @@ const AddressEdit = withStyles(styles, { name: "AddressEdit" })(
<div> <div>
<SingleAutocompleteSelectField <SingleAutocompleteSelectField
disabled={disabled} disabled={disabled}
displayValue={countryDisplayValue}
error={!!errors.country} error={!!errors.country}
helperText={errors.country} helperText={errors.country}
label={i18n.t("Country")} label={i18n.t("Country")}
name="country" name="country"
onChange={onChange} onChange={onCountryChange}
value={data.country} value={data.country}
choices={maybe( choices={countries}
() => countries.map(c => ({ ...c, value: c.code })),
[]
)}
InputProps={{ InputProps={{
autoComplete: "off" autoComplete: "off"
}} }}

View file

@ -1,6 +1,8 @@
import Avatar from "@material-ui/core/Avatar";
import Chip from "@material-ui/core/Chip"; import Chip from "@material-ui/core/Chip";
import ClickAwayListener from "@material-ui/core/ClickAwayListener"; import ClickAwayListener from "@material-ui/core/ClickAwayListener";
import Grow from "@material-ui/core/Grow"; import Grow from "@material-ui/core/Grow";
import Hidden from "@material-ui/core/Hidden";
import LinearProgress from "@material-ui/core/LinearProgress"; import LinearProgress from "@material-ui/core/LinearProgress";
import MenuItem from "@material-ui/core/MenuItem"; import MenuItem from "@material-ui/core/MenuItem";
import Menu from "@material-ui/core/MenuList"; import Menu from "@material-ui/core/MenuList";
@ -12,23 +14,25 @@ import {
withStyles, withStyles,
WithStyles WithStyles
} from "@material-ui/core/styles"; } from "@material-ui/core/styles";
import * as classNames from "classnames"; import classNames from "classnames";
import React from "react"; import React from "react";
import SVG from "react-inlinesvg"; import SVG from "react-inlinesvg";
import { RouteComponentProps, withRouter } from "react-router"; import { RouteComponentProps, withRouter } from "react-router";
import * as saleorDarkLogo from "@assets/images/logo-dark.svg"; import saleorDarkLogoSmall from "@assets/logo-dark-small.svg";
import * as saleorLightLogo from "@assets/images/logo-light.svg"; import saleorDarkLogo from "@assets/logo-dark.svg";
import menuArrowIcon from "@assets/menu-arrow-icon.svg";
import AppProgressProvider from "@saleor/components/AppProgress"; import AppProgressProvider from "@saleor/components/AppProgress";
import useLocalStorage from "@saleor/hooks/useLocalStorage";
import useNavigator from "@saleor/hooks/useNavigator"; import useNavigator from "@saleor/hooks/useNavigator";
import useTheme from "@saleor/hooks/useTheme"; import useTheme from "@saleor/hooks/useTheme";
import useUser from "@saleor/hooks/useUser"; import useUser from "@saleor/hooks/useUser";
import i18n from "../../i18n"; import i18n from "@saleor/i18n";
import ArrowDropdown from "../../icons/ArrowDropdown"; import ArrowDropdown from "@saleor/icons/ArrowDropdown";
import Container from "../Container"; import Container from "../Container";
import AppActionContext from "./AppActionContext"; import AppActionContext from "./AppActionContext";
import AppHeaderContext from "./AppHeaderContext"; import AppHeaderContext from "./AppHeaderContext";
import { appLoaderHeight, drawerWidth } from "./consts"; import { appLoaderHeight, drawerWidth, drawerWidthExpanded } from "./consts";
import MenuList from "./MenuList"; import MenuList from "./MenuList";
import menuStructure from "./menuStructure"; import menuStructure from "./menuStructure";
import ResponsiveDrawer from "./ResponsiveDrawer"; import ResponsiveDrawer from "./ResponsiveDrawer";
@ -51,9 +55,18 @@ const styles = (theme: Theme) =>
transition: theme.transitions.duration.standard + "ms" transition: theme.transitions.duration.standard + "ms"
}, },
content: { content: {
display: "flex", [theme.breakpoints.down("sm")]: {
flexDirection: "column", paddingLeft: 0
minHeight: `calc(100vh - ${appLoaderHeight}px)` },
paddingLeft: drawerWidthExpanded,
transition: "padding-left 0.5s ease",
width: "100%"
},
contentToggle: {
[theme.breakpoints.down("sm")]: {
paddingLeft: 0
},
paddingLeft: drawerWidth
}, },
darkThemeSwitch: { darkThemeSwitch: {
marginRight: theme.spacing.unit * 2 marginRight: theme.spacing.unit * 2
@ -67,20 +80,72 @@ const styles = (theme: Theme) =>
hide: { hide: {
opacity: 0 opacity: 0
}, },
isMenuSmall: {
"& path": {
fill: theme.palette.primary.main
},
"& span": {
margin: "0 8px"
},
"& svg": {
marginTop: 12,
transform: "rotate(180deg)"
},
"&:hover": {
background: "#E6F3F3"
},
background: theme.palette.background.paper,
border: `solid 1px #EAEAEA`,
borderRadius: "50%",
cursor: "pointer",
height: 32,
position: "absolute",
right: -16,
top: 65,
transition: `background ${theme.transitions.duration.shorter}ms`,
width: 32,
zIndex: 99
},
isMenuSmallDark: {
"&:hover": {
background: `linear-gradient(0deg, rgba(25, 195, 190, 0.1), rgba(25, 195, 190, 0.1)), ${
theme.palette.background.paper
}`
},
border: `solid 1px #252728`,
transition: `background ${theme.transitions.duration.shorter}ms`
},
isMenuSmallHide: {
"& svg": {
transform: "rotate(0deg)"
}
},
logo: { logo: {
"& svg": { "& svg": {
height: "100%" height: "100%",
margin: "20px 50px"
}, },
background: theme.palette.secondary.main,
display: "block", display: "block",
height: 28 height: 80
},
logoDark: {
"& path": {
fill: theme.palette.common.white
},
background: theme.palette.primary.main
},
logoSmall: {
"& svg": {
margin: "0px 25px"
}
}, },
menu: { menu: {
marginTop: theme.spacing.unit * 4 background: theme.palette.background.paper,
height: "100vh",
padding: 25
}, },
menuIcon: { menuIcon: {
[theme.breakpoints.up("md")]: {
display: "none"
},
"& span": { "& span": {
"&:nth-child(1)": { "&:nth-child(1)": {
top: 15 top: 15
@ -101,6 +166,9 @@ const styles = (theme: Theme) =>
transition: ".25s ease-in-out", transition: ".25s ease-in-out",
width: "60%" width: "60%"
}, },
[theme.breakpoints.up("md")]: {
display: "none"
},
background: theme.palette.background.paper, background: theme.palette.background.paper,
borderRadius: "50%", borderRadius: "50%",
cursor: "pointer", cursor: "pointer",
@ -109,7 +177,7 @@ const styles = (theme: Theme) =>
marginRight: theme.spacing.unit * 2, marginRight: theme.spacing.unit * 2,
position: "relative", position: "relative",
transform: "rotate(0deg)", transform: "rotate(0deg)",
transition: ".2s ease-in-out", transition: `${theme.transitions.duration.shorter}ms ease-in-out`,
width: 42 width: 42
}, },
menuIconDark: { menuIconDark: {
@ -135,15 +203,16 @@ const styles = (theme: Theme) =>
position: "absolute", position: "absolute",
zIndex: 1999 zIndex: 1999
}, },
menuSmall: {
background: theme.palette.background.paper,
height: "100vh",
padding: 25
},
popover: { popover: {
zIndex: 1 zIndex: 1
}, },
root: { root: {
[theme.breakpoints.down("sm")]: { width: `100%`
gridTemplateColumns: "1fr"
},
display: "grid",
gridTemplateColumns: `${drawerWidth}px 1fr`
}, },
rotate: { rotate: {
transform: "rotate(180deg)" transform: "rotate(180deg)"
@ -153,7 +222,7 @@ const styles = (theme: Theme) =>
padding: 0 padding: 0
}, },
background: theme.palette.background.paper, background: theme.palette.background.paper,
padding: `${theme.spacing.unit * 2}px ${theme.spacing.unit * 4}px` padding: `0 ${theme.spacing.unit * 4}px`
}, },
spacer: { spacer: {
flex: 1 flex: 1
@ -163,8 +232,8 @@ const styles = (theme: Theme) =>
display: "flex" display: "flex"
}, },
userChip: { userChip: {
backgroundColor: theme.palette.common.white, backgroundColor: theme.palette.background.paper,
border: `1px solid ${theme.palette.grey[200]}` color: theme.palette.text.primary
}, },
userMenuContainer: { userMenuContainer: {
position: "relative" position: "relative"
@ -191,7 +260,7 @@ interface AppLayoutProps {
const AppLayout = withStyles(styles, { const AppLayout = withStyles(styles, {
name: "AppLayout" name: "AppLayout"
})( })(
withRouter<AppLayoutProps & RouteComponentProps<any>>( withRouter<AppLayoutProps & RouteComponentProps<any>, any>(
({ ({
classes, classes,
children, children,
@ -200,6 +269,7 @@ const AppLayout = withStyles(styles, {
WithStyles<typeof styles> & WithStyles<typeof styles> &
RouteComponentProps<any>) => { RouteComponentProps<any>) => {
const { isDark, toggleTheme } = useTheme(); const { isDark, toggleTheme } = useTheme();
const [isMenuSmall, setMenuSmall] = useLocalStorage("isMenuSmall", false);
const [isDrawerOpened, setDrawerState] = React.useState(false); const [isDrawerOpened, setDrawerState] = React.useState(false);
const [isMenuOpened, setMenuState] = React.useState(false); const [isMenuOpened, setMenuState] = React.useState(false);
const appActionAnchor = React.useRef<HTMLDivElement>(); const appActionAnchor = React.useRef<HTMLDivElement>();
@ -223,6 +293,10 @@ const AppLayout = withStyles(styles, {
navigate(url); navigate(url);
}; };
const handleIsMenuSmall = () => {
setMenuSmall(!isMenuSmall);
};
return ( return (
<AppProgressProvider> <AppProgressProvider>
{({ isProgress }) => ( {({ isProgress }) => (
@ -239,14 +313,37 @@ const AppLayout = withStyles(styles, {
<ResponsiveDrawer <ResponsiveDrawer
onClose={() => setDrawerState(false)} onClose={() => setDrawerState(false)}
open={isDrawerOpened} open={isDrawerOpened}
small={!isMenuSmall}
> >
<SVG <div
className={classes.logo} className={classNames(classes.logo, {
src={isDark ? saleorDarkLogo : saleorLightLogo} [classes.logoSmall]: isMenuSmall,
/> [classes.logoDark]: isDark
})}
>
<SVG
src={
isMenuSmall ? saleorDarkLogoSmall : saleorDarkLogo
}
/>
</div>
<Hidden smDown>
<div
className={classNames(classes.isMenuSmall, {
[classes.isMenuSmallHide]: isMenuSmall,
[classes.isMenuSmallDark]: isDark
})}
onClick={handleIsMenuSmall}
>
<SVG src={menuArrowIcon} />
</div>
</Hidden>
<MenuList <MenuList
className={classes.menu} className={
isMenuSmall ? classes.menuSmall : classes.menu
}
menuItems={menuStructure} menuItems={menuStructure}
isMenuSmall={!isMenuSmall}
location={location.pathname} location={location.pathname}
user={user} user={user}
renderConfigure={true} renderConfigure={true}
@ -254,7 +351,11 @@ const AppLayout = withStyles(styles, {
/> />
</ResponsiveDrawer> </ResponsiveDrawer>
</div> </div>
<div className={classes.content}> <div
className={classNames(classes.content, {
[classes.contentToggle]: isMenuSmall
})}
>
<div> <div>
<Container> <Container>
<div className={classes.header}> <div className={classes.header}>
@ -283,6 +384,11 @@ const AppLayout = withStyles(styles, {
ref={anchor} ref={anchor}
> >
<Chip <Chip
avatar={
user.avatar && (
<Avatar alt="user" src={user.avatar.url} />
)
}
className={classes.userChip} className={classes.userChip}
label={ label={
<> <>

View file

@ -7,8 +7,11 @@ import {
import Typography from "@material-ui/core/Typography"; import Typography from "@material-ui/core/Typography";
import classNames from "classnames"; import classNames from "classnames";
import React from "react"; import React from "react";
import SVG from "react-inlinesvg";
import { matchPath } from "react-router"; import { matchPath } from "react-router";
import useTheme from "@saleor/hooks/useTheme";
import configureIcon from "@assets/images/menu-configure-icon.svg";
import { User } from "../../auth/types/User"; import { User } from "../../auth/types/User";
import { configurationMenu, configurationMenuUrl } from "../../configuration"; import { configurationMenu, configurationMenuUrl } from "../../configuration";
import i18n from "../../i18n"; import i18n from "../../i18n";
@ -19,6 +22,44 @@ import { IMenuItem } from "./menuStructure";
const styles = (theme: Theme) => const styles = (theme: Theme) =>
createStyles({ createStyles({
menuIcon: {
"& svg": {
height: 32,
width: 32
},
display: "inline-block",
position: "relative",
top: 8
},
menuIconDark: {
"& path": {
fill: theme.palette.common.white
}
},
menuIsActive: {
boxShadow: "0px 0px 12px 1px rgba(0,0,0,0.2)"
},
menuItemHover: {
"& path": {
transition: "fill 0.5s ease"
},
"&:hover": {
"& path": {
fill: theme.palette.primary.main
},
"&:before": {
borderLeft: `solid 2px ${theme.palette.primary.main}`,
content: "''",
height: 33,
left: -25,
position: "absolute",
top: 8
},
color: theme.palette.primary.main
},
cursor: "pointer",
position: "relative"
},
menuList: { menuList: {
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
@ -28,24 +69,33 @@ const styles = (theme: Theme) =>
paddingBottom: theme.spacing.unit * 3 paddingBottom: theme.spacing.unit * 3
}, },
menuListItem: { menuListItem: {
"&:hover": {
color: theme.palette.primary.main
},
alignItems: "center", alignItems: "center",
display: "block", display: "block",
marginTop: theme.spacing.unit * 2, marginBottom: theme.spacing.unit * 5,
paddingLeft: 0, paddingLeft: 0,
textDecoration: "none", textDecoration: "none",
transition: theme.transitions.duration.standard + "ms" transition: theme.transitions.duration.standard + "ms"
}, },
menuListItemActive: { menuListItemActive: {
"&:before": { "& $menuListItemText": {
background: theme.palette.primary.main, color: theme.palette.primary.main
},
"& path": {
color: theme.palette.primary.main,
fill: theme.palette.primary.main
}
},
menuListItemOpen: {
"&:after": {
borderBottom: `10px solid transparent`,
borderLeft: `10px solid ${theme.palette.background.paper}`,
borderTop: `10px solid transparent`,
content: "''", content: "''",
height: "100%", height: 0,
left: -32,
position: "absolute", position: "absolute",
width: 5 right: -35,
top: 15,
width: 0
}, },
position: "relative" position: "relative"
}, },
@ -54,123 +104,231 @@ const styles = (theme: Theme) =>
color: theme.palette.primary.main color: theme.palette.primary.main
}, },
cursor: "pointer", cursor: "pointer",
display: "inline-block",
fontSize: "1rem", fontSize: "1rem",
fontWeight: 500, fontWeight: 500,
opacity: 1,
paddingLeft: 16,
textTransform: "uppercase", textTransform: "uppercase",
transition: theme.transitions.duration.standard + "ms" transition: `opacity ${theme.transitions.duration.shorter}ms ease 0.1s`
}, },
menuListNested: { menuListItemTextHide: {
"& $menuListItemActive": { opacity: 0,
"& $menuListItemText": { position: "absolute",
color: theme.palette.primary.main transition: `opacity ${theme.transitions.duration.shorter}ms ease`
}, },
"&:before": { subMenu: {
borderRadius: "100%", padding: "0 15px"
height: 8, },
marginLeft: 9, subMenuDrawer: {
marginTop: 7, background: "#000",
width: 8 cursor: "pointer",
} height: "100vh",
}, left: 0,
"& $menuListItemText": { opacity: 0.2,
textTransform: "none" position: "absolute",
}, top: 0,
marginLeft: theme.spacing.unit * 3 width: 0,
zIndex: -2
},
subMenuDrawerOpen: {
width: `100vw`
} }
}); });
interface MenuListProps { interface MenuListProps {
className?: string; className?: string;
menuItems: IMenuItem[]; menuItems: IMenuItem[];
isMenuSmall: boolean;
location: string; location: string;
user: User; user: User;
renderConfigure: boolean; renderConfigure: boolean;
onMenuItemClick: (url: string, event: React.MouseEvent<any>) => void; onMenuItemClick: (url: string, event: React.MouseEvent<any>) => void;
} }
export interface IActiveSubMenu {
isActive: boolean;
label: string | null;
}
const MenuList = withStyles(styles, { name: "MenuList" })( const MenuList = withStyles(styles, { name: "MenuList" })(
({ ({
classes, classes,
className, className,
menuItems, menuItems,
isMenuSmall,
location, location,
user, user,
renderConfigure, renderConfigure,
onMenuItemClick onMenuItemClick
}: MenuListProps & WithStyles<typeof styles>) => ( }: MenuListProps & WithStyles<typeof styles>) => {
<div className={className}> const { isDark } = useTheme();
{/* FIXME: this .split("?")[0] looks gross */} const [activeSubMenu, setActiveSubMenu] = React.useState<IActiveSubMenu>({
{menuItems.map(menuItem => { isActive: false,
const isActive = (menuItem: IMenuItem) => label: null
location.split("?")[0] === orderDraftListUrl().split("?")[0] && });
menuItem.url.split("?")[0] === orderListUrl().split("?")[0]
? false
: !!matchPath(location.split("?")[0], {
exact: menuItem.url.split("?")[0] === "/",
path: menuItem.url.split("?")[0]
});
if ( const handleSubMenu = itemLabel => {
menuItem.permission && setActiveSubMenu({
!user.permissions.map(perm => perm.code).includes(menuItem.permission) isActive:
) { itemLabel === activeSubMenu.label ? !activeSubMenu.isActive : true,
return null; label: itemLabel
} });
};
if (!menuItem.url) { const closeSubMenu = (menuItemUrl, event) => {
const isAnyChildActive = menuItem.children.reduce( setActiveSubMenu({
(acc, child) => acc || isActive(child), isActive: false,
false label: null
); });
if (menuItemUrl && event) {
onMenuItemClick(menuItemUrl, event);
event.stopPropagation();
event.preventDefault();
}
};
return (
<div
className={classNames(className, {
[classes.menuIsActive]: activeSubMenu.isActive
})}
>
{/* FIXME: this .split("?")[0] looks gross */}
{menuItems.map(menuItem => {
const isActive = (menuItem: IMenuItem) =>
location.split("?")[0] === orderDraftListUrl().split("?")[0] &&
menuItem.url.split("?")[0] === orderListUrl().split("?")[0]
? false
: !!matchPath(location.split("?")[0], {
exact: menuItem.url.split("?")[0] === "/",
path: menuItem.url.split("?")[0]
});
if (
menuItem.permission &&
!user.permissions
.map(perm => perm.code)
.includes(menuItem.permission)
) {
return null;
}
if (!menuItem.url) {
const isAnyChildActive = menuItem.children.reduce(
(acc, child) => acc || isActive(child),
false
);
return (
<div
className={classNames(classes.menuListItem, {
[classes.menuListItemActive]: isAnyChildActive
})}
key={menuItem.label}
>
<div
className={classNames(classes.menuItemHover, {
[classes.menuListItemOpen]:
menuItem.ariaLabel === activeSubMenu.label &&
activeSubMenu.isActive
})}
onClick={() => handleSubMenu(menuItem.ariaLabel)}
>
<SVG
className={classNames(classes.menuIcon, {
[classes.menuIconDark]: isDark
})}
src={menuItem.icon}
/>
<Typography
aria-label={menuItem.ariaLabel}
className={classNames(classes.menuListItemText, {
[classes.menuListItemTextHide]: !isMenuSmall
})}
>
{menuItem.label}
</Typography>
</div>
<MenuNested
activeItem={activeSubMenu}
closeSubMenu={setActiveSubMenu}
menuItem={menuItem}
onMenuItemClick={onMenuItemClick}
handleSubMenu={handleSubMenu}
title={menuItem.label}
icon={menuItem.icon}
ariaLabel={menuItem.ariaLabel}
/>
<div
onClick={event => closeSubMenu(null, event)}
className={classNames(classes.subMenuDrawer, {
[classes.subMenuDrawerOpen]: activeSubMenu.isActive
})}
/>
</div>
);
}
return ( return (
<MenuNested <a
classes={classes} className={classNames(classes.menuListItem, {
isAnyChildActive={isAnyChildActive} [classes.menuListItemActive]: isActive(menuItem)
location={location} })}
menuItem={menuItem} href={createHref(menuItem.url)}
onMenuItemClick={onMenuItemClick} onClick={event => closeSubMenu(menuItem.url, event)}
user={user}
key={menuItem.label} key={menuItem.label}
/> >
<div className={classes.menuItemHover}>
<SVG
className={classNames(classes.menuIcon, {
[classes.menuIconDark]: isDark
})}
src={menuItem.icon}
/>
<Typography
aria-label={menuItem.ariaLabel}
className={classNames(classes.menuListItemText, {
[classes.menuListItemTextHide]: !isMenuSmall
})}
>
{menuItem.label}
</Typography>
</div>
</a>
); );
} })}
{renderConfigure &&
return ( configurationMenu.filter(menuItem =>
<a user.permissions
className={classNames(classes.menuListItem, { .map(perm => perm.code)
[classes.menuListItemActive]: isActive(menuItem) .includes(menuItem.permission)
})} ).length > 0 && (
href={createHref(menuItem.url)} <a
onClick={event => onMenuItemClick(menuItem.url, event)} className={classes.menuListItem}
key={menuItem.label} href={createHref(configurationMenuUrl)}
> onClick={event => onMenuItemClick(configurationMenuUrl, event)}
<Typography
aria-label={menuItem.ariaLabel}
className={classes.menuListItemText}
> >
{menuItem.label} <div className={classes.menuItemHover}>
</Typography> <SVG
</a> className={classNames(classes.menuIcon, {
); [classes.menuIconDark]: isDark
})} })}
{renderConfigure && src={configureIcon}
configurationMenu.filter(menuItem => />
user.permissions.map(perm => perm.code).includes(menuItem.permission) <Typography
).length > 0 && ( aria-label="configure"
<a className={classNames(classes.menuListItemText, {
className={classes.menuListItem} [classes.menuListItemTextHide]: !isMenuSmall
href={createHref(configurationMenuUrl)} })}
onClick={event => onMenuItemClick(configurationMenuUrl, event)} >
> {i18n.t("Configure")}
<Typography </Typography>
aria-label="configure" </div>
className={classes.menuListItemText} </a>
> )}
{i18n.t("Configure")} </div>
</Typography> );
</a> }
)}
</div>
)
); );
export default MenuList; export default MenuList;

View file

@ -1,61 +1,195 @@
import Hidden from "@material-ui/core/Hidden";
import {
createStyles,
Theme,
withStyles,
WithStyles
} from "@material-ui/core/styles";
import Typography from "@material-ui/core/Typography"; import Typography from "@material-ui/core/Typography";
import classNames from "classnames"; import classNames from "classnames";
import React from "react"; import React from "react";
import SVG from "react-inlinesvg";
import { User } from "../../auth/types/User"; import menuArrowIcon from "@assets/images/menu-arrow-icon.svg";
import MenuList from "./MenuList"; import useTheme from "@saleor/hooks/useTheme";
import { createHref } from "@saleor/misc";
import { drawerWidthExpanded } from "./consts";
import { IActiveSubMenu } from "./MenuList";
import { IMenuItem } from "./menuStructure"; import { IMenuItem } from "./menuStructure";
const styles = (theme: Theme) =>
createStyles({
menuListNested: {
background: theme.palette.background.paper,
height: "100vh",
position: "absolute",
right: 0,
top: 0,
transition: `right ${theme.transitions.duration.shorter}ms ease`,
width: 300,
zIndex: -1
},
menuListNestedClose: {
"& svg": {
fill: theme.palette.primary.main,
left: 7,
position: "relative",
top: -2
},
border: `solid 1px #EAEAEA`,
borderRadius: "100%",
cursor: "pointer",
height: 32,
position: "absolute",
right: 32,
top: 35,
transform: "rotate(180deg)",
width: 32
},
menuListNestedCloseDark: {
border: `solid 1px #252728`
},
menuListNestedHide: {
opacity: 0
},
menuListNestedIcon: {
"& path": {
fill: "initial"
},
"& svg": { height: 32, position: "relative", top: 7, width: 32 }
},
menuListNestedIconDark: {
"& path": {
fill: theme.palette.common.white
}
},
menuListNestedItem: {
"&:hover": {
"& p": {
color: theme.palette.primary.main
}
},
display: "block",
marginBottom: theme.spacing.unit * 2,
padding: "0px 30px",
textDecoration: "none"
},
menuListNestedOpen: {
[theme.breakpoints.down("sm")]: {
right: 0,
width: drawerWidthExpanded,
zIndex: 2
},
right: -300,
zIndex: -1
},
subHeader: {
borderBottom: "solid 1px #EAEAEA",
margin: "30px",
marginBottom: 39,
paddingBottom: 22
},
subHeaderDark: {
borderBottom: "solid 1px #252728"
},
subHeaderTitle: {
[theme.breakpoints.up("md")]: {
paddingLeft: 0
},
display: "inline",
paddingLeft: 10
}
});
export interface MenuNestedProps { export interface MenuNestedProps {
classes: Record< activeItem: IActiveSubMenu;
| "menuListItem" ariaLabel: string;
| "menuListItemActive" closeSubMenu: ({ isActive, label }: IActiveSubMenu) => void;
| "menuListItemText" icon: string;
| "menuListNested",
string
>;
isAnyChildActive: boolean;
menuItem: IMenuItem; menuItem: IMenuItem;
location: string; title: string;
user: User; handleSubMenu: (itemLabel: string) => void;
onMenuItemClick: (url: string, event: React.MouseEvent<any>) => void; onMenuItemClick: (url: string, event: React.MouseEvent<any>) => void;
} }
const MenuNested: React.FC<MenuNestedProps> = ({ const MenuNested = withStyles(styles, { name: "MenuNested" })(
classes, ({
isAnyChildActive, activeItem,
location, ariaLabel,
menuItem, classes,
onMenuItemClick, closeSubMenu,
user icon,
}) => { menuItem,
const [isOpened, setOpenStatus] = React.useState(false); onMenuItemClick,
title
return ( }: MenuNestedProps & WithStyles<typeof styles>) => {
<div const menuItems = menuItem.children;
onClick={() => setOpenStatus(!isOpened)} const { isDark } = useTheme();
className={classNames(classes.menuListItem, { const closeMenu = (menuItemUrl, event) => {
[classes.menuListItemActive]: isAnyChildActive onMenuItemClick(menuItemUrl, event);
})} closeSubMenu({
> isActive: false,
<Typography label: null
aria-label={menuItem.ariaLabel} });
className={classes.menuListItemText} event.stopPropagation();
> event.preventDefault();
{menuItem.label} };
</Typography> return (
{isOpened && ( <>
<div className={classes.menuListNested}> <div
<MenuList className={classNames(classes.menuListNested, {
menuItems={menuItem.children} [classes.menuListNestedOpen]:
location={location} activeItem.label === ariaLabel && activeItem.isActive
user={user} })}
renderConfigure={false} >
onMenuItemClick={onMenuItemClick} <Typography
/> className={classNames(classes.subHeader, {
[classes.subHeaderDark]: isDark
})}
variant="h5"
>
<Hidden mdUp>
<SVG
className={classNames(classes.menuListNestedIcon, {
[classes.menuListNestedIconDark]: isDark
})}
src={icon}
/>
</Hidden>
<div className={classes.subHeaderTitle}>{title}</div>
<Hidden mdUp>
<div
className={classNames(classes.menuListNestedClose, {
[classes.menuListNestedCloseDark]: isDark
})}
onClick={() =>
closeSubMenu({
isActive: false,
label: null
})
}
>
<SVG src={menuArrowIcon} />
</div>
</Hidden>
</Typography>
{menuItems.map(item => {
return (
<a
className={classNames(classes.menuListNestedItem)}
href={createHref(item.url)}
onClick={event => closeMenu(item.url, event)}
key={item.label}
>
<Typography aria-label={item.ariaLabel}>
{item.label}
</Typography>
</a>
);
})}
</div> </div>
)} </>
</div> );
); }
}; );
export default MenuNested; export default MenuNested;

View file

@ -7,7 +7,7 @@ import {
WithStyles WithStyles
} from "@material-ui/core/styles"; } from "@material-ui/core/styles";
import React from "react"; import React from "react";
import { drawerWidth } from "./consts"; import { drawerWidth, drawerWidthExpanded } from "./consts";
const styles = (theme: Theme) => const styles = (theme: Theme) =>
createStyles({ createStyles({
@ -15,31 +15,38 @@ const styles = (theme: Theme) =>
backgroundColor: theme.palette.background.paper, backgroundColor: theme.palette.background.paper,
border: "none", border: "none",
height: "100vh", height: "100vh",
padding: `${theme.spacing.unit * 2}px ${theme.spacing.unit * 4}px`, overflow: "visible",
padding: 0,
position: "fixed" as "fixed", position: "fixed" as "fixed",
transition: "width 0.2s ease",
width: drawerWidthExpanded
},
drawerDesktopSmall: {
overflow: "visible",
transition: "width 0.2s ease",
width: drawerWidth width: drawerWidth
}, },
drawerMobile: { drawerMobile: {
padding: `${theme.spacing.unit * 2}px ${theme.spacing.unit * 4}px`, width: drawerWidthExpanded
width: drawerWidth,
} }
}); });
interface ResponsiveDrawerProps extends WithStyles<typeof styles> { interface ResponsiveDrawerProps extends WithStyles<typeof styles> {
children?: React.ReactNode; children?: React.ReactNode;
open: boolean; open: boolean;
small: boolean;
onClose?(); onClose?();
} }
const ResponsiveDrawer = withStyles(styles, { name: "ResponsiveDrawer" })( const ResponsiveDrawer = withStyles(styles, { name: "ResponsiveDrawer" })(
({ children, classes, onClose, open }: ResponsiveDrawerProps) => ( ({ children, classes, onClose, open, small }: ResponsiveDrawerProps) => (
<> <>
<Hidden smDown> <Hidden smDown>
<Drawer <Drawer
variant="persistent" variant="persistent"
open open
classes={{ classes={{
paper: classes.drawerDesktop paper: small ? classes.drawerDesktop : classes.drawerDesktopSmall
}} }}
> >
{children} {children}

View file

@ -1,3 +1,4 @@
export const drawerWidth = 256; export const drawerWidthExpanded = 256;
export const drawerWidth = 80;
export const navigationBarHeight = 64; export const navigationBarHeight = 64;
export const appLoaderHeight = 4; export const appLoaderHeight = 4;

View file

@ -8,9 +8,17 @@ import { productListUrl } from "../../products/urls";
import { languageListUrl } from "../../translations/urls"; import { languageListUrl } from "../../translations/urls";
import { PermissionEnum } from "../../types/globalTypes"; import { PermissionEnum } from "../../types/globalTypes";
import catalogIcon from "@assets/images/menu-catalog-icon.svg";
import customerIcon from "@assets/images/menu-customers-icon.svg";
import discountsIcon from "@assets/images/menu-discounts-icon.svg";
import homeIcon from "@assets/images/menu-home-icon.svg";
import ordersIcon from "@assets/images/menu-orders-icon.svg";
import translationIcon from "@assets/images/menu-translation-icon.svg";
export interface IMenuItem { export interface IMenuItem {
ariaLabel: string; ariaLabel: string;
children?: IMenuItem[]; children?: IMenuItem[];
icon?: any;
label: string; label: string;
permission?: PermissionEnum; permission?: PermissionEnum;
url?: string; url?: string;
@ -19,6 +27,7 @@ export interface IMenuItem {
const menuStructure: IMenuItem[] = [ const menuStructure: IMenuItem[] = [
{ {
ariaLabel: "home", ariaLabel: "home",
icon: homeIcon,
label: i18n.t("Home", { context: "Menu label" }), label: i18n.t("Home", { context: "Menu label" }),
url: "/" url: "/"
}, },
@ -41,6 +50,7 @@ const menuStructure: IMenuItem[] = [
url: collectionListUrl() url: collectionListUrl()
} }
], ],
icon: catalogIcon,
label: i18n.t("Catalog", { context: "Menu label" }), label: i18n.t("Catalog", { context: "Menu label" }),
permission: PermissionEnum.MANAGE_PRODUCTS permission: PermissionEnum.MANAGE_PRODUCTS
}, },
@ -60,11 +70,13 @@ const menuStructure: IMenuItem[] = [
url: orderDraftListUrl() url: orderDraftListUrl()
} }
], ],
icon: ordersIcon,
label: i18n.t("Orders", { context: "Menu label" }), label: i18n.t("Orders", { context: "Menu label" }),
permission: PermissionEnum.MANAGE_ORDERS permission: PermissionEnum.MANAGE_ORDERS
}, },
{ {
ariaLabel: "customers", ariaLabel: "customers",
icon: customerIcon,
label: i18n.t("Customers", { context: "Menu label" }), label: i18n.t("Customers", { context: "Menu label" }),
permission: PermissionEnum.MANAGE_USERS, permission: PermissionEnum.MANAGE_USERS,
url: customerListUrl() url: customerListUrl()
@ -84,11 +96,13 @@ const menuStructure: IMenuItem[] = [
url: voucherListUrl() url: voucherListUrl()
} }
], ],
icon: discountsIcon,
label: i18n.t("Discounts", { context: "Menu label" }), label: i18n.t("Discounts", { context: "Menu label" }),
permission: PermissionEnum.MANAGE_DISCOUNTS permission: PermissionEnum.MANAGE_DISCOUNTS
}, },
{ {
ariaLabel: "translations", ariaLabel: "translations",
icon: translationIcon,
label: i18n.t("Translations", { context: "Menu label" }), label: i18n.t("Translations", { context: "Menu label" }),
permission: PermissionEnum.MANAGE_TRANSLATIONS, permission: PermissionEnum.MANAGE_TRANSLATIONS,
url: languageListUrl url: languageListUrl

View file

@ -15,8 +15,8 @@ import React from "react";
import ConfirmButton, { import ConfirmButton, {
ConfirmButtonTransitionState ConfirmButtonTransitionState
} from "@saleor/components/ConfirmButton"; } from "@saleor/components/ConfirmButton";
import Form from "@saleor/components/Form";
import FormSpacer from "@saleor/components/FormSpacer"; import FormSpacer from "@saleor/components/FormSpacer";
import useSearchQuery from "@saleor/hooks/useSearchQuery";
import { SearchCategories_categories_edges_node } from "../../containers/SearchCategories/types/SearchCategories"; import { SearchCategories_categories_edges_node } from "../../containers/SearchCategories/types/SearchCategories";
import i18n from "../../i18n"; import i18n from "../../i18n";
import Checkbox from "../Checkbox"; import Checkbox from "../Checkbox";
@ -50,13 +50,28 @@ interface AssignCategoriesDialogProps extends WithStyles<typeof styles> {
loading: boolean; loading: boolean;
onClose: () => void; onClose: () => void;
onFetch: (value: string) => void; onFetch: (value: string) => void;
onSubmit: (data: FormData) => void; onSubmit: (data: SearchCategories_categories_edges_node[]) => void;
}
function handleCategoryAssign(
product: SearchCategories_categories_edges_node,
isSelected: boolean,
selectedCategories: SearchCategories_categories_edges_node[],
setSelectedCategories: (
data: SearchCategories_categories_edges_node[]
) => void
) {
if (isSelected) {
setSelectedCategories(
selectedCategories.filter(
selectedProduct => selectedProduct.id !== product.id
)
);
} else {
setSelectedCategories([...selectedCategories, product]);
}
} }
const initialForm: FormData = {
categories: [],
query: ""
};
const AssignCategoriesDialog = withStyles(styles, { const AssignCategoriesDialog = withStyles(styles, {
name: "AssignCategoriesDialog" name: "AssignCategoriesDialog"
})( })(
@ -69,104 +84,93 @@ const AssignCategoriesDialog = withStyles(styles, {
onClose, onClose,
onFetch, onFetch,
onSubmit onSubmit
}: AssignCategoriesDialogProps) => ( }: AssignCategoriesDialogProps) => {
<Dialog const [query, onQueryChange] = useSearchQuery(onFetch);
open={open} const [selectedCategories, setSelectedCategories] = React.useState<
onClose={onClose} SearchCategories_categories_edges_node[]
classes={{ paper: classes.overflow }} >([]);
fullWidth
maxWidth="sm"
>
<Form initial={initialForm} onSubmit={onSubmit}>
{({ data, change }) => (
<>
<DialogTitle>{i18n.t("Assign Categories")}</DialogTitle>
<DialogContent className={classes.overflow}>
<TextField
name="query"
value={data.query}
onChange={event => change(event, () => onFetch(data.query))}
label={i18n.t("Search Categories", {
context: "product search input label"
})}
placeholder={i18n.t(
"Search by product name, attribute, product type etc...",
{
context: "product search input placeholder"
}
)}
fullWidth
InputProps={{
autoComplete: "off",
endAdornment: loading && <CircularProgress size={16} />
}}
/>
<FormSpacer />
<Table>
<TableBody>
{categories &&
categories.map(category => {
const isChecked = !!data.categories.find(
selectedCategories =>
selectedCategories.id === category.id
);
return ( const handleSubmit = () => onSubmit(selectedCategories);
<TableRow key={category.id}>
<TableCell return (
padding="checkbox" <Dialog
className={classes.checkboxCell} open={open}
> onClose={onClose}
<Checkbox classes={{ paper: classes.overflow }}
checked={isChecked} fullWidth
onChange={() => maxWidth="sm"
isChecked >
? change({ <DialogTitle>{i18n.t("Assign Categories")}</DialogTitle>
target: { <DialogContent className={classes.overflow}>
name: "categories", <TextField
value: data.categories.filter( name="query"
selectedCategories => value={query}
selectedCategories.id !== onChange={onQueryChange}
category.id label={i18n.t("Search Categories", {
) context: "category search input label"
} })}
} as any) placeholder={i18n.t("Search by category name, etc...", {
: change({ context: "category search input placeholder"
target: { })}
name: "categories", fullWidth
value: [...data.categories, category] InputProps={{
} autoComplete: "off",
} as any) endAdornment: loading && <CircularProgress size={16} />
} }}
/> />
</TableCell> <FormSpacer />
<TableCell className={classes.wideCell}> <Table>
{category.name} <TableBody>
</TableCell> {categories &&
</TableRow> categories.map(category => {
); const isSelected = !!selectedCategories.find(
})} selectedCategories => selectedCategories.id === category.id
</TableBody> );
</Table>
</DialogContent> return (
<DialogActions> <TableRow key={category.id}>
<Button onClick={onClose}> <TableCell
{i18n.t("Cancel", { context: "button" })} padding="checkbox"
</Button> className={classes.checkboxCell}
<ConfirmButton >
transitionState={confirmButtonState} <Checkbox
color="primary" checked={isSelected}
variant="contained" onChange={() =>
type="submit" handleCategoryAssign(
> category,
{i18n.t("Assign categories", { context: "button" })} isSelected,
</ConfirmButton> selectedCategories,
</DialogActions> setSelectedCategories
</> )
)} }
</Form> />
</Dialog> </TableCell>
) <TableCell className={classes.wideCell}>
{category.name}
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</DialogContent>
<DialogActions>
<Button onClick={onClose}>
{i18n.t("Cancel", { context: "button" })}
</Button>
<ConfirmButton
transitionState={confirmButtonState}
color="primary"
variant="contained"
type="submit"
onClick={handleSubmit}
>
{i18n.t("Assign categories", { context: "button" })}
</ConfirmButton>
</DialogActions>
</Dialog>
);
}
); );
AssignCategoriesDialog.displayName = "AssignCategoriesDialog"; AssignCategoriesDialog.displayName = "AssignCategoriesDialog";
export default AssignCategoriesDialog; export default AssignCategoriesDialog;

View file

@ -12,13 +12,13 @@ import TableRow from "@material-ui/core/TableRow";
import TextField from "@material-ui/core/TextField"; import TextField from "@material-ui/core/TextField";
import React from "react"; import React from "react";
import useSearchQuery from "@saleor/hooks/useSearchQuery";
import i18n from "@saleor/i18n";
import { SearchCollections_collections_edges_node } from "../../containers/SearchCollections/types/SearchCollections"; import { SearchCollections_collections_edges_node } from "../../containers/SearchCollections/types/SearchCollections";
import i18n from "../../i18n";
import Checkbox from "../Checkbox"; import Checkbox from "../Checkbox";
import ConfirmButton, { import ConfirmButton, {
ConfirmButtonTransitionState ConfirmButtonTransitionState
} from "../ConfirmButton/ConfirmButton"; } from "../ConfirmButton/ConfirmButton";
import Form from "../Form";
import FormSpacer from "../FormSpacer"; import FormSpacer from "../FormSpacer";
export interface FormData { export interface FormData {
@ -50,13 +50,28 @@ interface AssignCollectionDialogProps extends WithStyles<typeof styles> {
loading: boolean; loading: boolean;
onClose: () => void; onClose: () => void;
onFetch: (value: string) => void; onFetch: (value: string) => void;
onSubmit: (data: FormData) => void; onSubmit: (data: SearchCollections_collections_edges_node[]) => void;
}
function handleCollectionAssign(
product: SearchCollections_collections_edges_node,
isSelected: boolean,
selectedCollections: SearchCollections_collections_edges_node[],
setSelectedCollections: (
data: SearchCollections_collections_edges_node[]
) => void
) {
if (isSelected) {
setSelectedCollections(
selectedCollections.filter(
selectedProduct => selectedProduct.id !== product.id
)
);
} else {
setSelectedCollections([...selectedCollections, product]);
}
} }
const initialForm: FormData = {
collections: [],
query: ""
};
const AssignCollectionDialog = withStyles(styles, { const AssignCollectionDialog = withStyles(styles, {
name: "AssignCollectionDialog" name: "AssignCollectionDialog"
})( })(
@ -69,104 +84,94 @@ const AssignCollectionDialog = withStyles(styles, {
onClose, onClose,
onFetch, onFetch,
onSubmit onSubmit
}: AssignCollectionDialogProps) => ( }: AssignCollectionDialogProps) => {
<Dialog const [query, onQueryChange] = useSearchQuery(onFetch);
onClose={onClose} const [selectedCollections, setSelectedCollections] = React.useState<
open={open} SearchCollections_collections_edges_node[]
classes={{ paper: classes.overflow }} >([]);
fullWidth
maxWidth="sm"
>
<Form initial={initialForm} onSubmit={onSubmit}>
{({ data, change }) => (
<>
<DialogTitle>{i18n.t("Assign Collection")}</DialogTitle>
<DialogContent className={classes.overflow}>
<TextField
name="query"
value={data.query}
onChange={event => change(event, () => onFetch(data.query))}
label={i18n.t("Search Collection", {
context: "product search input label"
})}
placeholder={i18n.t(
"Search by product name, attribute, product type etc...",
{
context: "product search input placeholder"
}
)}
fullWidth
InputProps={{
autoComplete: "off",
endAdornment: loading && <CircularProgress size={16} />
}}
/>
<FormSpacer />
<Table>
<TableBody>
{collections &&
collections.map(category => {
const isChecked = !!data.collections.find(
selectedCollection =>
selectedCollection.id === category.id
);
return ( const handleSubmit = () => onSubmit(selectedCollections);
<TableRow key={category.id}>
<TableCell return (
padding="checkbox" <Dialog
className={classes.checkboxCell} onClose={onClose}
> open={open}
<Checkbox classes={{ paper: classes.overflow }}
checked={isChecked} fullWidth
onChange={() => maxWidth="sm"
isChecked >
? change({ <DialogTitle>{i18n.t("Assign Collection")}</DialogTitle>
target: { <DialogContent className={classes.overflow}>
name: "collections", <TextField
value: data.collections.filter( name="query"
selectedCollection => value={query}
selectedCollection.id !== onChange={onQueryChange}
category.id label={i18n.t("Search Collection", {
) context: "product search input label"
} })}
} as any) placeholder={i18n.t("Search by collection name, etc...", {
: change({ context: "product search input placeholder"
target: { })}
name: "collections", fullWidth
value: [...data.collections, category] InputProps={{
} autoComplete: "off",
} as any) endAdornment: loading && <CircularProgress size={16} />
} }}
/> />
</TableCell> <FormSpacer />
<TableCell className={classes.wideCell}> <Table>
{category.name} <TableBody>
</TableCell> {collections &&
</TableRow> collections.map(collection => {
); const isSelected = !!selectedCollections.find(
})} selectedCollection =>
</TableBody> selectedCollection.id === collection.id
</Table> );
</DialogContent>
<DialogActions> return (
<Button onClick={onClose}> <TableRow key={collection.id}>
{i18n.t("Cancel", { context: "button" })} <TableCell
</Button> padding="checkbox"
<ConfirmButton className={classes.checkboxCell}
transitionState={confirmButtonState} >
color="primary" <Checkbox
variant="contained" checked={isSelected}
type="submit" onChange={() =>
> handleCollectionAssign(
{i18n.t("Assign collections", { context: "button" })} collection,
</ConfirmButton> isSelected,
</DialogActions> selectedCollections,
</> setSelectedCollections
)} )
</Form> }
</Dialog> />
) </TableCell>
<TableCell className={classes.wideCell}>
{collection.name}
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</DialogContent>
<DialogActions>
<Button onClick={onClose}>
{i18n.t("Cancel", { context: "button" })}
</Button>
<ConfirmButton
transitionState={confirmButtonState}
color="primary"
variant="contained"
type="submit"
onClick={handleSubmit}
>
{i18n.t("Assign collections", { context: "button" })}
</ConfirmButton>
</DialogActions>
</Dialog>
);
}
); );
AssignCollectionDialog.displayName = "AssignCollectionDialog"; AssignCollectionDialog.displayName = "AssignCollectionDialog";
export default AssignCollectionDialog; export default AssignCollectionDialog;

View file

@ -15,12 +15,12 @@ import React from "react";
import ConfirmButton, { import ConfirmButton, {
ConfirmButtonTransitionState ConfirmButtonTransitionState
} from "@saleor/components/ConfirmButton"; } from "@saleor/components/ConfirmButton";
import Form from "@saleor/components/Form";
import FormSpacer from "@saleor/components/FormSpacer"; import FormSpacer from "@saleor/components/FormSpacer";
import TableCellAvatar from "@saleor/components/TableCellAvatar"; import TableCellAvatar from "@saleor/components/TableCellAvatar";
import useSearchQuery from "@saleor/hooks/useSearchQuery";
import i18n from "@saleor/i18n";
import { maybe } from "@saleor/misc";
import { SearchProducts_products_edges_node } from "../../containers/SearchProducts/types/SearchProducts"; import { SearchProducts_products_edges_node } from "../../containers/SearchProducts/types/SearchProducts";
import i18n from "../../i18n";
import { maybe } from "../../misc";
import Checkbox from "../Checkbox"; import Checkbox from "../Checkbox";
export interface FormData { export interface FormData {
@ -40,25 +40,41 @@ const styles = createStyles({
overflow: { overflow: {
overflowY: "visible" overflowY: "visible"
}, },
scrollArea: {
overflowY: "scroll"
},
wideCell: { wideCell: {
width: "100%" width: "100%"
} }
}); });
interface AssignProductDialogProps extends WithStyles<typeof styles> { export interface AssignProductDialogProps {
confirmButtonState: ConfirmButtonTransitionState; confirmButtonState: ConfirmButtonTransitionState;
open: boolean; open: boolean;
products: SearchProducts_products_edges_node[]; products: SearchProducts_products_edges_node[];
loading: boolean; loading: boolean;
onClose: () => void; onClose: () => void;
onFetch: (value: string) => void; onFetch: (value: string) => void;
onSubmit: (data: FormData) => void; onSubmit: (data: SearchProducts_products_edges_node[]) => void;
}
function handleProductAssign(
product: SearchProducts_products_edges_node,
isSelected: boolean,
selectedProducts: SearchProducts_products_edges_node[],
setSelectedProducts: (data: SearchProducts_products_edges_node[]) => void
) {
if (isSelected) {
setSelectedProducts(
selectedProducts.filter(
selectedProduct => selectedProduct.id !== product.id
)
);
} else {
setSelectedProducts([...selectedProducts, product]);
}
} }
const initialForm: FormData = {
products: [],
query: ""
};
const AssignProductDialog = withStyles(styles, { const AssignProductDialog = withStyles(styles, {
name: "AssignProductDialog" name: "AssignProductDialog"
})( })(
@ -71,106 +87,102 @@ const AssignProductDialog = withStyles(styles, {
onClose, onClose,
onFetch, onFetch,
onSubmit onSubmit
}: AssignProductDialogProps) => ( }: AssignProductDialogProps & WithStyles<typeof styles>) => {
<Dialog const [query, onQueryChange] = useSearchQuery(onFetch);
onClose={onClose} const [selectedProducts, setSelectedProducts] = React.useState<
open={open} SearchProducts_products_edges_node[]
classes={{ paper: classes.overflow }} >([]);
fullWidth
maxWidth="sm"
>
<Form initial={initialForm} onSubmit={onSubmit}>
{({ data, change }) => (
<>
<DialogTitle>{i18n.t("Assign Product")}</DialogTitle>
<DialogContent className={classes.overflow}>
<TextField
name="query"
value={data.query}
onChange={event => change(event, () => onFetch(data.query))}
label={i18n.t("Search Products", {
context: "product search input label"
})}
placeholder={i18n.t(
"Search by product name, attribute, product type etc...",
{
context: "product search input placeholder"
}
)}
fullWidth
InputProps={{
autoComplete: "off",
endAdornment: loading && <CircularProgress size={16} />
}}
/>
<FormSpacer />
<Table>
<TableBody>
{products &&
products.map(product => {
const isChecked = !!data.products.find(
selectedProduct => selectedProduct.id === product.id
);
return ( const handleSubmit = () => onSubmit(selectedProducts);
<TableRow key={product.id}>
<TableCellAvatar return (
className={classes.avatar} <Dialog
thumbnail={maybe(() => product.thumbnail.url)} onClose={onClose}
open={open}
classes={{ paper: classes.overflow }}
fullWidth
maxWidth="sm"
>
<DialogTitle>{i18n.t("Assign Product")}</DialogTitle>
<DialogContent>
<TextField
name="query"
value={query}
onChange={onQueryChange}
label={i18n.t("Search Products", {
context: "product search input label"
})}
placeholder={i18n.t(
"Search by product name, attribute, product type etc...",
{
context: "product search input placeholder"
}
)}
fullWidth
InputProps={{
autoComplete: "off",
endAdornment: loading && <CircularProgress size={16} />
}}
/>
<FormSpacer />
<div className={classes.scrollArea}>
<Table>
<TableBody>
{products &&
products.map(product => {
const isSelected = selectedProducts.some(
selectedProduct => selectedProduct.id === product.id
);
return (
<TableRow key={product.id}>
<TableCellAvatar
className={classes.avatar}
thumbnail={maybe(() => product.thumbnail.url)}
/>
<TableCell className={classes.wideCell}>
{product.name}
</TableCell>
<TableCell
padding="checkbox"
className={classes.checkboxCell}
>
<Checkbox
checked={isSelected}
onChange={() =>
handleProductAssign(
product,
isSelected,
selectedProducts,
setSelectedProducts
)
}
/> />
<TableCell className={classes.wideCell}> </TableCell>
{product.name} </TableRow>
</TableCell> );
<TableCell })}
padding="checkbox" </TableBody>
className={classes.checkboxCell} </Table>
> </div>
<Checkbox </DialogContent>
checked={isChecked} <DialogActions>
onChange={() => <Button onClick={onClose}>
isChecked {i18n.t("Cancel", { context: "button" })}
? change({ </Button>
target: { <ConfirmButton
name: "products", transitionState={confirmButtonState}
value: data.products.filter( color="primary"
selectedProduct => variant="contained"
selectedProduct.id !== product.id type="submit"
) onClick={handleSubmit}
} >
} as any) {i18n.t("Assign products", { context: "button" })}
: change({ </ConfirmButton>
target: { </DialogActions>
name: "products", </Dialog>
value: [...data.products, product] );
} }
} as any)
}
/>
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</DialogContent>
<DialogActions>
<Button onClick={onClose}>
{i18n.t("Cancel", { context: "button" })}
</Button>
<ConfirmButton
transitionState={confirmButtonState}
color="primary"
variant="contained"
type="submit"
>
{i18n.t("Assign products", { context: "button" })}
</ConfirmButton>
</DialogActions>
</>
)}
</Form>
</Dialog>
)
); );
AssignProductDialog.displayName = "AssignProductDialog"; AssignProductDialog.displayName = "AssignProductDialog";
export default AssignProductDialog; export default AssignProductDialog;

View file

@ -5,7 +5,7 @@ import {
WithStyles WithStyles
} from "@material-ui/core/styles"; } from "@material-ui/core/styles";
import Typography from "@material-ui/core/Typography"; import Typography from "@material-ui/core/Typography";
import * as classNames from "classnames"; import classNames from "classnames";
import React from "react"; import React from "react";
const styles = (theme: Theme) => const styles = (theme: Theme) =>
@ -32,7 +32,7 @@ const styles = (theme: Theme) =>
lineHeight: 1 lineHeight: 1
}, },
toolbar: { toolbar: {
marginRight: -theme.spacing.unit * 2 marginRight: -theme.spacing.unit
} }
}); });

View file

@ -9,7 +9,6 @@ import {
} from "@material-ui/core/styles"; } from "@material-ui/core/styles";
import classNames from "classnames"; import classNames from "classnames";
import React from "react"; import React from "react";
import { stopPropagation } from "../../misc";
export type CheckboxProps = Omit< export type CheckboxProps = Omit<
MuiCheckboxProps, MuiCheckboxProps,
@ -19,7 +18,9 @@ export type CheckboxProps = Omit<
| "indeterminateIcon" | "indeterminateIcon"
| "classes" | "classes"
| "onChange" | "onChange"
| "onClick"
> & { > & {
disableClickPropagation?: boolean;
onChange?: (event: React.ChangeEvent<any>) => void; onChange?: (event: React.ChangeEvent<any>) => void;
}; };
@ -45,7 +46,6 @@ const styles = (theme: Theme) =>
}, },
"&:before": { "&:before": {
background: "rgba(0, 0, 0, 0)", background: "rgba(0, 0, 0, 0)",
borderRadius: 2,
content: '""', content: '""',
height: 8, height: 8,
left: 2, left: 2,
@ -56,7 +56,6 @@ const styles = (theme: Theme) =>
}, },
WebkitAppearance: "none", WebkitAppearance: "none",
border: `1px solid ${theme.palette.grey[500]}`, border: `1px solid ${theme.palette.grey[500]}`,
borderRadius: 4,
boxSizing: "border-box", boxSizing: "border-box",
cursor: "pointer", cursor: "pointer",
height: 14, height: 14,
@ -84,21 +83,31 @@ const Checkbox = withStyles(styles, { name: "Checkbox" })(
className, className,
classes, classes,
disabled, disabled,
disableClickPropagation,
indeterminate, indeterminate,
onChange, onChange,
onClick,
value, value,
name, name,
...props ...props
}: CheckboxProps & WithStyles<typeof styles>) => { }: CheckboxProps & WithStyles<typeof styles>) => {
const inputRef = React.useRef<HTMLInputElement>(null); const inputRef = React.useRef<HTMLInputElement>(null);
const handleClick = React.useCallback(
disableClickPropagation
? event => {
event.stopPropagation();
inputRef.current.click();
}
: () => inputRef.current.click(),
[]
);
return ( return (
<ButtonBase <ButtonBase
{...props} {...props}
centerRipple centerRipple
className={classNames(classes.root, className)} className={classNames(classes.root, className)}
disabled={disabled} disabled={disabled}
onClick={stopPropagation(() => inputRef.current.click())} onClick={handleClick}
> >
<input <input
className={classNames(classes.box, { className={classNames(classes.box, {

View file

@ -4,7 +4,7 @@ import {
withStyles, withStyles,
WithStyles WithStyles
} from "@material-ui/core/styles"; } from "@material-ui/core/styles";
import * as classNames from "classnames"; import classNames from "classnames";
import React from "react"; import React from "react";
const styles = (theme: Theme) => const styles = (theme: Theme) =>

View file

@ -20,7 +20,7 @@ export const ControlledCheckbox: React.StatelessComponent<
<Checkbox <Checkbox
checked={checked} checked={checked}
name={name} name={name}
onClick={() => onChange({ target: { name, value: !checked } })} disableClickPropagation
onChange={() => onChange({ target: { name, value: !checked } })} onChange={() => onChange({ target: { name, value: !checked } })}
/> />
} }

View file

@ -7,6 +7,9 @@ const styles = (theme: Theme) =>
createStyles({ createStyles({
label: { label: {
marginLeft: theme.spacing.unit * 2 marginLeft: theme.spacing.unit * 2
},
labelText: {
fontSize: 14
} }
}); });
@ -15,7 +18,7 @@ interface ControlledSwitchProps extends WithStyles<typeof styles> {
disabled?: boolean; disabled?: boolean;
label: string | React.ReactNode; label: string | React.ReactNode;
name: string; name: string;
secondLabel?: string; secondLabel?: string | React.ReactNode;
uncheckedLabel?: string | React.ReactNode; uncheckedLabel?: string | React.ReactNode;
onChange?(event: React.ChangeEvent<any>); onChange?(event: React.ChangeEvent<any>);
} }
@ -46,7 +49,17 @@ export const ControlledSwitch = withStyles(styles, {
} }
label={ label={
<div className={classes.label}> <div className={classes.label}>
{uncheckedLabel ? (checked ? label : uncheckedLabel) : label} {uncheckedLabel ? (
checked ? (
label
) : (
uncheckedLabel
)
) : typeof label === "string" ? (
<span className={classes.labelText}>{label}</span>
) : (
label
)}
<div>{secondLabel ? secondLabel : null}</div> <div>{secondLabel ? secondLabel : null}</div>
</div> </div>
} }

View file

@ -1,5 +1,5 @@
import React from "react"; import React from "react";
import * as renderer from "react-test-renderer"; import renderer from "react-test-renderer";
import { TimezoneProvider } from "../Timezone"; import { TimezoneProvider } from "../Timezone";
import Date from "./Date"; import Date from "./Date";

View file

@ -1,5 +1,5 @@
import Tooltip from "@material-ui/core/Tooltip"; import Tooltip from "@material-ui/core/Tooltip";
import * as moment from "moment-timezone"; import moment from "moment-timezone";
import React from "react"; import React from "react";
import useDateLocalize from "@saleor/hooks/useDateLocalize"; import useDateLocalize from "@saleor/hooks/useDateLocalize";

View file

@ -1,5 +1,5 @@
import Tooltip from "@material-ui/core/Tooltip"; import Tooltip from "@material-ui/core/Tooltip";
import * as moment from "moment-timezone"; import moment from "moment-timezone";
import React from "react"; import React from "react";
import ReactMoment from "react-moment"; import ReactMoment from "react-moment";

View file

@ -1,5 +1,5 @@
import { RawDraftContentState } from "draft-js"; import { RawDraftContentState } from "draft-js";
import * as draftToHtml from "draftjs-to-html"; import draftToHtml from "draftjs-to-html";
import React from "react"; import React from "react";
interface DraftRendererProps { interface DraftRendererProps {

View file

@ -1,3 +1,3 @@
/* tslint:disable:no-submodule-imports */ /* tslint:disable:no-submodule-imports */
import * as Dropzone from "react-dropzone/dist/index"; import Dropzone from "react-dropzone/dist/index";
export default Dropzone; export default Dropzone;

View file

@ -9,10 +9,10 @@ import {
import TableCell from "@material-ui/core/TableCell"; import TableCell from "@material-ui/core/TableCell";
import TextField, { TextFieldProps } from "@material-ui/core/TextField"; import TextField, { TextFieldProps } from "@material-ui/core/TextField";
import Typography from "@material-ui/core/Typography"; import Typography from "@material-ui/core/Typography";
import * as classNames from "classnames"; import classNames from "classnames";
import React from "react"; import React from "react";
import Form from "@saleor/components/Form"; import useForm from "@saleor/hooks/useForm";
const styles = (theme: Theme) => const styles = (theme: Theme) =>
createStyles({ createStyles({
@ -66,48 +66,39 @@ export const EditableTableCell = withStyles(styles, {
value, value,
onConfirm onConfirm
}: EditableTableCellProps) => { }: EditableTableCellProps) => {
const [opened, setOpenStatus] = React.useState(focused);
const enable = () => setOpenStatus(true);
const disable = () => setOpenStatus(false);
const handleConfirm = (data: { value: string }) => { const handleConfirm = (data: { value: string }) => {
disable(); disable();
onConfirm(data.value); onConfirm(data.value);
}; };
const [opened, setOpenStatus] = React.useState(focused);
const { change, data } = useForm({ value }, [], handleConfirm);
const enable = () => setOpenStatus(true);
const disable = () => setOpenStatus(false);
return ( return (
<TableCell className={classNames(classes.container, className)}> <TableCell className={classNames(classes.container, className)}>
{opened && <div className={classes.overlay} onClick={disable} />} {opened && <div className={classes.overlay} onClick={disable} />}
<Form initial={{ value }} onSubmit={handleConfirm} useForm={false}> <Typography variant="caption" onClick={enable} className={classes.text}>
{({ change, data }) => ( {value || defaultValue}
<> </Typography>
<Typography {opened && (
variant="caption" <div className={classes.root}>
onClick={enable} <Card className={classes.card}>
className={classes.text} <CardContent>
> <TextField
{value || defaultValue} name="value"
</Typography> autoFocus
{opened && ( fullWidth
<div className={classes.root}> onChange={change}
<Card className={classes.card}> value={data.value}
<CardContent> variant="standard"
<TextField {...InputProps}
name="value" />
autoFocus </CardContent>
fullWidth </Card>
onChange={change} </div>
value={data.value} )}
variant="standard"
{...InputProps}
/>
</CardContent>
</Card>
</div>
)}
</>
)}
</Form>
</TableCell> </TableCell>
); );
} }

View file

@ -9,7 +9,7 @@ import Typography from "@material-ui/core/Typography";
import React from "react"; import React from "react";
import SVG from "react-inlinesvg"; import SVG from "react-inlinesvg";
import * as notFoundImage from "@assets/images/what.svg"; import notFoundImage from "@assets/images/what.svg";
import i18n from "../../i18n"; import i18n from "../../i18n";
export interface ErrorPageProps extends WithStyles<typeof styles> { export interface ErrorPageProps extends WithStyles<typeof styles> {

View file

@ -4,7 +4,7 @@ import {
withStyles, withStyles,
WithStyles WithStyles
} from "@material-ui/core/styles"; } from "@material-ui/core/styles";
import * as classNames from "classnames"; import classNames from "classnames";
import React from "react"; import React from "react";
const styles = (theme: Theme) => const styles = (theme: Theme) =>

View file

@ -11,7 +11,7 @@ import {
import { fade } from "@material-ui/core/styles/colorManipulator"; import { fade } from "@material-ui/core/styles/colorManipulator";
import Typography from "@material-ui/core/Typography"; import Typography from "@material-ui/core/Typography";
import ArrowDropDownIcon from "@material-ui/icons/ArrowDropDown"; import ArrowDropDownIcon from "@material-ui/icons/ArrowDropDown";
import * as classNames from "classnames"; import classNames from "classnames";
import React from "react"; import React from "react";
import { FilterContent } from "."; import { FilterContent } from ".";

View file

@ -47,7 +47,7 @@ const FilterContent: React.FC<FilterContentProps> = ({
}) => { }) => {
const [menuValue, setMenuValue] = React.useState<string>(""); const [menuValue, setMenuValue] = React.useState<string>("");
const [filterValue, setFilterValue] = React.useState<string | string[]>(""); const [filterValue, setFilterValue] = React.useState<string | string[]>("");
const classes = useStyles(); const classes = useStyles({});
const activeMenu = menuValue const activeMenu = menuValue
? getMenuItemByValue(filters, menuValue) ? getMenuItemByValue(filters, menuValue)

View file

@ -41,7 +41,7 @@ const FilterElement: React.FC<FilterElementProps> = ({
onChange, onChange,
value value
}) => { }) => {
const classes = useStyles(); const classes = useStyles({});
if (filter.data.type === FieldType.date) { if (filter.data.type === FieldType.date) {
return ( return (

View file

@ -41,6 +41,7 @@ const FilterBar: React.FC<FilterBarProps> = ({
<FilterTab <FilterTab
onClick={() => onTabChange(tabIndex + 1)} onClick={() => onTabChange(tabIndex + 1)}
label={tab.name} label={tab.name}
key={tabIndex}
/> />
))} ))}
{isCustom && ( {isCustom && (

View file

@ -1,170 +1,42 @@
import React from "react"; import React from "react";
import { UserError } from "../../types";
export interface FormProps<T extends {}> { import useForm, { UseFormResult } from "@saleor/hooks/useForm";
children: (props: { import { UserError } from "@saleor/types";
data: T;
hasChanged: boolean; export interface FormProps<T> {
errors: { [key: string]: string }; children: (props: UseFormResult<T>) => React.ReactNode;
change(event: React.ChangeEvent<any>, cb?: () => void); confirmLeave?: boolean;
reset();
submit(event?: React.FormEvent<any>);
}) => React.ReactElement<any>;
errors?: UserError[]; errors?: UserError[];
initial?: T; initial?: T;
confirmLeave?: boolean;
useForm?: boolean;
resetOnSubmit?: boolean; resetOnSubmit?: boolean;
onSubmit?(data: T); onSubmit?: (data: T) => void;
} }
interface FormComponentProps<T extends {}> extends FormProps<T> { function Form<T>(props: FormProps<T>) {
hasChanged: boolean; const { children, errors, initial, resetOnSubmit, onSubmit } = props;
toggleFormChangeState: () => void; const renderProps = useForm(initial, errors, onSubmit);
}
interface FormState<T extends {}> { function handleSubmit(event?: React.FormEvent<any>, cb?: () => void) {
initial: T; const { reset, submit } = renderProps;
fields: T;
hasChanged: boolean;
}
class FormComponent<T extends {} = {}> extends React.Component<
FormComponentProps<T>,
FormState<T>
> {
static getDerivedStateFromProps<T extends {} = {}>(
nextProps: FormComponentProps<T>,
prevState: FormState<T>
): FormState<T> {
const changedFields = Object.keys(nextProps.initial).filter(
nextFieldName =>
JSON.stringify(nextProps.initial[nextFieldName]) !==
JSON.stringify(prevState.initial[nextFieldName])
);
if (changedFields.length > 0) {
const swapFields = changedFields.reduce((prev, curr) => {
prev[curr] = nextProps.initial[curr];
return prev;
}, {});
return {
fields: {
...(prevState.fields as any),
...swapFields
},
hasChanged: false,
initial: {
...(prevState.initial as any),
...swapFields
}
};
}
return null;
}
state: FormState<T> = {
fields: this.props.initial,
hasChanged: false,
initial: this.props.initial
};
componentDidUpdate() {
const { hasChanged, confirmLeave, toggleFormChangeState } = this.props;
if (this.state.hasChanged !== hasChanged && confirmLeave) {
toggleFormChangeState();
}
}
componentDidMount() {
const { hasChanged, confirmLeave, toggleFormChangeState } = this.props;
if (this.state.hasChanged !== hasChanged && confirmLeave) {
toggleFormChangeState();
}
}
componentWillUnmount() {
const { hasChanged, confirmLeave, toggleFormChangeState } = this.props;
if (hasChanged && confirmLeave) {
toggleFormChangeState();
}
}
handleChange = (event: React.ChangeEvent<any>, cb?: () => void) => {
const { target } = event;
if (!(target.name in this.state.fields)) {
console.error(`Unknown form field: ${target.name}`);
return;
}
this.setState(
{
fields: {
...(this.state.fields as any),
[target.name]: target.value
},
hasChanged: true
},
typeof cb === "function" ? cb : undefined
);
};
handleKeyDown = (event: React.KeyboardEvent<any>) => {
switch (event.keyCode) {
// Enter
case 13:
this.props.onSubmit(this.state.fields);
break;
}
};
handleSubmit = (event?: React.FormEvent<any>, cb?: () => void) => {
const { resetOnSubmit, onSubmit } = this.props;
if (event) { if (event) {
event.stopPropagation(); event.stopPropagation();
event.preventDefault(); event.preventDefault();
} }
if (onSubmit !== undefined) {
onSubmit(this.state.fields);
}
if (cb) { if (cb) {
cb(); cb();
} }
if (resetOnSubmit) { if (resetOnSubmit) {
this.setState({ reset();
fields: this.state.initial
});
} }
};
render() { submit();
const { children, errors, useForm = true } = this.props;
const contents = children({
change: this.handleChange,
data: this.state.fields,
errors: errors
? errors.reduce(
(prev, curr) => ({
...prev,
[curr.field.split(":")[0]]: curr.message
}),
{}
)
: {},
hasChanged: this.state.hasChanged,
reset: () =>
this.setState({
fields: this.state.initial
}),
submit: this.handleSubmit
});
return useForm ? (
<form onSubmit={this.handleSubmit}>{contents}</form>
) : (
<div onKeyDown={this.handleKeyDown}>{contents}</div>
);
} }
return <form onSubmit={handleSubmit}>{children(renderProps)}</form>;
} }
export default FormComponent; Form.displayName = "Form";
export default Form;

View file

@ -1,5 +1,4 @@
export * from "./Form"; export * from "./Form";
export { default } from "./Form";
export { default as FormActions } from "./FormActions"; export { default as FormActions } from "./FormActions";
export * from "./FormActions"; export * from "./FormActions";
export * from "./FormContext";
export { default } from "./FormContext";

View file

@ -5,7 +5,7 @@ import {
WithStyles WithStyles
} from "@material-ui/core/styles"; } from "@material-ui/core/styles";
import Typography, { TypographyProps } from "@material-ui/core/Typography"; import Typography, { TypographyProps } from "@material-ui/core/Typography";
import * as classNames from "classnames"; import classNames from "classnames";
import React from "react"; import React from "react";
const styles = (theme: Theme) => const styles = (theme: Theme) =>

View file

@ -1,4 +1,5 @@
import CircularProgress from "@material-ui/core/CircularProgress"; import CircularProgress from "@material-ui/core/CircularProgress";
import IconButton from "@material-ui/core/IconButton";
import MenuItem from "@material-ui/core/MenuItem"; import MenuItem from "@material-ui/core/MenuItem";
import Paper from "@material-ui/core/Paper"; import Paper from "@material-ui/core/Paper";
import { import {
@ -8,27 +9,77 @@ import {
WithStyles WithStyles
} from "@material-ui/core/styles"; } from "@material-ui/core/styles";
import TextField from "@material-ui/core/TextField"; import TextField from "@material-ui/core/TextField";
import Typography from "@material-ui/core/Typography";
import CloseIcon from "@material-ui/icons/Close";
import Downshift, { ControllerStateAndHelpers } from "downshift"; import Downshift, { ControllerStateAndHelpers } from "downshift";
import React from "react"; import React from "react";
import { compareTwoStrings } from "string-similarity";
import i18n from "../../i18n"; import { fade } from "@material-ui/core/styles/colorManipulator";
import ArrowDropdownIcon from "../../icons/ArrowDropdown"; import Checkbox from "@saleor/components/Checkbox";
import Debounce, { DebounceProps } from "../Debounce"; import Debounce, { DebounceProps } from "@saleor/components/Debounce";
import i18n from "@saleor/i18n";
import ArrowDropdownIcon from "@saleor/icons/ArrowDropdown";
import Hr from "../Hr";
interface ChoiceType { export interface MultiAutocompleteChoiceType {
label: string; label: string;
value: string; value: string;
} }
const styles = (theme: Theme) => const styles = (theme: Theme) =>
createStyles({ createStyles({
checkbox: {
height: 24,
width: 20
},
chip: { chip: {
margin: `${theme.spacing.unit / 2}px ${theme.spacing.unit / 2}px` width: "100%"
},
chipClose: {
height: 32,
padding: 0,
width: 32
},
chipContainer: {
display: "flex",
flexDirection: "column",
marginTop: theme.spacing.unit
},
chipInner: {
"& svg": {
color: theme.palette.primary.contrastText
},
alignItems: "center",
background: fade(theme.palette.primary.main, 0.6),
borderRadius: 24,
color: theme.palette.primary.contrastText,
display: "flex",
justifyContent: "space-between",
margin: `${theme.spacing.unit}px 0`,
paddingLeft: theme.spacing.unit * 2,
paddingRight: theme.spacing.unit
},
chipLabel: {
color: theme.palette.primary.contrastText
}, },
container: { container: {
flexGrow: 1, flexGrow: 1,
position: "relative" position: "relative"
}, },
hr: {
margin: `${theme.spacing.unit}px 0`
},
menuItem: {
display: "grid",
gridColumnGap: theme.spacing.unit + "px",
gridTemplateColumns: "20px 1fr",
height: "auto",
whiteSpace: "normal"
},
menuItemLabel: {
overflowWrap: "break-word"
},
paper: { paper: {
left: 0, left: 0,
marginTop: theme.spacing.unit, marginTop: theme.spacing.unit,
@ -39,39 +90,32 @@ const styles = (theme: Theme) =>
} }
}); });
export interface MultiAutocompleteSelectFieldChildrenFunc { export interface MultiAutocompleteSelectFieldProps {
deleteItem: (item: ChoiceType) => void; allowCustomValues?: boolean;
items: ChoiceType[]; displayValues: MultiAutocompleteChoiceType[];
}
export type MultiAutocompleteSelectFieldChildren = (
props: MultiAutocompleteSelectFieldChildrenFunc
) => React.ReactNode;
export interface MultiAutocompleteSelectFieldProps
extends WithStyles<typeof styles> {
name: string; name: string;
children: MultiAutocompleteSelectFieldChildren; choices: MultiAutocompleteChoiceType[];
choices: ChoiceType[]; value: string[];
value?: ChoiceType[];
loading?: boolean; loading?: boolean;
placeholder?: string; placeholder?: string;
helperText?: string; helperText?: string;
label?: string; label?: string;
fetchChoices(value: string); fetchChoices?: (value: string) => void;
onChange(event); onChange: (event: React.ChangeEvent<any>) => void;
} }
const DebounceAutocomplete: React.ComponentType< const DebounceAutocomplete: React.ComponentType<
DebounceProps<string> DebounceProps<string>
> = Debounce; > = Debounce;
export const MultiAutocompleteSelectField = withStyles(styles, { export const MultiAutocompleteSelectFieldComponent = withStyles(styles, {
name: "MultiAutocompleteSelectField" name: "MultiAutocompleteSelectField"
})( })(
({ ({
children, allowCustomValues,
choices, choices,
classes, classes,
displayValues,
helperText, helperText,
label, label,
loading, loading,
@ -80,102 +124,200 @@ export const MultiAutocompleteSelectField = withStyles(styles, {
value, value,
fetchChoices, fetchChoices,
onChange onChange
}: MultiAutocompleteSelectFieldProps) => { }: MultiAutocompleteSelectFieldProps & WithStyles<typeof styles>) => {
const handleSelect = ( const handleSelect = (
item: ChoiceType, item: string,
{ reset }: ControllerStateAndHelpers downshiftOpts?: ControllerStateAndHelpers
) => { ) => {
reset({ inputValue: "" }); if (downshiftOpts) {
onChange({ target: { name, value: [...value, item] } }); downshiftOpts.reset({ inputValue: "" });
}
onChange({
target: { name, value: item }
} as any);
}; };
const handleDelete = (item: ChoiceType) => { const suggestions = choices.filter(choice => !value.includes(choice.value));
const newValue = value.slice();
newValue.splice(
value.findIndex(listItem => listItem.value === item.value),
1
);
onChange({ target: { name, value: newValue } });
};
const filteredChoices = choices.filter(
suggestion => value.map(v => v.value).indexOf(suggestion.value) === -1
);
return ( return (
<DebounceAutocomplete debounceFn={fetchChoices}> <>
{debounce => ( <Downshift
<Downshift onInputValueChange={fetchChoices}
selectedItem={value} onSelect={handleSelect}
itemToString={item => (item ? item.label : "")} itemToString={() => ""}
onSelect={handleSelect} >
onInputValueChange={value => debounce(value)} {({
> getInputProps,
{({ getItemProps,
getInputProps, isOpen,
getItemProps, toggleMenu,
isOpen, highlightedIndex,
selectedItem, inputValue
toggleMenu, }) => (
closeMenu, <div className={classes.container}>
openMenu, <TextField
highlightedIndex InputProps={{
}) => { ...getInputProps({
return ( placeholder
<div className={classes.container}> }),
<TextField endAdornment: (
InputProps={{ <div>
...getInputProps({ {loading ? (
placeholder <CircularProgress size={20} />
}), ) : (
endAdornment: ( <ArrowDropdownIcon onClick={toggleMenu} />
<div> )}
{loading ? ( </div>
<CircularProgress size={20} /> ),
) : ( id: undefined,
<ArrowDropdownIcon onClick={toggleMenu} /> onClick: toggleMenu
)} }}
</div> helperText={helperText}
), label={label}
id: undefined, fullWidth={true}
onBlur: closeMenu, />
onFocus: openMenu {isOpen && (!!inputValue || !!choices.length) && (
}} <Paper className={classes.paper} square>
helperText={helperText} {choices.length > 0 ||
label={label} displayValues.length > 0 ||
fullWidth={true} allowCustomValues ? (
/> <>
{isOpen && ( {displayValues.map(value => (
<Paper className={classes.paper} square> <MenuItem
{!loading && filteredChoices.length > 0 className={classes.menuItem}
? filteredChoices.map((suggestion, index) => ( key={value.value}
<MenuItem selected={true}
key={suggestion.value} component="div"
selected={highlightedIndex === index} {...getItemProps({
component="div" item: value.value
{...getItemProps({ item: suggestion })} })}
> >
{suggestion.label} <Checkbox
</MenuItem> className={classes.checkbox}
)) checked={true}
: !loading && ( disableRipple
<MenuItem disabled={true} component="div"> />
{i18n.t("No results found")} <span className={classes.menuItemLabel}>
</MenuItem> {value.label}
)} </span>
</Paper> </MenuItem>
))}
{displayValues.length > 0 && suggestions.length > 0 && (
<Hr className={classes.hr} />
)}
{suggestions.map((suggestion, index) => (
<MenuItem
className={classes.menuItem}
key={suggestion.value}
selected={highlightedIndex === index + value.length}
component="div"
{...getItemProps({
item: suggestion.value
})}
>
<Checkbox
checked={value.includes(suggestion.value)}
className={classes.checkbox}
disableRipple
/>
<span className={classes.menuItemLabel}>
{suggestion.label}
</span>
</MenuItem>
))}
{allowCustomValues &&
inputValue &&
!choices.find(
choice =>
choice.label.toLowerCase() ===
inputValue.toLowerCase()
) && (
<MenuItem
className={classes.menuItem}
key={"customValue"}
component="div"
{...getItemProps({
item: inputValue
})}
>
<span className={classes.menuItemLabel}>
{i18n.t("Add new value: {{ value }}", {
context: "add custom option",
value: inputValue
})}
</span>
</MenuItem>
)}
</>
) : (
!loading && (
<MenuItem disabled={true} component="div">
{i18n.t("No results found")}
</MenuItem>
)
)} )}
{children({ </Paper>
deleteItem: handleDelete, )}
items: selectedItem </div>
})} )}
</div> </Downshift>
); <div className={classes.chipContainer}>
}} {displayValues.map(value => (
</Downshift> <div className={classes.chip} key={value.value}>
<div className={classes.chipInner}>
<Typography className={classes.chipLabel} variant="caption">
{value.label}
</Typography>
<IconButton
className={classes.chipClose}
onClick={() => handleSelect(value.value)}
>
<CloseIcon fontSize="small" />
</IconButton>
</div>
</div>
))}
</div>
</>
);
}
);
const MultiAutocompleteSelectField: React.FC<
MultiAutocompleteSelectFieldProps
> = ({ choices, fetchChoices, ...props }) => {
const [query, setQuery] = React.useState("");
if (fetchChoices) {
return (
<DebounceAutocomplete debounceFn={fetchChoices}>
{debounceFn => (
<MultiAutocompleteSelectFieldComponent
choices={choices}
{...props}
fetchChoices={debounceFn}
/>
)} )}
</DebounceAutocomplete> </DebounceAutocomplete>
); );
} }
);
const sortedChoices = choices.sort((a, b) => {
const ratingA = compareTwoStrings(query, a.label);
const ratingB = compareTwoStrings(query, b.label);
if (ratingA > ratingB) {
return -1;
}
if (ratingA < ratingB) {
return 1;
}
return 0;
});
return (
<MultiAutocompleteSelectFieldComponent
fetchChoices={q => setQuery(q || "")}
choices={sortedChoices}
{...props}
/>
);
};
MultiAutocompleteSelectField.displayName = "MultiAutocompleteSelectField"; MultiAutocompleteSelectField.displayName = "MultiAutocompleteSelectField";
export default MultiAutocompleteSelectField; export default MultiAutocompleteSelectField;

View file

@ -9,8 +9,8 @@ import Typography from "@material-ui/core/Typography";
import React from "react"; import React from "react";
import SVG from "react-inlinesvg"; import SVG from "react-inlinesvg";
import * as notFoundImage from "@assets/images/not-found-404.svg"; import notFoundImage from "@assets/images/not-found-404.svg";
import i18n from "../../i18n"; import i18n from "@saleor/i18n";
const styles = (theme: Theme) => const styles = (theme: Theme) =>
createStyles({ createStyles({

View file

@ -12,6 +12,9 @@ import Skeleton from "../Skeleton";
const styles = (theme: Theme) => const styles = (theme: Theme) =>
createStyles({ createStyles({
root: {
display: "flex"
},
title: { title: {
flex: 1, flex: 1,
fontSize: 24, fontSize: 24,
@ -35,7 +38,7 @@ const PageHeader = withStyles(styles)(
</Typography> </Typography>
} }
> >
{children} <div className={classes.root}>{children}</div>
</ExtendedPageHeader> </ExtendedPageHeader>
) )
); );

View file

@ -11,11 +11,15 @@ import TableFooter from "@material-ui/core/TableFooter";
import TableRow from "@material-ui/core/TableRow"; import TableRow from "@material-ui/core/TableRow";
import React from "react"; import React from "react";
import TableCellAvatar from "@saleor/components/TableCellAvatar"; import TableCellAvatar, {
AVATAR_MARGIN
} from "@saleor/components/TableCellAvatar";
import { ProductListColumns } from "@saleor/config";
import i18n from "@saleor/i18n";
import { maybe, renderCollection } from "@saleor/misc";
import { ListActions, ListProps } from "@saleor/types";
import { isSelected } from "@saleor/utils/lists";
import { CategoryDetails_category_products_edges_node } from "../../categories/types/CategoryDetails"; import { CategoryDetails_category_products_edges_node } from "../../categories/types/CategoryDetails";
import i18n from "../../i18n";
import { maybe, renderCollection } from "../../misc";
import { ListActions, ListProps } from "../../types";
import Checkbox from "../Checkbox"; import Checkbox from "../Checkbox";
import Money from "../Money"; import Money from "../Money";
import Skeleton from "../Skeleton"; import Skeleton from "../Skeleton";
@ -27,7 +31,7 @@ const styles = (theme: Theme) =>
createStyles({ createStyles({
[theme.breakpoints.up("lg")]: { [theme.breakpoints.up("lg")]: {
colName: { colName: {
width: 430 width: "auto"
}, },
colPrice: { colPrice: {
width: 200 width: 200
@ -39,12 +43,14 @@ const styles = (theme: Theme) =>
width: 200 width: 200
} }
}, },
avatarCell: { colFill: {
paddingLeft: theme.spacing.unit * 2, padding: 0,
paddingRight: 0, width: "100%"
width: theme.spacing.unit * 5
}, },
colName: {}, colName: {},
colNameHeader: {
marginLeft: AVATAR_MARGIN
},
colPrice: { colPrice: {
textAlign: "right" textAlign: "right"
}, },
@ -53,6 +59,12 @@ const styles = (theme: Theme) =>
link: { link: {
cursor: "pointer" cursor: "pointer"
}, },
table: {
tableLayout: "fixed"
},
tableContainer: {
overflowX: "scroll"
},
textLeft: { textLeft: {
textAlign: "left" textAlign: "left"
}, },
@ -62,7 +74,7 @@ const styles = (theme: Theme) =>
}); });
interface ProductListProps interface ProductListProps
extends ListProps, extends ListProps<ProductListColumns>,
ListActions, ListActions,
WithStyles<typeof styles> { WithStyles<typeof styles> {
products: CategoryDetails_category_products_edges_node[]; products: CategoryDetails_category_products_edges_node[];
@ -71,6 +83,7 @@ interface ProductListProps
export const ProductList = withStyles(styles, { name: "ProductList" })( export const ProductList = withStyles(styles, { name: "ProductList" })(
({ ({
classes, classes,
settings,
disabled, disabled,
isChecked, isChecked,
pageInfo, pageInfo,
@ -81,114 +94,158 @@ export const ProductList = withStyles(styles, { name: "ProductList" })(
toolbar, toolbar,
onNextPage, onNextPage,
onPreviousPage, onPreviousPage,
onUpdateListSettings,
onRowClick onRowClick
}: ProductListProps) => ( }: ProductListProps) => {
<Table> const displayColumn = React.useCallback(
<TableHead (column: ProductListColumns) =>
selected={selected} isSelected(column, settings.columns, (a, b) => a === b),
disabled={disabled} [settings.columns]
items={products} );
toggleAll={toggleAll} const numberOfColumns = 2 + settings.columns.length;
toolbar={toolbar}
>
<TableCell />
<TableCell className={classes.colName}>
{i18n.t("Name", { context: "object" })}
</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={6}
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 ( return (
<TableRow <div className={classes.tableContainer}>
selected={isSelected} <Table className={classes.table}>
hover={!!product} <col />
key={product ? product.id : "skeleton"} <col className={classes.colName} />
onClick={product && onRowClick(product.id)} {displayColumn("productType") && <col className={classes.colType} />}
className={classes.link} {displayColumn("isPublished") && (
> <col className={classes.colPublished} />
<TableCell padding="checkbox"> )}
<Checkbox {displayColumn("price") && <col className={classes.colPrice} />}
checked={isSelected} <TableHead
disabled={disabled} colSpan={numberOfColumns}
onChange={() => toggle(product.id)} selected={selected}
/> disabled={disabled}
</TableCell> items={products}
<TableCellAvatar toggleAll={toggleAll}
thumbnail={maybe(() => product.thumbnail.url)} toolbar={toolbar}
/> >
<TableCell className={classes.colName}> <TableCell className={classes.colName}>
{product ? product.name : <Skeleton />} <span className={classes.colNameHeader}>
</TableCell> {i18n.t("Name", { context: "object" })}
<TableCell className={classes.colType}> </span>
{product && product.productType ? ( </TableCell>
product.productType.name {displayColumn("productType") && (
) : ( <TableCell className={classes.colType}>
<Skeleton /> {i18n.t("Type", { context: "object" })}
)} </TableCell>
</TableCell> )}
<TableCell className={classes.colPublished}> {displayColumn("isPublished") && (
{product && maybe(() => product.isAvailable !== undefined) ? ( <TableCell className={classes.colPublished}>
<StatusLabel {i18n.t("Published", { context: "object" })}
label={ </TableCell>
product.isAvailable )}
? i18n.t("Published", { context: "product status" }) {displayColumn("price") && (
: i18n.t("Not published", { <TableCell className={classes.colPrice}>
context: "product status" {i18n.t("Price", { context: "object" })}
}) </TableCell>
} )}
status={product.isAvailable ? "success" : "error"} </TableHead>
/> <TableFooter>
) : (
<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> <TableRow>
<TableCell colSpan={6}>{i18n.t("No products found")}</TableCell> <TablePagination
colSpan={numberOfColumns}
settings={settings}
hasNextPage={
pageInfo && !disabled ? pageInfo.hasNextPage : false
}
onNextPage={onNextPage}
onUpdateListSettings={onUpdateListSettings}
hasPreviousPage={
pageInfo && !disabled ? pageInfo.hasPreviousPage : false
}
onPreviousPage={onPreviousPage}
/>
</TableRow> </TableRow>
) </TableFooter>
)} <TableBody>
</TableBody> {renderCollection(
</Table> 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>
{displayColumn("productType") && (
<TableCell className={classes.colType}>
{product && product.productType ? (
product.productType.name
) : (
<Skeleton />
)}
</TableCell>
)}
{displayColumn("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("price") && (
<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>
);
}
); );
ProductList.displayName = "ProductList"; ProductList.displayName = "ProductList";
export default ProductList; export default ProductList;

View file

@ -6,7 +6,7 @@ import {
} from "@material-ui/core/styles"; } from "@material-ui/core/styles";
import { fade } from "@material-ui/core/styles/colorManipulator"; import { fade } from "@material-ui/core/styles/colorManipulator";
import Typography from "@material-ui/core/Typography"; import Typography from "@material-ui/core/Typography";
import * as classNames from "classnames"; import classNames from "classnames";
import { RawDraftContentState } from "draft-js"; import { RawDraftContentState } from "draft-js";
import { import {
BLOCK_TYPE, BLOCK_TYPE,
@ -14,6 +14,7 @@ import {
ENTITY_TYPE, ENTITY_TYPE,
INLINE_STYLE INLINE_STYLE
} from "draftail"; } from "draftail";
import isEqual from "lodash-es/isEqual";
import React from "react"; import React from "react";
import BoldIcon from "../../icons/BoldIcon"; import BoldIcon from "../../icons/BoldIcon";
@ -29,6 +30,7 @@ import UnorderedListIcon from "../../icons/UnorderedListIcon";
// import ImageEntity from "./ImageEntity"; // import ImageEntity from "./ImageEntity";
// import ImageSource from "./ImageSource"; // import ImageSource from "./ImageSource";
import { ChangeEvent } from "@saleor/hooks/useForm";
import LinkEntity from "./LinkEntity"; import LinkEntity from "./LinkEntity";
import LinkSource from "./LinkSource"; import LinkSource from "./LinkSource";
@ -45,22 +47,6 @@ export interface RichTextEditorProps {
const styles = (theme: Theme) => const styles = (theme: Theme) =>
createStyles({ createStyles({
"@keyframes focus": {
from: {
transform: "scaleX(0) scaleY(1)"
},
to: {
transform: "scaleX(1) scaleY(1)"
}
},
"@keyframes hover": {
from: {
transform: "scaleX(1) scaleY(0)"
},
to: {
transform: "scaleX(1) scaleY(1)"
}
},
error: { error: {
color: theme.palette.error.main color: theme.palette.error.main
}, },
@ -68,21 +54,13 @@ const styles = (theme: Theme) =>
marginTop: theme.spacing.unit * 0.75 marginTop: theme.spacing.unit * 0.75
}, },
input: { input: {
"&:hover": { position: "relative"
borderBottomColor: theme.palette.primary.main
},
backgroundColor: theme.overrides.MuiFilledInput.root.backgroundColor,
borderBottom: `1px rgba(0, 0, 0, 0) solid`,
borderTopLeftRadius: 4,
borderTopRightRadius: 4,
padding: "27px 12px 10px",
position: "relative",
transition: theme.transitions.duration.shortest + "ms"
}, },
label: { label: {
fontSize: theme.typography.caption.fontSize, fontSize: theme.typography.caption.fontSize,
marginBottom: theme.spacing.unit * 2, left: 12,
marginTop: -21 position: "absolute",
top: 9
}, },
linkIcon: { linkIcon: {
marginTop: 2 marginTop: 2
@ -97,8 +75,6 @@ const styles = (theme: Theme) =>
color: theme.palette.primary.light color: theme.palette.primary.light
}, },
"&:after": { "&:after": {
animationDuration: theme.transitions.duration.shortest + "ms",
animationFillMode: "both",
background: theme.palette.getContrastText( background: theme.palette.getContrastText(
theme.palette.background.default theme.palette.background.default
), ),
@ -120,16 +96,19 @@ const styles = (theme: Theme) =>
"& .Draftail": { "& .Draftail": {
"&-Editor": { "&-Editor": {
"&--focus": { "&--focus": {
"& .DraftEditor": { boxShadow: `inset 0px 0px 0px 2px ${theme.palette.primary.main}`
"&-editorContainer": { },
"&:after": { "&:hover": {
animationName: "focus !important", borderColor: theme.palette.primary.main
background: theme.palette.primary.main, },
transform: "scaleX(0) scaleY(1)" border: `1px ${
} theme.overrides.MuiOutlinedInput.root.borderColor
} } solid`,
} borderTopLeftRadius: 4,
} borderTopRightRadius: 4,
padding: "27px 12px 10px",
position: "relative",
transition: theme.transitions.duration.shortest + "ms"
}, },
"&-Toolbar": { "&-Toolbar": {
"&Button": { "&Button": {
@ -190,6 +169,7 @@ const styles = (theme: Theme) =>
display: "inline-flex", display: "inline-flex",
flexWrap: "wrap", flexWrap: "wrap",
marginBottom: theme.spacing.unit, marginBottom: theme.spacing.unit,
marginTop: 10,
[theme.breakpoints.down(460)]: { [theme.breakpoints.down(460)]: {
width: "min-content" width: "min-content"
} }
@ -205,24 +185,7 @@ const styles = (theme: Theme) =>
"&$error": { "&$error": {
"& .Draftail": { "& .Draftail": {
"&-Editor": { "&-Editor": {
"& .DraftEditor": { borderColor: theme.palette.error.main
"&-editorContainer": {
"&:after": {
animationName: "none",
background: theme.palette.error.main,
transform: "scaleX(1) scaleY(1)"
}
}
},
"&--focus": {
"& .DraftEditor": {
"&-editorContainer": {
"&:after": {
animationName: "none !important"
}
}
}
}
} }
} }
} }
@ -240,6 +203,23 @@ const styles = (theme: Theme) =>
marginLeft: 10 marginLeft: 10
} }
}); });
function handleSave(
value: any,
initial: any,
name: string,
onChange: (event: ChangeEvent) => void
) {
if (value && !isEqual(value, initial)) {
onChange({
target: {
name,
value
}
});
}
}
const RichTextEditor = withStyles(styles, { name: "RichTextEditor" })( const RichTextEditor = withStyles(styles, { name: "RichTextEditor" })(
({ ({
classes, classes,
@ -267,14 +247,7 @@ const RichTextEditor = withStyles(styles, { name: "RichTextEditor" })(
rawContentState={ rawContentState={
initial && Object.keys(initial).length > 0 ? initial : null initial && Object.keys(initial).length > 0 ? initial : null
} }
onSave={value => onSave={value => handleSave(value, initial, name, onChange)}
onChange({
target: {
name,
value
}
} as any)
}
blockTypes={[ blockTypes={[
{ {
icon: <HeaderOne />, icon: <HeaderOne />,

View file

@ -9,7 +9,7 @@ import {
import classNames from "classnames"; import classNames from "classnames";
import React from "react"; import React from "react";
import useScroll from "@saleor/hooks/useScroll"; import useWindowScroll from "@saleor/hooks/useWindowScroll";
import i18n from "../../i18n"; import i18n from "../../i18n";
import { maybe } from "../../misc"; import { maybe } from "../../misc";
import AppActionContext from "../AppLayout/AppActionContext"; import AppActionContext from "../AppLayout/AppActionContext";
@ -82,7 +82,7 @@ export const SaveButtonBar = withStyles(styles, { name: "SaveButtonBar" })(
onSave, onSave,
...props ...props
}: SaveButtonBarProps) => { }: SaveButtonBarProps) => {
const scrollPosition = useScroll(); const scrollPosition = useWindowScroll();
const scrolledToBottom = const scrolledToBottom =
scrollPosition.y + window.innerHeight >= document.body.scrollHeight; scrollPosition.y + window.innerHeight >= document.body.scrollHeight;

View file

@ -9,7 +9,7 @@ import {
} from "@material-ui/core/styles"; } from "@material-ui/core/styles";
import TextField from "@material-ui/core/TextField"; import TextField from "@material-ui/core/TextField";
import Typography from "@material-ui/core/Typography"; import Typography from "@material-ui/core/Typography";
import * as classNames from "classnames"; import classNames from "classnames";
import React from "react"; import React from "react";
import i18n from "../../i18n"; import i18n from "../../i18n";
@ -43,6 +43,9 @@ const styles = (theme: Theme) =>
flex: 1 flex: 1
}, },
labelContainer: { labelContainer: {
"& span": {
paddingRight: 30
},
display: "flex" display: "flex"
}, },
preview: { preview: {
@ -128,7 +131,6 @@ const SeoForm = withStyles(styles, { name: "SeoForm" })(
)} )}
value={title.slice(0, 69)} value={title.slice(0, 69)}
disabled={loading || disabled} disabled={loading || disabled}
InputLabelProps={{ shrink: true }}
placeholder={titlePlaceholder} placeholder={titlePlaceholder}
onChange={onChange} onChange={onChange}
fullWidth fullWidth
@ -158,7 +160,6 @@ const SeoForm = withStyles(styles, { name: "SeoForm" })(
fullWidth fullWidth
multiline multiline
placeholder={descriptionPlaceholder} placeholder={descriptionPlaceholder}
InputLabelProps={{ shrink: true }}
rows={10} rows={10}
/> />
</div> </div>

View file

@ -1,4 +1,5 @@
import { Omit } from "@material-ui/core"; import { Omit } from "@material-ui/core";
import CircularProgress from "@material-ui/core/CircularProgress";
import { InputProps } from "@material-ui/core/Input"; import { InputProps } from "@material-ui/core/Input";
import MenuItem from "@material-ui/core/MenuItem"; import MenuItem from "@material-ui/core/MenuItem";
import Paper from "@material-ui/core/Paper"; import Paper from "@material-ui/core/Paper";
@ -9,10 +10,12 @@ import {
WithStyles WithStyles
} from "@material-ui/core/styles"; } from "@material-ui/core/styles";
import TextField from "@material-ui/core/TextField"; import TextField from "@material-ui/core/TextField";
import Typography from "@material-ui/core/Typography";
import Downshift from "downshift"; import Downshift from "downshift";
import React from "react"; import React from "react";
import { compareTwoStrings } from "string-similarity"; import { compareTwoStrings } from "string-similarity";
import useStateFromProps from "@saleor/hooks/useStateFromProps";
import i18n from "../../i18n"; import i18n from "../../i18n";
import ArrowDropdownIcon from "../../icons/ArrowDropdown"; import ArrowDropdownIcon from "../../icons/ArrowDropdown";
import Debounce, { DebounceProps } from "../Debounce"; import Debounce, { DebounceProps } from "../Debounce";
@ -23,6 +26,10 @@ const styles = (theme: Theme) =>
flexGrow: 1, flexGrow: 1,
position: "relative" position: "relative"
}, },
menuItem: {
height: "auto",
whiteSpace: "normal"
},
paper: { paper: {
borderRadius: 4, borderRadius: 4,
left: 0, left: 0,
@ -34,26 +41,26 @@ const styles = (theme: Theme) =>
} }
}); });
export interface SingleAutocompleteChoiceType {
label: string;
value: any;
}
export interface SingleAutocompleteSelectFieldProps { export interface SingleAutocompleteSelectFieldProps {
error?: boolean; error?: boolean;
name: string; name: string;
choices: Array<{ displayValue: string;
label: string; emptyOption?: boolean;
value: any; choices: SingleAutocompleteChoiceType[];
}>; value?: string;
value?: {
label: string;
value: any;
};
disabled?: boolean; disabled?: boolean;
loading?: boolean; loading?: boolean;
placeholder?: string; placeholder?: string;
custom?: boolean; allowCustomValues?: boolean;
helperText?: string; helperText?: string;
label?: string; label?: string;
InputProps?: InputProps; InputProps?: InputProps;
fetchChoices?(value: string); fetchChoices?: (value: string) => void;
onChange(event); onChange: (event: React.ChangeEvent<any>) => void;
} }
interface SingleAutocompleteSelectFieldState { interface SingleAutocompleteSelectFieldState {
@ -73,8 +80,10 @@ const SingleAutocompleteSelectFieldComponent = withStyles(styles, {
({ ({
choices, choices,
classes, classes,
custom, allowCustomValues,
disabled, disabled,
displayValue,
emptyOption,
error, error,
helperText, helperText,
label, label,
@ -86,16 +95,24 @@ const SingleAutocompleteSelectFieldComponent = withStyles(styles, {
fetchChoices, fetchChoices,
onChange onChange
}: SingleAutocompleteSelectFieldProps & WithStyles<typeof styles>) => { }: SingleAutocompleteSelectFieldProps & WithStyles<typeof styles>) => {
const handleChange = item => onChange({ target: { name, value: item } }); const [prevDisplayValue] = useStateFromProps(displayValue);
const handleChange = item =>
onChange({
target: {
name,
value: item
}
} as any);
return ( return (
<DebounceAutocomplete debounceFn={fetchChoices}> <DebounceAutocomplete debounceFn={fetchChoices}>
{debounceFn => ( {debounceFn => (
<Downshift <Downshift
selectedItem={value} defaultInputValue={displayValue}
itemToString={item => (item ? item.label : "")} itemToString={() => displayValue}
onSelect={handleChange}
onInputValueChange={value => debounceFn(value)} onInputValueChange={value => debounceFn(value)}
onSelect={handleChange}
selectedItem={value}
> >
{({ {({
getInputProps, getInputProps,
@ -106,13 +123,18 @@ const SingleAutocompleteSelectFieldComponent = withStyles(styles, {
toggleMenu, toggleMenu,
openMenu, openMenu,
closeMenu, closeMenu,
highlightedIndex highlightedIndex,
reset
}) => { }) => {
const isCustom = const isCustomValueSelected =
choices && selectedItem choices && selectedItem
? choices.filter(c => c.value === selectedItem.value) ? choices.filter(c => c.value === selectedItem).length === 0
.length === 0
: false; : false;
if (prevDisplayValue !== displayValue) {
reset({ inputValue: displayValue });
}
return ( return (
<div className={classes.container}> <div className={classes.container}>
<TextField <TextField
@ -122,9 +144,13 @@ const SingleAutocompleteSelectFieldComponent = withStyles(styles, {
placeholder placeholder
}), }),
endAdornment: ( endAdornment: (
<ArrowDropdownIcon <div>
onClick={disabled ? undefined : toggleMenu} {loading ? (
/> <CircularProgress size={20} />
) : (
<ArrowDropdownIcon onClick={toggleMenu} />
)}
</div>
), ),
error, error,
id: undefined, id: undefined,
@ -136,36 +162,66 @@ const SingleAutocompleteSelectFieldComponent = withStyles(styles, {
label={label} label={label}
fullWidth={true} fullWidth={true}
/> />
{isOpen && ( {isOpen && (!!inputValue || !!choices.length) && (
<Paper className={classes.paper} square> <Paper className={classes.paper} square>
{loading ? ( {choices.length > 0 || allowCustomValues ? (
<MenuItem disabled={true} component="div">
{i18n.t("Loading...")}
</MenuItem>
) : choices.length > 0 || custom ? (
<> <>
{choices.map((suggestion, index) => ( {emptyOption && (
<MenuItem <MenuItem
key={JSON.stringify(suggestion)} className={classes.menuItem}
selected={highlightedIndex === index}
component="div"
{...getItemProps({ item: suggestion })}
>
{suggestion.label}
</MenuItem>
))}
{custom && (
<MenuItem
key={"customValue"}
selected={isCustom}
component="div" component="div"
{...getItemProps({ {...getItemProps({
item: { label: inputValue, value: inputValue } item: ""
})} })}
> >
{i18n.t("Add custom value")} <Typography color="textSecondary">
{i18n.t("None")}
</Typography>
</MenuItem> </MenuItem>
)} )}
{choices.map((suggestion, index) => {
const choiceIndex = index + (emptyOption ? 1 : 0);
return (
<MenuItem
className={classes.menuItem}
key={JSON.stringify(suggestion)}
selected={
highlightedIndex === choiceIndex ||
selectedItem === suggestion.value
}
component="div"
{...getItemProps({
index: choiceIndex,
item: suggestion.value
})}
>
{suggestion.label}
</MenuItem>
);
})}
{allowCustomValues &&
!!inputValue &&
!choices.find(
choice =>
choice.label.toLowerCase() ===
inputValue.toLowerCase()
) && (
<MenuItem
className={classes.menuItem}
key={"customValue"}
selected={isCustomValueSelected}
component="div"
{...getItemProps({
item: inputValue
})}
>
{i18n.t("Add new value: {{ value }}", {
context: "add custom option",
value: inputValue
})}
</MenuItem>
)}
</> </>
) : ( ) : (
<MenuItem disabled={true} component="div"> <MenuItem disabled={true} component="div">

View file

@ -17,6 +17,11 @@ const styles = (theme: Theme) =>
opacity: 1 opacity: 1
} }
}, },
primary: {
"&$skeleton": {
background: theme.palette.primary.main
}
},
skeleton: { skeleton: {
animation: "skeleton-animation .75s linear infinite forwards alternate", animation: "skeleton-animation .75s linear infinite forwards alternate",
background: theme.palette.background.default, background: theme.palette.background.default,
@ -29,12 +34,18 @@ const styles = (theme: Theme) =>
interface SkeletonProps extends WithStyles<typeof styles> { interface SkeletonProps extends WithStyles<typeof styles> {
className?: string; className?: string;
primary?: boolean;
style?: React.CSSProperties; style?: React.CSSProperties;
} }
const Skeleton = withStyles(styles, { name: "Skeleton" })( const Skeleton = withStyles(styles, { name: "Skeleton" })(
({ className, classes, style }: SkeletonProps) => ( ({ className, classes, primary, style }: SkeletonProps) => (
<span className={classNames(classes.skeleton, className)} style={style}> <span
className={classNames(classes.skeleton, className, {
[classes.primary]: primary
})}
style={style}
>
&zwnj; &zwnj;
</span> </span>
) )

View file

@ -6,7 +6,7 @@ import {
WithStyles WithStyles
} from "@material-ui/core/styles"; } from "@material-ui/core/styles";
import Typography, { TypographyProps } from "@material-ui/core/Typography"; import Typography, { TypographyProps } from "@material-ui/core/Typography";
import * as classNames from "classnames"; import classNames from "classnames";
import React from "react"; import React from "react";
const styles = (theme: Theme) => { const styles = (theme: Theme) => {

View file

@ -5,7 +5,7 @@ import {
WithStyles WithStyles
} from "@material-ui/core/styles"; } from "@material-ui/core/styles";
import Typography from "@material-ui/core/Typography"; import Typography from "@material-ui/core/Typography";
import * as classNames from "classnames"; import classNames from "classnames";
import React from "react"; import React from "react";
const styles = (theme: Theme) => const styles = (theme: Theme) =>

View file

@ -7,11 +7,13 @@ import {
} from "@material-ui/core/styles"; } from "@material-ui/core/styles";
import TableCell from "@material-ui/core/TableCell"; import TableCell from "@material-ui/core/TableCell";
import Cached from "@material-ui/icons/Cached"; import Cached from "@material-ui/icons/Cached";
import * as classNames from "classnames"; import classNames from "classnames";
import React from "react"; import React from "react";
import Image from "../../icons/Image"; import Image from "../../icons/Image";
export const AVATAR_MARGIN = 56;
const styles = (theme: Theme) => const styles = (theme: Theme) =>
createStyles({ createStyles({
avatar: { avatar: {
@ -19,8 +21,17 @@ const styles = (theme: Theme) =>
border: `1px solid ${theme.overrides.MuiCard.root.borderColor}`, border: `1px solid ${theme.overrides.MuiCard.root.borderColor}`,
borderRadius: 2, borderRadius: 2,
color: "#bdbdbd", color: "#bdbdbd",
display: "inline-flex",
padding: theme.spacing.unit / 2 padding: theme.spacing.unit / 2
}, },
children: {
alignSelf: "center",
marginLeft: theme.spacing.unit * 2
},
content: {
alignItems: "center",
display: "flex"
},
root: { root: {
paddingRight: theme.spacing.unit * 3, paddingRight: theme.spacing.unit * 3,
width: "1%" width: "1%"
@ -31,25 +42,35 @@ interface TableCellAvatarProps extends WithStyles<typeof styles> {
className?: string; className?: string;
thumbnail?: string; thumbnail?: string;
avatarProps?: string; avatarProps?: string;
children?: React.ReactNode | React.ReactNodeArray;
} }
const TableCellAvatar = withStyles(styles, { name: "TableCellAvatar" })( const TableCellAvatar = withStyles(styles, { name: "TableCellAvatar" })(
({ classes, className, thumbnail, avatarProps }: TableCellAvatarProps) => ( ({
classes,
children,
className,
thumbnail,
avatarProps
}: TableCellAvatarProps) => (
<TableCell className={classNames(classes.root, className)}> <TableCell className={classNames(classes.root, className)}>
{thumbnail === undefined ? ( <div className={classes.content}>
<Avatar className={classNames(classes.avatar, avatarProps)}> {thumbnail === undefined ? (
<Cached color="primary" /> <Avatar className={classNames(classes.avatar, avatarProps)}>
</Avatar> <Cached color="primary" />
) : thumbnail === null ? ( </Avatar>
<Avatar className={classNames(classes.avatar, avatarProps)}> ) : thumbnail === null ? (
<Image color="primary" /> <Avatar className={classNames(classes.avatar, avatarProps)}>
</Avatar> <Image color="primary" />
) : ( </Avatar>
<Avatar ) : (
className={classNames(classes.avatar, avatarProps)} <Avatar
src={thumbnail} className={classNames(classes.avatar, avatarProps)}
/> src={thumbnail}
)} />
)}
<span className={classes.children}>{children}</span>
</div>
</TableCell> </TableCell>
) )
); );

View file

@ -27,7 +27,7 @@ const useInputStyles = makeStyles({
}); });
const Search: React.FC<TextFieldProps> = props => { const Search: React.FC<TextFieldProps> = props => {
const classes = useInputStyles(); const classes = useInputStyles({});
return ( return (
<TextField <TextField
{...props} {...props}

View file

@ -11,7 +11,7 @@ import MuiTableHead, {
} from "@material-ui/core/TableHead"; } from "@material-ui/core/TableHead";
import TableRow from "@material-ui/core/TableRow"; import TableRow from "@material-ui/core/TableRow";
import Typography from "@material-ui/core/Typography"; import Typography from "@material-ui/core/Typography";
import * as classNames from "classnames"; import classNames from "classnames";
import React from "react"; import React from "react";
import { Node } from "../../types"; import { Node } from "../../types";
@ -20,7 +20,9 @@ import i18n from "../../i18n";
import Checkbox from "../Checkbox"; import Checkbox from "../Checkbox";
export interface TableHeadProps extends MuiTableHeadProps { export interface TableHeadProps extends MuiTableHeadProps {
colSpan: number;
disabled: boolean; disabled: boolean;
dragRows?: boolean;
selected: number; selected: number;
items: Node[]; items: Node[];
toolbar: React.ReactNode | React.ReactNodeArray; toolbar: React.ReactNode | React.ReactNodeArray;
@ -34,7 +36,7 @@ const styles = (theme: Theme) =>
}, },
checkboxPartialSelect: { checkboxPartialSelect: {
"&:after": { "&:after": {
background: "#fff", background: theme.palette.common.white,
content: "''", content: "''",
height: 2, height: 2,
position: "absolute", position: "absolute",
@ -50,6 +52,10 @@ const styles = (theme: Theme) =>
height: 47, height: 47,
marginRight: -theme.spacing.unit * 2 marginRight: -theme.spacing.unit * 2
}, },
dragRows: {
padding: 0,
width: 52
},
padding: { padding: {
"&:last-child": { "&:last-child": {
padding: 0 padding: 0
@ -57,7 +63,6 @@ const styles = (theme: Theme) =>
}, },
root: { root: {
backgroundColor: fade(theme.palette.primary.main, 0.05), backgroundColor: fade(theme.palette.primary.main, 0.05),
borderBottom: "1px solid rgba(224, 224, 224, 1)",
paddingLeft: 0, paddingLeft: 0,
paddingRight: 24 paddingRight: 24
}, },
@ -77,7 +82,9 @@ const TableHead = withStyles(styles, {
({ ({
classes, classes,
children, children,
colSpan,
disabled, disabled,
dragRows,
items, items,
selected, selected,
toggleAll, toggleAll,
@ -87,13 +94,21 @@ const TableHead = withStyles(styles, {
return ( return (
<MuiTableHead {...muiTableHeadProps}> <MuiTableHead {...muiTableHeadProps}>
<TableRow> <TableRow>
<TableCell {dragRows && (items === undefined || items.length > 0) && (
padding="checkbox" <TableCell
className={classNames({ className={classNames({
[classes.checkboxSelected]: selected [classes.checkboxSelected]: selected
})} })}
> />
{items && items.length > 0 ? ( )}
{(items === undefined || items.length > 0) && (
<TableCell
padding="checkbox"
className={classNames({
[classes.checkboxSelected]: selected,
[classes.dragRows]: dragRows
})}
>
<Checkbox <Checkbox
className={classNames({ className={classNames({
[classes.checkboxPartialSelect]: [classes.checkboxPartialSelect]:
@ -103,11 +118,14 @@ const TableHead = withStyles(styles, {
disabled={disabled} disabled={disabled}
onChange={() => toggleAll(items, selected)} onChange={() => toggleAll(items, selected)}
/> />
) : null} </TableCell>
</TableCell> )}
{selected ? ( {selected ? (
<> <>
<TableCell className={classNames(classes.root)} colSpan={50}> <TableCell
className={classNames(classes.root)}
colSpan={colSpan - 1}
>
<div className={classes.container}> <div className={classes.container}>
{selected && ( {selected && (
<Typography> <Typography>

View file

@ -11,6 +11,9 @@ import TableCell from "@material-ui/core/TableCell";
import Toolbar from "@material-ui/core/Toolbar"; import Toolbar from "@material-ui/core/Toolbar";
import React from "react"; import React from "react";
import RowNumberSelect from "@saleor/components/RowNumberSelect";
import { maybe } from "@saleor/misc";
import { ListSettings } from "../../types";
import TablePaginationActions from "./TablePaginationActions"; import TablePaginationActions from "./TablePaginationActions";
const styles = (theme: Theme) => const styles = (theme: Theme) =>
@ -50,6 +53,7 @@ const styles = (theme: Theme) =>
toolbar: { toolbar: {
height: 56, height: 56,
minHeight: 56, minHeight: 56,
paddingLeft: 2,
paddingRight: 2 paddingRight: 2
} }
}); });
@ -59,11 +63,13 @@ interface TablePaginationProps extends WithStyles<typeof styles> {
backIconButtonProps?: Partial<IconButtonProps>; backIconButtonProps?: Partial<IconButtonProps>;
colSpan: number; colSpan: number;
component?: string | typeof TableCell; component?: string | typeof TableCell;
settings?: ListSettings;
hasNextPage: boolean; hasNextPage: boolean;
hasPreviousPage: boolean; hasPreviousPage: boolean;
nextIconButtonProps?: Partial<IconButtonProps>; nextIconButtonProps?: Partial<IconButtonProps>;
onNextPage(event); onNextPage(event);
onPreviousPage(event); onPreviousPage(event);
onUpdateListSettings?(key: keyof ListSettings, value: any): void;
} }
const TablePagination = withStyles(styles, { name: "TablePagination" })( const TablePagination = withStyles(styles, { name: "TablePagination" })(
@ -73,11 +79,13 @@ const TablePagination = withStyles(styles, { name: "TablePagination" })(
classes, classes,
colSpan: colSpanProp, colSpan: colSpanProp,
component: Component, component: Component,
settings,
hasNextPage, hasNextPage,
hasPreviousPage, hasPreviousPage,
nextIconButtonProps, nextIconButtonProps,
onNextPage, onNextPage,
onPreviousPage, onPreviousPage,
onUpdateListSettings,
...other ...other
}: TablePaginationProps) => { }: TablePaginationProps) => {
let colSpan; let colSpan;
@ -89,7 +97,15 @@ const TablePagination = withStyles(styles, { name: "TablePagination" })(
return ( return (
<Component className={classes.root} colSpan={colSpan} {...other}> <Component className={classes.root} colSpan={colSpan} {...other}>
<Toolbar className={classes.toolbar}> <Toolbar className={classes.toolbar}>
<div className={classes.spacer} /> <div className={classes.spacer}>
{maybe(() => settings.rowNumber) && (
<RowNumberSelect
choices={[20, 30, 50, 100]}
settings={settings}
onChange={onUpdateListSettings}
/>
)}
</div>
<Actions <Actions
backIconButtonProps={backIconButtonProps} backIconButtonProps={backIconButtonProps}
hasNextPage={hasNextPage} hasNextPage={hasNextPage}

View file

@ -9,7 +9,7 @@ import { fade } from "@material-ui/core/styles/colorManipulator";
import ArrowLeft from "@material-ui/icons/ArrowLeft"; import ArrowLeft from "@material-ui/icons/ArrowLeft";
import ArrowRight from "@material-ui/icons/ArrowRight"; import ArrowRight from "@material-ui/icons/ArrowRight";
import useTheme from "@saleor/hooks/useTheme"; import useTheme from "@saleor/hooks/useTheme";
import * as classNames from "classnames"; import classNames from "classnames";
import React from "react"; import React from "react";
const styles = (theme: Theme) => const styles = (theme: Theme) =>

View file

@ -78,7 +78,7 @@ const TextFieldWithChoice = withStyles(styles, {
<Typography component="span" variant="caption"> <Typography component="span" variant="caption">
{ChoiceProps.label} {ChoiceProps.label}
</Typography> </Typography>
<DropdownIcon /> {ChoiceProps.values ? <DropdownIcon /> : null}
</div> </div>
<Popper <Popper
open={menuOpen} open={menuOpen}

View file

@ -14,17 +14,21 @@ const dark: IThemeColors = {
}, },
error: "#C22D74", error: "#C22D74",
font: { font: {
button: "#202124",
default: "#FCFCFC", default: "#FCFCFC",
gray: "#9E9D9D" gray: "#9E9D9D",
textButton: "#FFFFFF"
}, },
gray: { gray: {
default: "#202124", default: "#202124",
disabled: "rgba(32, 33, 36, 0.6)" disabled: "rgba(32, 33, 36, 0.6)"
}, },
input: { input: {
border: "#9d9d9d",
default: "#25262A", default: "#25262A",
disabled: "#292A2D", disabled: "none",
focused: "#25262A" text: "#FCFCFC",
textHover: "#616161"
}, },
paperBorder: "#252728", paperBorder: "#252728",
primary: "#13BEBB", primary: "#13BEBB",
@ -38,20 +42,24 @@ const light: IThemeColors = {
}, },
error: "#C22D74", error: "#C22D74",
font: { font: {
button: "#FFFFFF",
default: "#3D3D3D", default: "#3D3D3D",
gray: "#616161" gray: "#616161",
textButton: "#06847B"
}, },
gray: { gray: {
default: "#C8C8C8", default: "#C8C8C8",
disabled: "rgba(216, 216, 216, 0.3)" disabled: "rgba(216, 216, 216, 0.3)"
}, },
input: { input: {
default: "#F1F6F6", border: "#616161",
default: "#FFFFFF",
disabled: "#EAEAEA", disabled: "#EAEAEA",
focused: "#DCEBEB" text: "#3D3D3D",
textHover: "#616161"
}, },
paperBorder: "#EAEAEA", paperBorder: "#EAEAEA",
primary: "#13BEBB", primary: "#06847B",
secondary: "#21125E" secondary: "#21125E"
}; };

View file

@ -1,7 +1,7 @@
import Avatar from "@material-ui/core/Avatar"; import Avatar from "@material-ui/core/Avatar";
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 * as colors from "@material-ui/core/colors"; import colors from "@material-ui/core/colors";
import { import {
createStyles, createStyles,
Theme, Theme,
@ -10,7 +10,7 @@ import {
} from "@material-ui/core/styles"; } from "@material-ui/core/styles";
import Typography from "@material-ui/core/Typography"; import Typography from "@material-ui/core/Typography";
import PersonIcon from "@material-ui/icons/Person"; import PersonIcon from "@material-ui/icons/Person";
import * as CRC from "crc-32"; import CRC from "crc-32";
import React from "react"; import React from "react";
import { DateTime } from "../Date"; import { DateTime } from "../Date";

View file

@ -1,4 +1,5 @@
import { SearchQueryVariables } from "./containers/BaseSearch"; import { SearchQueryVariables } from "./containers/BaseSearch";
import { ListSettings, ListViews } from "./types";
export const APP_MOUNT_URI = process.env.APP_MOUNT_URI || "/"; export const APP_MOUNT_URI = process.env.APP_MOUNT_URI || "/";
export const API_URI = process.env.API_URI || "/graphql/"; export const API_URI = process.env.API_URI || "/graphql/";
@ -10,3 +11,58 @@ export const DEFAULT_INITIAL_SEARCH_DATA: SearchQueryVariables = {
}; };
export const PAGINATE_BY = 20; export const PAGINATE_BY = 20;
export type ProductListColumns = "productType" | "isPublished" | "price";
export interface AppListViewSettings {
[ListViews.CATEGORY_LIST]: ListSettings;
[ListViews.COLLECTION_LIST]: ListSettings;
[ListViews.CUSTOMER_LIST]: ListSettings;
[ListViews.DRAFT_LIST]: ListSettings;
[ListViews.NAVIGATION_LIST]: ListSettings;
[ListViews.ORDER_LIST]: ListSettings;
[ListViews.PAGES_LIST]: ListSettings;
[ListViews.PRODUCT_LIST]: ListSettings<ProductListColumns>;
[ListViews.SALES_LIST]: ListSettings;
[ListViews.SHIPPING_METHODS_LIST]: ListSettings;
[ListViews.STAFF_MEMBERS_LIST]: ListSettings;
[ListViews.VOUCHER_LIST]: ListSettings;
}
export const defaultListSettings: AppListViewSettings = {
[ListViews.CATEGORY_LIST]: {
rowNumber: PAGINATE_BY
},
[ListViews.COLLECTION_LIST]: {
rowNumber: PAGINATE_BY
},
[ListViews.CUSTOMER_LIST]: {
rowNumber: PAGINATE_BY
},
[ListViews.DRAFT_LIST]: {
rowNumber: PAGINATE_BY
},
[ListViews.NAVIGATION_LIST]: {
rowNumber: PAGINATE_BY
},
[ListViews.ORDER_LIST]: {
rowNumber: PAGINATE_BY
},
[ListViews.PAGES_LIST]: {
rowNumber: PAGINATE_BY
},
[ListViews.PRODUCT_LIST]: {
columns: ["isPublished", "price", "productType"],
rowNumber: PAGINATE_BY
},
[ListViews.SALES_LIST]: {
rowNumber: PAGINATE_BY
},
[ListViews.SHIPPING_METHODS_LIST]: {
rowNumber: PAGINATE_BY
},
[ListViews.STAFF_MEMBERS_LIST]: {
rowNumber: PAGINATE_BY
},
[ListViews.VOUCHER_LIST]: {
rowNumber: PAGINATE_BY
}
};

View file

@ -1,28 +1,36 @@
import React from "react"; import React from "react";
import { attributeListUrl } from "@saleor/attributes/urls";
import { WindowTitle } from "@saleor/components/WindowTitle";
import useNavigator from "@saleor/hooks/useNavigator"; import useNavigator from "@saleor/hooks/useNavigator";
import useUser from "@saleor/hooks/useUser"; import useUser from "@saleor/hooks/useUser";
import { WindowTitle } from "../components/WindowTitle"; import i18n from "@saleor/i18n";
import i18n from "../i18n"; import Navigation from "@saleor/icons/Navigation";
import Navigation from "../icons/Navigation"; import Pages from "@saleor/icons/Pages";
import Pages from "../icons/Pages"; import ProductTypes from "@saleor/icons/ProductTypes";
import ProductTypes from "../icons/ProductTypes"; import ShippingMethods from "@saleor/icons/ShippingMethods";
import ShippingMethods from "../icons/ShippingMethods"; import SiteSettings from "@saleor/icons/SiteSettings";
import SiteSettings from "../icons/SiteSettings"; import StaffMembers from "@saleor/icons/StaffMembers";
import StaffMembers from "../icons/StaffMembers"; import Taxes from "@saleor/icons/Taxes";
import Taxes from "../icons/Taxes"; import { maybe } from "@saleor/misc";
import { maybe } from "../misc"; import { menuListUrl } from "@saleor/navigation/urls";
import { menuListUrl } from "../navigation/urls"; import { pageListUrl } from "@saleor/pages/urls";
import { pageListUrl } from "../pages/urls"; import { productTypeListUrl } from "@saleor/productTypes/urls";
import { productTypeListUrl } from "../productTypes/urls"; import { shippingZonesListUrl } from "@saleor/shipping/urls";
import { shippingZonesListUrl } from "../shipping/urls"; import { siteSettingsUrl } from "@saleor/siteSettings/urls";
import { siteSettingsUrl } from "../siteSettings/urls"; import { staffListUrl } from "@saleor/staff/urls";
import { staffListUrl } from "../staff/urls"; import { taxSection } from "@saleor/taxes/urls";
import { taxSection } from "../taxes/urls"; import { PermissionEnum } from "@saleor/types/globalTypes";
import { PermissionEnum } from "../types/globalTypes";
import ConfigurationPage, { MenuItem } from "./ConfigurationPage"; import ConfigurationPage, { MenuItem } from "./ConfigurationPage";
export const configurationMenu: MenuItem[] = [ export const configurationMenu: MenuItem[] = [
{
description: i18n.t("Determine attributes used to create product types"),
icon: <ProductTypes fontSize="inherit" viewBox="0 0 44 44" />,
permission: PermissionEnum.MANAGE_PRODUCTS,
title: i18n.t("Attributes"),
url: attributeListUrl()
},
{ {
description: i18n.t("Define types of products you sell"), description: i18n.t("Define types of products you sell"),
icon: <ProductTypes fontSize="inherit" viewBox="0 0 44 44" />, icon: <ProductTypes fontSize="inherit" viewBox="0 0 44 44" />,

View file

@ -12,9 +12,11 @@ import ConfirmButton, {
ConfirmButtonTransitionState ConfirmButtonTransitionState
} from "@saleor/components/ConfirmButton"; } from "@saleor/components/ConfirmButton";
import Form from "@saleor/components/Form"; import Form from "@saleor/components/Form";
import i18n from "../../../i18n"; import useStateFromProps from "@saleor/hooks/useStateFromProps";
import { maybe } from "../../../misc"; import i18n from "@saleor/i18n";
import { UserError } from "../../../types"; import { maybe } from "@saleor/misc";
import { UserError } from "@saleor/types";
import createSingleAutocompleteSelectHandler from "@saleor/utils/handlers/singleAutocompleteSelectChangeHandler";
import { AddressTypeInput } from "../../types"; import { AddressTypeInput } from "../../types";
import { CustomerAddresses_user_addresses } from "../../types/CustomerAddresses"; import { CustomerAddresses_user_addresses } from "../../types/CustomerAddresses";
@ -50,14 +52,14 @@ const CustomerAddressDialog = withStyles(styles, {})(
onClose, onClose,
onConfirm onConfirm
}: CustomerAddressDialogProps & WithStyles<typeof styles>) => { }: CustomerAddressDialogProps & WithStyles<typeof styles>) => {
const [countryDisplayName, setCountryDisplayName] = useStateFromProps(
maybe(() => address.country.country, "")
);
const initialForm: AddressTypeInput = { const initialForm: AddressTypeInput = {
city: maybe(() => address.city, ""), city: maybe(() => address.city, ""),
cityArea: maybe(() => address.cityArea, ""), cityArea: maybe(() => address.cityArea, ""),
companyName: maybe(() => address.companyName, ""), companyName: maybe(() => address.companyName, ""),
country: { country: maybe(() => address.country.code, ""),
label: maybe(() => address.country.country, ""),
value: maybe(() => address.country.code, "")
},
countryArea: maybe(() => address.countryArea, ""), countryArea: maybe(() => address.countryArea, ""),
firstName: maybe(() => address.firstName, ""), firstName: maybe(() => address.firstName, ""),
lastName: maybe(() => address.lastName, ""), lastName: maybe(() => address.lastName, ""),
@ -66,6 +68,16 @@ const CustomerAddressDialog = withStyles(styles, {})(
streetAddress1: maybe(() => address.streetAddress1, ""), streetAddress1: maybe(() => address.streetAddress1, ""),
streetAddress2: maybe(() => address.streetAddress2, "") streetAddress2: maybe(() => address.streetAddress2, "")
}; };
const countryChoices = maybe(
() =>
countries.map(country => ({
label: country.label,
value: country.code
})),
[]
);
return ( return (
<Dialog <Dialog
onClose={onClose} onClose={onClose}
@ -75,38 +87,48 @@ const CustomerAddressDialog = withStyles(styles, {})(
maxWidth="sm" maxWidth="sm"
> >
<Form initial={initialForm} errors={errors} onSubmit={onConfirm}> <Form initial={initialForm} errors={errors} onSubmit={onConfirm}>
{({ change, data, errors, submit }) => ( {({ change, data, errors, submit }) => {
<> const handleCountrySelect = createSingleAutocompleteSelectHandler(
<DialogTitle> change,
{variant === "create" setCountryDisplayName,
? i18n.t("Add Address") countryChoices
: i18n.t("Edit Address")} );
</DialogTitle>
<DialogContent className={classes.overflow}> return (
<AddressEdit <>
countries={countries} <DialogTitle>
data={data} {variant === "create"
errors={errors} ? i18n.t("Add Address")
onChange={change} : i18n.t("Edit Address")}
/> </DialogTitle>
</DialogContent> <DialogContent className={classes.overflow}>
<DialogActions> <AddressEdit
<Button onClick={onClose}> countries={countryChoices}
{i18n.t("Cancel", { context: "button" })} data={data}
</Button> countryDisplayValue={countryDisplayName}
<ConfirmButton errors={errors}
transitionState={confirmButtonState} onChange={change}
color="primary" onCountryChange={handleCountrySelect}
variant="contained" />
onClick={submit} </DialogContent>
type="submit" <DialogActions>
> <Button onClick={onClose}>
{i18n.t("Save Address", { context: "button" })} {i18n.t("Cancel", { context: "button" })}
<AddIcon /> </Button>
</ConfirmButton> <ConfirmButton
</DialogActions> transitionState={confirmButtonState}
</> color="primary"
)} variant="contained"
onClick={submit}
type="submit"
>
{i18n.t("Save Address", { context: "button" })}
<AddIcon />
</ConfirmButton>
</DialogActions>
</>
);
}}
</Form> </Form>
</Dialog> </Dialog>
); );

View file

@ -7,10 +7,10 @@ import React from "react";
import AddressEdit from "@saleor/components/AddressEdit"; import AddressEdit from "@saleor/components/AddressEdit";
import CardTitle from "@saleor/components/CardTitle"; import CardTitle from "@saleor/components/CardTitle";
import { FormSpacer } from "@saleor/components/FormSpacer"; import { FormSpacer } from "@saleor/components/FormSpacer";
import { SingleAutocompleteChoiceType } from "@saleor/components/SingleAutocompleteSelectField";
import i18n from "../../../i18n"; import i18n from "../../../i18n";
import { FormErrors } from "../../../types"; import { FormErrors } from "../../../types";
import { AddressTypeInput } from "../../types"; import { AddressTypeInput } from "../../types";
import { CustomerCreateData_shop_countries } from "../../types/CustomerCreateData";
const styles = createStyles({ const styles = createStyles({
overflow: { overflow: {
@ -19,11 +19,13 @@ const styles = createStyles({
}); });
export interface CustomerCreateAddressProps extends WithStyles<typeof styles> { export interface CustomerCreateAddressProps extends WithStyles<typeof styles> {
countries: CustomerCreateData_shop_countries[]; countries: SingleAutocompleteChoiceType[];
countryDisplayName: string;
data: AddressTypeInput; data: AddressTypeInput;
disabled: boolean; disabled: boolean;
errors: FormErrors<keyof AddressTypeInput>; errors: FormErrors<keyof AddressTypeInput>;
onChange(event: React.ChangeEvent<any>); onChange(event: React.ChangeEvent<any>);
onCountryChange(event: React.ChangeEvent<any>);
} }
const CustomerCreateAddress = withStyles(styles, { const CustomerCreateAddress = withStyles(styles, {
@ -32,10 +34,12 @@ const CustomerCreateAddress = withStyles(styles, {
({ ({
classes, classes,
countries, countries,
countryDisplayName,
data, data,
disabled, disabled,
errors, errors,
onChange onChange,
onCountryChange
}: CustomerCreateAddressProps) => ( }: CustomerCreateAddressProps) => (
<Card className={classes.overflow}> <Card className={classes.overflow}>
<CardTitle title={i18n.t("Primary address")} /> <CardTitle title={i18n.t("Primary address")} />
@ -45,14 +49,13 @@ const CustomerCreateAddress = withStyles(styles, {
</Typography> </Typography>
<FormSpacer /> <FormSpacer />
<AddressEdit <AddressEdit
countries={countries.map(country => ({ countries={countries}
code: country.code,
label: country.country
}))}
data={data} data={data}
disabled={disabled} disabled={disabled}
countryDisplayValue={countryDisplayName}
errors={errors} errors={errors}
onChange={onChange} onChange={onChange}
onCountryChange={onCountryChange}
/> />
</CardContent> </CardContent>
</Card> </Card>

View file

@ -8,6 +8,7 @@ import Form from "@saleor/components/Form";
import Grid from "@saleor/components/Grid"; import Grid from "@saleor/components/Grid";
import PageHeader from "@saleor/components/PageHeader"; import PageHeader from "@saleor/components/PageHeader";
import SaveButtonBar from "@saleor/components/SaveButtonBar"; import SaveButtonBar from "@saleor/components/SaveButtonBar";
import createSingleAutocompleteSelectHandler from "@saleor/utils/handlers/singleAutocompleteSelectChangeHandler";
import i18n from "../../../i18n"; import i18n from "../../../i18n";
import { UserError } from "../../../types"; import { UserError } from "../../../types";
import { AddressTypeInput } from "../../types"; import { AddressTypeInput } from "../../types";
@ -27,10 +28,7 @@ const initialForm: CustomerCreatePageFormData = {
city: "", city: "",
cityArea: "", cityArea: "",
companyName: "", companyName: "",
country: { country: "",
label: "",
value: ""
},
countryArea: "", countryArea: "",
customerFirstName: "", customerFirstName: "",
customerLastName: "", customerLastName: "",
@ -60,46 +58,69 @@ const CustomerCreatePage: React.StatelessComponent<CustomerCreatePageProps> = ({
saveButtonBar, saveButtonBar,
onBack, onBack,
onSubmit onSubmit
}: CustomerCreatePageProps) => ( }: CustomerCreatePageProps) => {
<Form initial={initialForm} onSubmit={onSubmit} errors={errors} confirmLeave> const [countryDisplayName, setCountryDisplayName] = React.useState("");
{({ change, data, errors: formErrors, hasChanged, submit }) => ( const countryChoices = countries.map(country => ({
<Container> label: country.country,
<AppHeader onBack={onBack}>{i18n.t("Customers")}</AppHeader> value: country.code
<PageHeader title={i18n.t("Add customer")} /> }));
<Grid>
<div> return (
<CustomerCreateDetails <Form
data={data} initial={initialForm}
disabled={disabled} onSubmit={onSubmit}
errors={formErrors} errors={errors}
onChange={change} confirmLeave
>
{({ change, data, errors: formErrors, hasChanged, submit }) => {
const handleCountrySelect = createSingleAutocompleteSelectHandler(
change,
setCountryDisplayName,
countryChoices
);
return (
<Container>
<AppHeader onBack={onBack}>{i18n.t("Customers")}</AppHeader>
<PageHeader title={i18n.t("Add customer")} />
<Grid>
<div>
<CustomerCreateDetails
data={data}
disabled={disabled}
errors={formErrors}
onChange={change}
/>
<CardSpacer />
<CustomerCreateAddress
countries={countryChoices}
countryDisplayName={countryDisplayName}
data={data}
disabled={disabled}
errors={formErrors}
onChange={change}
onCountryChange={handleCountrySelect}
/>
<CardSpacer />
<CustomerCreateNote
data={data}
disabled={disabled}
errors={formErrors}
onChange={change}
/>
</div>
</Grid>
<SaveButtonBar
disabled={disabled || !hasChanged}
state={saveButtonBar}
onSave={submit}
onCancel={onBack}
/> />
<CardSpacer /> </Container>
<CustomerCreateAddress );
countries={countries} }}
data={data} </Form>
disabled={disabled} );
errors={formErrors} };
onChange={change}
/>
<CardSpacer />
<CustomerCreateNote
data={data}
disabled={disabled}
errors={formErrors}
onChange={change}
/>
</div>
</Grid>
<SaveButtonBar
disabled={disabled || !hasChanged}
state={saveButtonBar}
onSave={submit}
onCancel={onBack}
/>
</Container>
)}
</Form>
);
CustomerCreatePage.displayName = "CustomerCreatePage"; CustomerCreatePage.displayName = "CustomerCreatePage";
export default CustomerCreatePage; export default CustomerCreatePage;

View file

@ -8,7 +8,7 @@ import {
} from "@material-ui/core/styles"; } from "@material-ui/core/styles";
import TextField from "@material-ui/core/TextField"; import TextField from "@material-ui/core/TextField";
import Typography from "@material-ui/core/Typography"; import Typography from "@material-ui/core/Typography";
import * as moment from "moment-timezone"; import moment from "moment-timezone";
import React from "react"; import React from "react";
import CardTitle from "@saleor/components/CardTitle"; import CardTitle from "@saleor/components/CardTitle";

View file

@ -55,11 +55,11 @@ const CustomerDetailsPage: React.StatelessComponent<
<Form <Form
errors={errors} errors={errors}
initial={{ initial={{
email: maybe(() => customer.email), email: maybe(() => customer.email, ""),
firstName: maybe(() => customer.firstName), firstName: maybe(() => customer.firstName, ""),
isActive: maybe(() => customer.isActive, false), isActive: maybe(() => customer.isActive, false),
lastName: maybe(() => customer.lastName), lastName: maybe(() => customer.lastName, ""),
note: maybe(() => customer.note) note: maybe(() => customer.note, "")
}} }}
onSubmit={onSubmit} onSubmit={onSubmit}
confirmLeave confirmLeave

View file

@ -16,9 +16,9 @@ import Checkbox from "@saleor/components/Checkbox";
import Skeleton from "@saleor/components/Skeleton"; import Skeleton from "@saleor/components/Skeleton";
import TableHead from "@saleor/components/TableHead"; import TableHead from "@saleor/components/TableHead";
import TablePagination from "@saleor/components/TablePagination"; import TablePagination from "@saleor/components/TablePagination";
import i18n from "../../../i18n"; import i18n from "@saleor/i18n";
import { getUserName, maybe, renderCollection } from "../../../misc"; import { getUserName, maybe, renderCollection } from "@saleor/misc";
import { ListActions, ListProps } from "../../../types"; import { ListActions, ListProps } from "@saleor/types";
import { ListCustomers_customers_edges_node } from "../../types/ListCustomers"; import { ListCustomers_customers_edges_node } from "../../types/ListCustomers";
const styles = (theme: Theme) => const styles = (theme: Theme) =>
@ -47,14 +47,18 @@ export interface CustomerListProps
customers: ListCustomers_customers_edges_node[]; customers: ListCustomers_customers_edges_node[];
} }
const numberOfColumns = 4;
const CustomerList = withStyles(styles, { name: "CustomerList" })( const CustomerList = withStyles(styles, { name: "CustomerList" })(
({ ({
classes, classes,
settings,
disabled, disabled,
customers, customers,
pageInfo, pageInfo,
onNextPage, onNextPage,
onPreviousPage, onPreviousPage,
onUpdateListSettings,
onRowClick, onRowClick,
toolbar, toolbar,
toggle, toggle,
@ -65,6 +69,7 @@ const CustomerList = withStyles(styles, { name: "CustomerList" })(
<Card> <Card>
<Table> <Table>
<TableHead <TableHead
colSpan={numberOfColumns}
selected={selected} selected={selected}
disabled={disabled} disabled={disabled}
items={customers} items={customers}
@ -84,9 +89,11 @@ const CustomerList = withStyles(styles, { name: "CustomerList" })(
<TableFooter> <TableFooter>
<TableRow> <TableRow>
<TablePagination <TablePagination
colSpan={4} colSpan={numberOfColumns}
settings={settings}
hasNextPage={pageInfo && !disabled ? pageInfo.hasNextPage : false} hasNextPage={pageInfo && !disabled ? pageInfo.hasNextPage : false}
onNextPage={onNextPage} onNextPage={onNextPage}
onUpdateListSettings={onUpdateListSettings}
hasPreviousPage={ hasPreviousPage={
pageInfo && !disabled ? pageInfo.hasPreviousPage : false pageInfo && !disabled ? pageInfo.hasPreviousPage : false
} }
@ -112,6 +119,7 @@ const CustomerList = withStyles(styles, { name: "CustomerList" })(
<Checkbox <Checkbox
checked={isSelected} checked={isSelected}
disabled={disabled} disabled={disabled}
disableClickPropagation
onChange={() => toggle(customer.id)} onChange={() => toggle(customer.id)}
/> />
</TableCell> </TableCell>
@ -132,7 +140,7 @@ const CustomerList = withStyles(styles, { name: "CustomerList" })(
}, },
() => ( () => (
<TableRow> <TableRow>
<TableCell colSpan={4}> <TableCell colSpan={numberOfColumns}>
{i18n.t("No customers found")} {i18n.t("No customers found")}
</TableCell> </TableCell>
</TableRow> </TableRow>

View file

@ -4,8 +4,8 @@ import React from "react";
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 i18n from "../../../i18n"; import i18n from "@saleor/i18n";
import { ListActions, PageListProps } from "../../../types"; import { ListActions, PageListProps } from "@saleor/types";
import { ListCustomers_customers_edges_node } from "../../types/ListCustomers"; import { ListCustomers_customers_edges_node } from "../../types/ListCustomers";
import CustomerList from "../CustomerList/CustomerList"; import CustomerList from "../CustomerList/CustomerList";

View file

@ -2,10 +2,7 @@ export interface AddressTypeInput {
city: string; city: string;
cityArea?: string; cityArea?: string;
companyName?: string; companyName?: string;
country: { country: string;
label: string;
value: string;
};
countryArea?: string; countryArea?: string;
firstName: string; firstName: string;
lastName: string; lastName: string;

View file

@ -133,6 +133,16 @@ const CustomerAddresses: React.FC<CustomerAddressesProps> = ({
[] []
) )
); );
const countryChoices = maybe(
() =>
shop.countries.map(country => ({
code: country.code,
label: country.country
})),
[]
);
return ( return (
<> <>
<WindowTitle <WindowTitle
@ -156,14 +166,7 @@ const CustomerAddresses: React.FC<CustomerAddressesProps> = ({
<CustomerAddressDialog <CustomerAddressDialog
address={undefined} address={undefined}
confirmButtonState={createAddressTransitionState} confirmButtonState={createAddressTransitionState}
countries={maybe( countries={countryChoices}
() =>
shop.countries.map(country => ({
code: country.code,
label: country.country
})),
[]
)}
errors={maybe( errors={maybe(
() => () =>
createCustomerAddressOpts.data.addressCreate createCustomerAddressOpts.data.addressCreate
@ -173,14 +176,11 @@ const CustomerAddresses: React.FC<CustomerAddressesProps> = ({
open={params.action === "add"} open={params.action === "add"}
variant="create" variant="create"
onClose={closeModal} onClose={closeModal}
onConfirm={formData => onConfirm={input =>
createCustomerAddress({ createCustomerAddress({
variables: { variables: {
id, id,
input: { input
...formData,
country: formData.country.value
}
} }
}) })
} }
@ -192,7 +192,7 @@ const CustomerAddresses: React.FC<CustomerAddressesProps> = ({
) )
)} )}
confirmButtonState={updateAddressTransitionState} confirmButtonState={updateAddressTransitionState}
countries={[]} countries={countryChoices}
errors={maybe( errors={maybe(
() => () =>
updateCustomerAddressOpts.data.addressUpdate updateCustomerAddressOpts.data.addressUpdate
@ -202,14 +202,11 @@ const CustomerAddresses: React.FC<CustomerAddressesProps> = ({
open={params.action === "edit"} open={params.action === "edit"}
variant="edit" variant="edit"
onClose={closeModal} onClose={closeModal}
onConfirm={formData => onConfirm={input =>
updateCustomerAddress({ updateCustomerAddress({
variables: { variables: {
id: params.id, id: params.id,
input: { input
...formData,
country: formData.country.value
}
} }
}) })
} }

View file

@ -67,14 +67,8 @@ export const CustomerCreate: React.StatelessComponent<{}> = () => {
createCustomer({ createCustomer({
variables: { variables: {
input: { input: {
defaultBillingAddress: { defaultBillingAddress: address,
...address, defaultShippingAddress: address,
country: address.country.value
},
defaultShippingAddress: {
...address,
country: address.country.value
},
email: formData.email, email: formData.email,
firstName: formData.customerFirstName, firstName: formData.customerFirstName,
lastName: formData.customerLastName, lastName: formData.customerLastName,

View file

@ -5,14 +5,15 @@ import React from "react";
import ActionDialog from "@saleor/components/ActionDialog"; import ActionDialog from "@saleor/components/ActionDialog";
import useBulkActions from "@saleor/hooks/useBulkActions"; import useBulkActions from "@saleor/hooks/useBulkActions";
import useListSettings from "@saleor/hooks/useListSettings";
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 usePaginator, { import usePaginator, {
createPaginationState createPaginationState
} from "@saleor/hooks/usePaginator"; } from "@saleor/hooks/usePaginator";
import { PAGINATE_BY } from "../../config"; import i18n from "@saleor/i18n";
import i18n from "../../i18n"; import { getMutationState, maybe } from "@saleor/misc";
import { getMutationState, maybe } from "../../misc"; import { ListViews } from "@saleor/types";
import CustomerListPage from "../components/CustomerListPage"; import CustomerListPage from "../components/CustomerListPage";
import { TypedBulkRemoveCustomers } from "../mutations"; import { TypedBulkRemoveCustomers } from "../mutations";
import { TypedCustomerListQuery } from "../queries"; import { TypedCustomerListQuery } from "../queries";
@ -37,6 +38,9 @@ export const CustomerList: React.StatelessComponent<CustomerListProps> = ({
const { isSelected, listElements, reset, toggle, toggleAll } = useBulkActions( const { isSelected, listElements, reset, toggle, toggleAll } = useBulkActions(
params.ids params.ids
); );
const { updateListSettings, settings } = useListSettings(
ListViews.CUSTOMER_LIST
);
const closeModal = () => const closeModal = () =>
navigate( navigate(
@ -48,7 +52,7 @@ export const CustomerList: React.StatelessComponent<CustomerListProps> = ({
true true
); );
const paginationState = createPaginationState(PAGINATE_BY, params); const paginationState = createPaginationState(settings.rowNumber, params);
return ( return (
<TypedCustomerListQuery displayLoader variables={paginationState}> <TypedCustomerListQuery displayLoader variables={paginationState}>
@ -87,11 +91,13 @@ export const CustomerList: React.StatelessComponent<CustomerListProps> = ({
customers={maybe(() => customers={maybe(() =>
data.customers.edges.map(edge => edge.node) data.customers.edges.map(edge => edge.node)
)} )}
settings={settings}
disabled={loading} disabled={loading}
pageInfo={pageInfo} pageInfo={pageInfo}
onAdd={() => navigate(customerAddUrl)} onAdd={() => navigate(customerAddUrl)}
onNextPage={loadNextPage} onNextPage={loadNextPage}
onPreviousPage={loadPreviousPage} onPreviousPage={loadPreviousPage}
onUpdateListSettings={updateListSettings}
onRowClick={id => () => navigate(customerUrl(id))} onRowClick={id => () => navigate(customerUrl(id))}
toolbar={ toolbar={
<IconButton <IconButton

View file

@ -50,6 +50,9 @@ const styles = (theme: Theme) =>
width: "60%" width: "60%"
} }
}); });
const numberOfColumns = 4;
const DiscountCategories = withStyles(styles, { const DiscountCategories = withStyles(styles, {
name: "DiscountCategories" name: "DiscountCategories"
})( })(
@ -71,9 +74,7 @@ const DiscountCategories = withStyles(styles, {
}: DiscountCategoriesProps & WithStyles<typeof styles>) => ( }: DiscountCategoriesProps & WithStyles<typeof styles>) => (
<Card> <Card>
<CardTitle <CardTitle
title={i18n.t("Categories assigned to {{ saleName }}", { title={i18n.t("Eligible Categories")}
saleName: maybe(() => sale.name)
})}
toolbar={ toolbar={
<Button color="primary" onClick={onCategoryAssign}> <Button color="primary" onClick={onCategoryAssign}>
{i18n.t("Assign categories")} {i18n.t("Assign categories")}
@ -82,6 +83,7 @@ const DiscountCategories = withStyles(styles, {
/> />
<Table> <Table>
<TableHead <TableHead
colSpan={numberOfColumns}
selected={selected} selected={selected}
disabled={disabled} disabled={disabled}
items={maybe(() => sale.categories.edges.map(edge => edge.node))} items={maybe(() => sale.categories.edges.map(edge => edge.node))}
@ -89,7 +91,6 @@ const DiscountCategories = withStyles(styles, {
toolbar={toolbar} toolbar={toolbar}
> >
<> <>
<TableCell />
<TableCell className={classes.wideColumn}> <TableCell className={classes.wideColumn}>
{i18n.t("Category name")} {i18n.t("Category name")}
</TableCell> </TableCell>
@ -102,7 +103,7 @@ const DiscountCategories = withStyles(styles, {
<TableFooter> <TableFooter>
<TableRow> <TableRow>
<TablePagination <TablePagination
colSpan={4} colSpan={numberOfColumns}
hasNextPage={pageInfo && !disabled ? pageInfo.hasNextPage : false} hasNextPage={pageInfo && !disabled ? pageInfo.hasNextPage : false}
onNextPage={onNextPage} onNextPage={onNextPage}
hasPreviousPage={ hasPreviousPage={
@ -130,6 +131,7 @@ const DiscountCategories = withStyles(styles, {
<Checkbox <Checkbox
checked={isSelected} checked={isSelected}
disabled={disabled} disabled={disabled}
disableClickPropagation
onChange={() => toggle(category.id)} onChange={() => toggle(category.id)}
/> />
</TableCell> </TableCell>
@ -158,7 +160,7 @@ const DiscountCategories = withStyles(styles, {
}, },
() => ( () => (
<TableRow> <TableRow>
<TableCell colSpan={4}> <TableCell colSpan={numberOfColumns}>
{i18n.t("No categories found")} {i18n.t("No categories found")}
</TableCell> </TableCell>
</TableRow> </TableRow>

View file

@ -50,6 +50,9 @@ const styles = (theme: Theme) =>
width: "60%" width: "60%"
} }
}); });
const numberOfColumns = 4;
const DiscountCollections = withStyles(styles, { const DiscountCollections = withStyles(styles, {
name: "DiscountCollections" name: "DiscountCollections"
})( })(
@ -71,9 +74,7 @@ const DiscountCollections = withStyles(styles, {
}: DiscountCollectionsProps & WithStyles<typeof styles>) => ( }: DiscountCollectionsProps & WithStyles<typeof styles>) => (
<Card> <Card>
<CardTitle <CardTitle
title={i18n.t("Collections assigned to {{ saleName }}", { title={i18n.t("Eligible Collections")}
saleName: maybe(() => sale.name)
})}
toolbar={ toolbar={
<Button color="primary" onClick={onCollectionAssign}> <Button color="primary" onClick={onCollectionAssign}>
{i18n.t("Assign collections")} {i18n.t("Assign collections")}
@ -82,6 +83,7 @@ const DiscountCollections = withStyles(styles, {
/> />
<Table> <Table>
<TableHead <TableHead
colSpan={numberOfColumns}
selected={selected} selected={selected}
disabled={disabled} disabled={disabled}
items={maybe(() => sale.collections.edges.map(edge => edge.node))} items={maybe(() => sale.collections.edges.map(edge => edge.node))}
@ -94,11 +96,12 @@ const DiscountCollections = withStyles(styles, {
<TableCell className={classes.textRight}> <TableCell className={classes.textRight}>
{i18n.t("Products")} {i18n.t("Products")}
</TableCell> </TableCell>
<TableCell />
</TableHead> </TableHead>
<TableFooter> <TableFooter>
<TableRow> <TableRow>
<TablePagination <TablePagination
colSpan={4} colSpan={numberOfColumns}
hasNextPage={pageInfo && !disabled ? pageInfo.hasNextPage : false} hasNextPage={pageInfo && !disabled ? pageInfo.hasNextPage : false}
onNextPage={onNextPage} onNextPage={onNextPage}
hasPreviousPage={ hasPreviousPage={
@ -125,6 +128,7 @@ const DiscountCollections = withStyles(styles, {
<Checkbox <Checkbox
checked={isSelected} checked={isSelected}
disabled={disabled} disabled={disabled}
disableClickPropagation
onChange={() => toggle(collection.id)} onChange={() => toggle(collection.id)}
/> />
</TableCell> </TableCell>
@ -156,7 +160,7 @@ const DiscountCollections = withStyles(styles, {
}, },
() => ( () => (
<TableRow> <TableRow>
<TableCell colSpan={4}> <TableCell colSpan={numberOfColumns}>
{i18n.t("No collections found")} {i18n.t("No collections found")}
</TableCell> </TableCell>
</TableRow> </TableRow>

View file

@ -19,7 +19,9 @@ import CardTitle from "@saleor/components/CardTitle";
import Checkbox from "@saleor/components/Checkbox"; import Checkbox from "@saleor/components/Checkbox";
import Skeleton from "@saleor/components/Skeleton"; import Skeleton from "@saleor/components/Skeleton";
import StatusLabel from "@saleor/components/StatusLabel"; import StatusLabel from "@saleor/components/StatusLabel";
import TableCellAvatar from "@saleor/components/TableCellAvatar"; import TableCellAvatar, {
AVATAR_MARGIN
} from "@saleor/components/TableCellAvatar";
import TableHead from "@saleor/components/TableHead"; import TableHead from "@saleor/components/TableHead";
import TablePagination from "@saleor/components/TablePagination"; import TablePagination from "@saleor/components/TablePagination";
import i18n from "../../../i18n"; import i18n from "../../../i18n";
@ -36,28 +38,34 @@ export interface SaleProductsProps extends ListProps, ListActions {
const styles = (theme: Theme) => const styles = (theme: Theme) =>
createStyles({ createStyles({
[theme.breakpoints.up("lg")]: { colActions: {
colName: {},
colPublished: {
width: 150
},
colType: {
width: 200
}
},
colName: {},
colPublished: {},
colType: {},
iconCell: {
"&:last-child": { "&:last-child": {
paddingRight: 0 paddingRight: 0
}, },
width: 48 + theme.spacing.unit / 2 width: 48 + theme.spacing.unit / 2
}, },
colName: {
width: "auto"
},
colNameLabel: {
marginLeft: AVATAR_MARGIN
},
colPublished: {
width: 150
},
colType: {
width: 200
},
table: {
tableLayout: "fixed"
},
tableRow: { tableRow: {
cursor: "pointer" cursor: "pointer"
} }
}); });
const numberOfColumns = 5;
const DiscountProducts = withStyles(styles, { const DiscountProducts = withStyles(styles, {
name: "DiscountProducts" name: "DiscountProducts"
})( })(
@ -79,9 +87,7 @@ const DiscountProducts = withStyles(styles, {
}: SaleProductsProps & WithStyles<typeof styles>) => ( }: SaleProductsProps & WithStyles<typeof styles>) => (
<Card> <Card>
<CardTitle <CardTitle
title={i18n.t("Products assigned to {{ saleName }}", { title={i18n.t("Eligible Products")}
saleName: maybe(() => sale.name)
})}
toolbar={ toolbar={
<Button color="primary" onClick={onProductAssign}> <Button color="primary" onClick={onProductAssign}>
{i18n.t("Assign products")} {i18n.t("Assign products")}
@ -90,16 +96,17 @@ const DiscountProducts = withStyles(styles, {
/> />
<Table> <Table>
<TableHead <TableHead
colSpan={numberOfColumns}
selected={selected} selected={selected}
disabled={disabled} disabled={disabled}
items={maybe(() => sale.products.edges.map(edge => edge.node))} items={maybe(() => sale.products.edges.map(edge => edge.node))}
toggleAll={toggleAll} toggleAll={toggleAll}
toolbar={toolbar} toolbar={toolbar}
> >
<TableCell />
<TableCell />
<TableCell className={classes.colName}> <TableCell className={classes.colName}>
{i18n.t("Product name")} <span className={classes.colNameLabel}>
{i18n.t("Product name")}
</span>
</TableCell> </TableCell>
<TableCell className={classes.colType}> <TableCell className={classes.colType}>
{i18n.t("Product Type")} {i18n.t("Product Type")}
@ -112,7 +119,7 @@ const DiscountProducts = withStyles(styles, {
<TableFooter> <TableFooter>
<TableRow> <TableRow>
<TablePagination <TablePagination
colSpan={6} colSpan={numberOfColumns}
hasNextPage={pageInfo && !disabled ? pageInfo.hasNextPage : false} hasNextPage={pageInfo && !disabled ? pageInfo.hasNextPage : false}
onNextPage={onNextPage} onNextPage={onNextPage}
hasPreviousPage={ hasPreviousPage={
@ -139,15 +146,16 @@ const DiscountProducts = withStyles(styles, {
<Checkbox <Checkbox
checked={isSelected} checked={isSelected}
disabled={disabled} disabled={disabled}
disableClickPropagation
onChange={() => toggle(product.id)} onChange={() => toggle(product.id)}
/> />
</TableCell> </TableCell>
<TableCellAvatar <TableCellAvatar
className={classes.colName}
thumbnail={maybe(() => product.thumbnail.url)} thumbnail={maybe(() => product.thumbnail.url)}
/> >
<TableCell className={classes.colName}>
{maybe<React.ReactNode>(() => product.name, <Skeleton />)} {maybe<React.ReactNode>(() => product.name, <Skeleton />)}
</TableCell> </TableCellAvatar>
<TableCell className={classes.colType}> <TableCell className={classes.colType}>
{maybe<React.ReactNode>( {maybe<React.ReactNode>(
() => product.productType.name, () => product.productType.name,
@ -170,7 +178,7 @@ const DiscountProducts = withStyles(styles, {
<Skeleton /> <Skeleton />
)} )}
</TableCell> </TableCell>
<TableCell className={classes.iconCell}> <TableCell className={classes.colActions}>
<IconButton <IconButton
disabled={!product || disabled} disabled={!product || disabled}
onClick={event => { onClick={event => {
@ -186,7 +194,9 @@ const DiscountProducts = withStyles(styles, {
}, },
() => ( () => (
<TableRow> <TableRow>
<TableCell colSpan={6}>{i18n.t("No products found")}</TableCell> <TableCell colSpan={numberOfColumns}>
{i18n.t("No products found")}
</TableCell>
</TableRow> </TableRow>
) )
)} )}

View file

@ -19,10 +19,10 @@ import Percent from "@saleor/components/Percent";
import Skeleton from "@saleor/components/Skeleton"; import Skeleton from "@saleor/components/Skeleton";
import TableHead from "@saleor/components/TableHead"; import TableHead from "@saleor/components/TableHead";
import TablePagination from "@saleor/components/TablePagination"; import TablePagination from "@saleor/components/TablePagination";
import i18n from "../../../i18n"; import i18n from "@saleor/i18n";
import { maybe, renderCollection } from "../../../misc"; import { maybe, renderCollection } from "@saleor/misc";
import { ListActions, ListProps } from "../../../types"; import { ListActions, ListProps } from "@saleor/types";
import { SaleType } from "../../../types/globalTypes"; import { SaleType } from "@saleor/types/globalTypes";
import { SaleList_sales_edges_node } from "../../types/SaleList"; import { SaleList_sales_edges_node } from "../../types/SaleList";
export interface SaleListProps extends ListProps, ListActions { export interface SaleListProps extends ListProps, ListActions {
@ -59,15 +59,19 @@ const styles = (theme: Theme) =>
} }
}); });
const numberOfColumns = 5;
const SaleList = withStyles(styles, { const SaleList = withStyles(styles, {
name: "SaleList" name: "SaleList"
})( })(
({ ({
classes, classes,
settings,
defaultCurrency, defaultCurrency,
disabled, disabled,
onNextPage, onNextPage,
onPreviousPage, onPreviousPage,
onUpdateListSettings,
onRowClick, onRowClick,
pageInfo, pageInfo,
sales, sales,
@ -80,6 +84,7 @@ const SaleList = withStyles(styles, {
<Card> <Card>
<Table> <Table>
<TableHead <TableHead
colSpan={numberOfColumns}
selected={selected} selected={selected}
disabled={disabled} disabled={disabled}
items={sales} items={sales}
@ -110,9 +115,11 @@ const SaleList = withStyles(styles, {
<TableFooter> <TableFooter>
<TableRow> <TableRow>
<TablePagination <TablePagination
colSpan={5} colSpan={numberOfColumns}
settings={settings}
hasNextPage={pageInfo && !disabled ? pageInfo.hasNextPage : false} hasNextPage={pageInfo && !disabled ? pageInfo.hasNextPage : false}
onNextPage={onNextPage} onNextPage={onNextPage}
onUpdateListSettings={onUpdateListSettings}
hasPreviousPage={ hasPreviousPage={
pageInfo && !disabled ? pageInfo.hasPreviousPage : false pageInfo && !disabled ? pageInfo.hasPreviousPage : false
} }
@ -138,6 +145,7 @@ const SaleList = withStyles(styles, {
<Checkbox <Checkbox
checked={isSelected} checked={isSelected}
disabled={disabled} disabled={disabled}
disableClickPropagation
onChange={() => toggle(sale.id)} onChange={() => toggle(sale.id)}
/> />
</TableCell> </TableCell>
@ -184,7 +192,9 @@ const SaleList = withStyles(styles, {
}, },
() => ( () => (
<TableRow> <TableRow>
<TableCell colSpan={5}>{i18n.t("No sales found")}</TableCell> <TableCell colSpan={numberOfColumns}>
{i18n.t("No sales found")}
</TableCell>
</TableRow> </TableRow>
) )
)} )}

View file

@ -4,8 +4,8 @@ import React from "react";
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 i18n from "../../../i18n"; import i18n from "@saleor/i18n";
import { ListActions, PageListProps } from "../../../types"; import { ListActions, PageListProps } from "@saleor/types";
import { SaleList_sales_edges_node } from "../../types/SaleList"; import { SaleList_sales_edges_node } from "../../types/SaleList";
import SaleList from "../SaleList"; import SaleList from "../SaleList";

View file

@ -96,6 +96,9 @@ const SalePricing = withStyles(styles, {
label={i18n.t("Start Date")} label={i18n.t("Start Date")}
value={data.startDate} value={data.startDate}
type="date" type="date"
InputLabelProps={{
shrink: true
}}
fullWidth fullWidth
/> />
<TextField <TextField
@ -107,6 +110,9 @@ const SalePricing = withStyles(styles, {
label={i18n.t("End Date")} label={i18n.t("End Date")}
value={data.endDate} value={data.endDate}
type="date" type="date"
InputLabelProps={{
shrink: true
}}
fullWidth fullWidth
/> />
</CardContent> </CardContent>

View file

@ -11,22 +11,33 @@ import SaveButtonBar from "@saleor/components/SaveButtonBar";
import i18n from "../../../i18n"; import i18n from "../../../i18n";
import { UserError } from "../../../types"; import { UserError } from "../../../types";
import { import {
VoucherDiscountValueType, DiscountValueTypeEnum,
VoucherType VoucherTypeEnum
} from "../../../types/globalTypes"; } from "../../../types/globalTypes";
import { RequirementsPicker } from "../../types";
import VoucherDates from "../VoucherDates";
import VoucherInfo from "../VoucherInfo"; import VoucherInfo from "../VoucherInfo";
import VoucherOptions from "../VoucherOptions"; import VoucherLimits from "../VoucherLimits";
import VoucherRequirements from "../VoucherRequirements";
import VoucherTypes from "../VoucherTypes";
import VoucherValue from "../VoucherValue";
export interface FormData { export interface FormData {
applyOncePerCustomer: boolean;
applyOncePerOrder: boolean; applyOncePerOrder: boolean;
code: string; code: string;
discountType: VoucherDiscountValueType; discountType: DiscountValueTypeEnum;
endDate: string; endDate: string;
minAmountSpent: number; endTime: string;
name: string; hasEndDate: boolean;
hasUsageLimit: boolean;
minAmountSpent: string;
minCheckoutItemsQuantity: string;
requirementsPicker: RequirementsPicker;
startDate: string; startDate: string;
type: VoucherType; startTime: string;
usageLimit: number; type: VoucherTypeEnum;
usageLimit: string;
value: number; value: number;
} }
@ -48,15 +59,21 @@ const VoucherCreatePage: React.StatelessComponent<VoucherCreatePageProps> = ({
onSubmit onSubmit
}) => { }) => {
const initialForm: FormData = { const initialForm: FormData = {
applyOncePerCustomer: false,
applyOncePerOrder: false, applyOncePerOrder: false,
code: "", code: "",
discountType: VoucherDiscountValueType.FIXED, discountType: DiscountValueTypeEnum.FIXED,
endDate: "", endDate: "",
minAmountSpent: 0, endTime: "",
name: "", hasEndDate: false,
hasUsageLimit: false,
minAmountSpent: "0",
minCheckoutItemsQuantity: "0",
requirementsPicker: RequirementsPicker.NONE,
startDate: "", startDate: "",
type: VoucherType.VALUE, startTime: "",
usageLimit: 0, type: VoucherTypeEnum.ENTIRE_ORDER,
usageLimit: "0",
value: 0 value: 0
}; };
@ -72,11 +89,28 @@ const VoucherCreatePage: React.StatelessComponent<VoucherCreatePageProps> = ({
data={data} data={data}
errors={formErrors} errors={formErrors}
disabled={disabled} disabled={disabled}
variant="create"
onChange={change} onChange={change}
variant="create"
/> />
<CardSpacer /> <CardSpacer />
<VoucherOptions <VoucherTypes
data={data}
disabled={disabled}
errors={formErrors}
onChange={change}
/>
{data.discountType.toString() !== "SHIPPING" ? (
<VoucherValue
data={data}
disabled={disabled}
defaultCurrency={defaultCurrency}
errors={formErrors}
onChange={change}
variant="create"
/>
) : null}
<CardSpacer />
<VoucherRequirements
data={data} data={data}
disabled={disabled} disabled={disabled}
defaultCurrency={defaultCurrency} defaultCurrency={defaultCurrency}
@ -84,6 +118,21 @@ const VoucherCreatePage: React.StatelessComponent<VoucherCreatePageProps> = ({
onChange={change} onChange={change}
/> />
<CardSpacer /> <CardSpacer />
<VoucherLimits
data={data}
disabled={disabled}
defaultCurrency={defaultCurrency}
errors={formErrors}
onChange={change}
/>
<CardSpacer />
<VoucherDates
data={data}
disabled={disabled}
defaultCurrency={defaultCurrency}
errors={formErrors}
onChange={change}
/>
</div> </div>
</Grid> </Grid>
<SaveButtonBar <SaveButtonBar

View file

@ -11,26 +11,32 @@ import Grid from "@saleor/components/Grid";
import PageHeader from "@saleor/components/PageHeader"; import PageHeader from "@saleor/components/PageHeader";
import SaveButtonBar from "@saleor/components/SaveButtonBar"; import SaveButtonBar from "@saleor/components/SaveButtonBar";
import { Tab, TabContainer } from "@saleor/components/Tab"; import { Tab, TabContainer } from "@saleor/components/Tab";
import { RequirementsPicker } from "@saleor/discounts/types";
import i18n from "../../../i18n"; import i18n from "../../../i18n";
import { maybe } from "../../../misc"; import { maybe, splitDateTime } from "../../../misc";
import { ListProps, TabListActions, UserError } from "../../../types"; import { ListProps, TabListActions, UserError } from "../../../types";
import { import {
VoucherDiscountValueType, DiscountValueTypeEnum,
VoucherType VoucherTypeEnum
} from "../../../types/globalTypes"; } from "../../../types/globalTypes";
import { VoucherDetails_voucher } from "../../types/VoucherDetails"; import { VoucherDetails_voucher } from "../../types/VoucherDetails";
import DiscountCategories from "../DiscountCategories"; import DiscountCategories from "../DiscountCategories";
import DiscountCollections from "../DiscountCollections"; import DiscountCollections from "../DiscountCollections";
import DiscountProducts from "../DiscountProducts"; import DiscountProducts from "../DiscountProducts";
import VoucherDates from "../VoucherDates";
import VoucherInfo from "../VoucherInfo"; import VoucherInfo from "../VoucherInfo";
import VoucherOptions from "../VoucherOptions"; import VoucherLimits from "../VoucherLimits";
import VoucherRequirements from "../VoucherRequirements";
import VoucherSummary from "../VoucherSummary"; import VoucherSummary from "../VoucherSummary";
import VoucherTypes from "../VoucherTypes";
import VoucherValue from "../VoucherValue";
export enum VoucherDetailsPageTab { export enum VoucherDetailsPageTab {
categories = "categories", categories = "categories",
collections = "collections", collections = "collections",
products = "products" products = "products"
} }
export function voucherDetailsPageTab(tab: string): VoucherDetailsPageTab { export function voucherDetailsPageTab(tab: string): VoucherDetailsPageTab {
return tab === VoucherDetailsPageTab.products return tab === VoucherDetailsPageTab.products
? VoucherDetailsPageTab.products ? VoucherDetailsPageTab.products
@ -40,15 +46,21 @@ export function voucherDetailsPageTab(tab: string): VoucherDetailsPageTab {
} }
export interface FormData { export interface FormData {
applyOncePerCustomer: boolean;
applyOncePerOrder: boolean; applyOncePerOrder: boolean;
code: string; code: string;
discountType: VoucherDiscountValueType; discountType: DiscountValueTypeEnum;
endDate: string; endDate: string;
minAmountSpent: number; endTime: string;
name: string; hasEndDate: boolean;
hasUsageLimit: boolean;
minAmountSpent: string;
minCheckoutItemsQuantity: string;
requirementsPicker: RequirementsPicker;
startDate: string; startDate: string;
type: VoucherType; startTime: string;
usageLimit: number; type: VoucherTypeEnum;
usageLimit: string;
value: number; value: number;
} }
@ -116,19 +128,37 @@ const VoucherDetailsPage: React.StatelessComponent<VoucherDetailsPageProps> = ({
collectionListToolbar, collectionListToolbar,
productListToolbar productListToolbar
}) => { }) => {
let requirementsPickerInitValue;
if (maybe(() => voucher.minAmountSpent.amount) > 0) {
requirementsPickerInitValue = RequirementsPicker.ORDER;
} else if (maybe(() => voucher.minCheckoutItemsQuantity) > 0) {
requirementsPickerInitValue = RequirementsPicker.ITEM;
} else {
requirementsPickerInitValue = RequirementsPicker.NONE;
}
const initialForm: FormData = { const initialForm: FormData = {
applyOncePerCustomer: maybe(() => voucher.applyOncePerCustomer, false),
applyOncePerOrder: maybe(() => voucher.applyOncePerOrder, false), applyOncePerOrder: maybe(() => voucher.applyOncePerOrder, false),
code: maybe(() => voucher.code, ""), code: maybe(() => voucher.code, ""),
discountType: maybe( discountType: maybe(
() => voucher.discountValueType, () => voucher.discountValueType,
VoucherDiscountValueType.FIXED DiscountValueTypeEnum.FIXED
), ),
endDate: maybe(() => voucher.endDate, ""), endDate: splitDateTime(maybe(() => voucher.endDate, "")).date,
minAmountSpent: maybe(() => voucher.minAmountSpent.amount, 0), endTime: splitDateTime(maybe(() => voucher.endDate, "")).time,
name: maybe(() => voucher.name, ""), hasEndDate: maybe(() => !!voucher.endDate),
startDate: maybe(() => voucher.startDate, ""), hasUsageLimit: maybe(() => !!voucher.usageLimit),
type: maybe(() => voucher.type, VoucherType.VALUE), minAmountSpent: maybe(() => voucher.minAmountSpent.amount.toString(), "0"),
usageLimit: maybe(() => voucher.usageLimit || 0, 0), minCheckoutItemsQuantity: maybe(
() => voucher.minCheckoutItemsQuantity.toString(),
"0"
),
requirementsPicker: requirementsPickerInitValue,
startDate: splitDateTime(maybe(() => voucher.startDate, "")).date,
startTime: splitDateTime(maybe(() => voucher.startDate, "")).time,
type: maybe(() => voucher.type, VoucherTypeEnum.ENTIRE_ORDER),
usageLimit: maybe(() => voucher.usageLimit.toString(), "0"),
value: maybe(() => voucher.discountValue, 0) value: maybe(() => voucher.discountValue, 0)
}; };
@ -137,28 +167,37 @@ const VoucherDetailsPage: React.StatelessComponent<VoucherDetailsPageProps> = ({
{({ change, data, errors: formErrors, hasChanged, submit }) => ( {({ change, data, errors: formErrors, hasChanged, submit }) => (
<Container> <Container>
<AppHeader onBack={onBack}>{i18n.t("Vouchers")}</AppHeader> <AppHeader onBack={onBack}>{i18n.t("Vouchers")}</AppHeader>
<PageHeader title={maybe(() => voucher.name)} /> <PageHeader title={maybe(() => voucher.code)} />
<Grid> <Grid>
<div> <div>
<VoucherInfo <VoucherInfo
data={data} data={data}
disabled={disabled} disabled={disabled}
errors={formErrors} errors={formErrors}
variant="update"
onChange={change} onChange={change}
variant="update"
/> />
<CardSpacer /> <CardSpacer />
<VoucherOptions <VoucherTypes
data={data} data={data}
disabled={disabled} disabled={disabled}
defaultCurrency={defaultCurrency}
errors={formErrors} errors={formErrors}
onChange={change} onChange={change}
/> />
<CardSpacer /> <CardSpacer />
{data.type === VoucherType.CATEGORY || {data.discountType.toString() !== "SHIPPING" ? (
data.type === VoucherType.COLLECTION || <VoucherValue
data.type === VoucherType.PRODUCT ? ( data={data}
disabled={disabled}
defaultCurrency={defaultCurrency}
errors={formErrors}
onChange={change}
variant="update"
/>
) : null}
<CardSpacer />
{data.type === VoucherTypeEnum.SPECIFIC_PRODUCT &&
data.discountType.toString() !== "SHIPPING" ? (
<> <>
<TabContainer> <TabContainer>
<CategoriesTab <CategoriesTab
@ -246,16 +285,16 @@ const VoucherDetailsPage: React.StatelessComponent<VoucherDetailsPageProps> = ({
/> />
)} )}
</> </>
) : data.type === VoucherType.SHIPPING ? ( ) : null}
<CardSpacer />
{data.discountType.toString() === "SHIPPING" ? (
<CountryList <CountryList
countries={maybe(() => voucher.countries)} countries={maybe(() => voucher.countries)}
disabled={disabled} disabled={disabled}
emptyText={i18n.t("Voucher applies to all countries")} emptyText={i18n.t("Voucher applies to all countries")}
title={ title={
<> <>
{i18n.t("Countries assigned to {{ voucherName }}", { {i18n.t("Countries")}
voucherName: maybe(() => voucher.name)
})}
<Typography variant="caption"> <Typography variant="caption">
{i18n.t("Vouchers limited to these countries")} {i18n.t("Vouchers limited to these countries")}
</Typography> </Typography>
@ -265,6 +304,30 @@ const VoucherDetailsPage: React.StatelessComponent<VoucherDetailsPageProps> = ({
onCountryUnassign={onCountryUnassign} onCountryUnassign={onCountryUnassign}
/> />
) : null} ) : null}
<CardSpacer />
<VoucherRequirements
data={data}
disabled={disabled}
defaultCurrency={defaultCurrency}
errors={formErrors}
onChange={change}
/>
<CardSpacer />
<VoucherLimits
data={data}
disabled={disabled}
defaultCurrency={defaultCurrency}
errors={formErrors}
onChange={change}
/>
<CardSpacer />
<VoucherDates
data={data}
disabled={disabled}
defaultCurrency={defaultCurrency}
errors={formErrors}
onChange={change}
/>
</div> </div>
<div> <div>
<VoucherSummary <VoucherSummary

View file

@ -1,101 +1,63 @@
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 {
createStyles,
Theme,
withStyles,
WithStyles
} from "@material-ui/core/styles";
import TextField from "@material-ui/core/TextField"; import TextField from "@material-ui/core/TextField";
import React from "react"; import React from "react";
import Button from "@material-ui/core/Button";
import CardTitle from "@saleor/components/CardTitle"; import CardTitle from "@saleor/components/CardTitle";
import FormSpacer from "@saleor/components/FormSpacer";
import SingleSelectField from "@saleor/components/SingleSelectField";
import i18n from "../../../i18n"; import i18n from "../../../i18n";
import { generateCode } from "../../../misc";
import { FormErrors } from "../../../types"; import { FormErrors } from "../../../types";
import { VoucherType } from "../../../types/globalTypes";
import { translateVoucherTypes } from "../../translations";
import { FormData } from "../VoucherDetailsPage"; import { FormData } from "../VoucherDetailsPage";
interface VoucherInfoProps { interface VoucherInfoProps {
data: FormData; data: FormData;
errors: FormErrors<"name" | "code" | "type">; errors: FormErrors<"code">;
disabled: boolean; disabled: boolean;
variant: "create" | "update"; variant: "create" | "update";
onChange: (event: React.ChangeEvent<any>) => void; onChange: (event: any) => void;
} }
const styles = (theme: Theme) => const VoucherInfo = ({
createStyles({ data,
nameInput: { disabled,
gridColumnEnd: "span 2" errors,
}, variant,
root: { onChange
display: "grid", }: VoucherInfoProps) => {
gridColumnGap: theme.spacing.unit * 2 + "px", const onGenerateCode = () =>
gridTemplateColumns: "1fr 1fr" onChange({
} target: {
}); name: "code",
value: generateCode(10)
}
});
const VoucherInfo = withStyles(styles, { return (
name: "VoucherInfo" <Card>
})( <CardTitle
({ title={i18n.t("General Information")}
classes, toolbar={
data, variant === "create" && (
disabled, <Button color="primary" onClick={onGenerateCode}>
errors, {i18n.t("Generate Code")}
variant, </Button>
onChange )
}: VoucherInfoProps & WithStyles<typeof styles>) => { }
const translatedVoucherTypes = translateVoucherTypes(); />
const voucherTypeChoices = Object.values(VoucherType).map(type => ({ <CardContent>
label: translatedVoucherTypes[type], <TextField
value: type disabled={variant === "update" || disabled}
})); error={!!errors.code}
fullWidth
return ( helperText={errors.code}
<Card> name={"code" as keyof FormData}
<CardTitle title={i18n.t("General Information")} /> label={i18n.t("Discount Code")}
<CardContent> value={data.code}
<TextField onChange={onChange}
className={classes.nameInput} />
disabled={disabled} </CardContent>
error={!!errors.name} </Card>
fullWidth );
helperText={errors.name} };
name={"name" as keyof FormData}
label={i18n.t("Name")}
value={data.name}
onChange={onChange}
/>
<FormSpacer />
<div className={classes.root}>
<TextField
disabled={disabled}
error={!!errors.code}
fullWidth
helperText={errors.code}
name={"code" as keyof FormData}
label={i18n.t("Discount Code")}
value={data.code}
onChange={onChange}
/>
<SingleSelectField
choices={voucherTypeChoices}
disabled={disabled || variant === "update"}
error={!!errors.type}
hint={errors.type}
name={"type" as keyof FormData}
label={i18n.t("Type of Discount")}
value={data.type}
onChange={onChange}
/>
</div>
</CardContent>
</Card>
);
}
);
export default VoucherInfo; export default VoucherInfo;

View file

@ -19,10 +19,10 @@ import Percent from "@saleor/components/Percent";
import Skeleton from "@saleor/components/Skeleton"; import Skeleton from "@saleor/components/Skeleton";
import TableHead from "@saleor/components/TableHead"; import TableHead from "@saleor/components/TableHead";
import TablePagination from "@saleor/components/TablePagination"; import TablePagination from "@saleor/components/TablePagination";
import i18n from "../../../i18n"; import i18n from "@saleor/i18n";
import { maybe, renderCollection } from "../../../misc"; import { maybe, renderCollection } from "@saleor/misc";
import { ListActions, ListProps } from "../../../types"; import { ListActions, ListProps } from "@saleor/types";
import { VoucherDiscountValueType } from "../../../types/globalTypes"; import { DiscountValueTypeEnum } from "@saleor/types/globalTypes";
import { VoucherList_vouchers_edges_node } from "../../types/VoucherList"; import { VoucherList_vouchers_edges_node } from "../../types/VoucherList";
export interface VoucherListProps extends ListProps, ListActions { export interface VoucherListProps extends ListProps, ListActions {
@ -74,15 +74,19 @@ const styles = (theme: Theme) =>
} }
}); });
const numberOfColumns = 7;
const VoucherList = withStyles(styles, { const VoucherList = withStyles(styles, {
name: "VoucherList" name: "VoucherList"
})( })(
({ ({
classes, classes,
settings,
defaultCurrency, defaultCurrency,
disabled, disabled,
onNextPage, onNextPage,
onPreviousPage, onPreviousPage,
onUpdateListSettings,
onRowClick, onRowClick,
pageInfo, pageInfo,
vouchers, vouchers,
@ -95,6 +99,7 @@ const VoucherList = withStyles(styles, {
<Card> <Card>
<Table> <Table>
<TableHead <TableHead
colSpan={numberOfColumns}
selected={selected} selected={selected}
disabled={disabled} disabled={disabled}
items={vouchers} items={vouchers}
@ -102,7 +107,7 @@ const VoucherList = withStyles(styles, {
toolbar={toolbar} toolbar={toolbar}
> >
<TableCell className={classes.colName}> <TableCell className={classes.colName}>
{i18n.t("Name", { {i18n.t("Code", {
context: "voucher list table header" context: "voucher list table header"
})} })}
</TableCell> </TableCell>
@ -135,9 +140,11 @@ const VoucherList = withStyles(styles, {
<TableFooter> <TableFooter>
<TableRow> <TableRow>
<TablePagination <TablePagination
colSpan={7} colSpan={numberOfColumns}
settings={settings}
hasNextPage={pageInfo && !disabled ? pageInfo.hasNextPage : false} hasNextPage={pageInfo && !disabled ? pageInfo.hasNextPage : false}
onNextPage={onNextPage} onNextPage={onNextPage}
onUpdateListSettings={onUpdateListSettings}
hasPreviousPage={ hasPreviousPage={
pageInfo && !disabled ? pageInfo.hasPreviousPage : false pageInfo && !disabled ? pageInfo.hasPreviousPage : false
} }
@ -163,11 +170,12 @@ const VoucherList = withStyles(styles, {
<Checkbox <Checkbox
checked={isSelected} checked={isSelected}
disabled={disabled} disabled={disabled}
disableClickPropagation
onChange={() => toggle(voucher.id)} onChange={() => toggle(voucher.id)}
/> />
</TableCell> </TableCell>
<TableCell className={classes.colName}> <TableCell className={classes.colName}>
{maybe<React.ReactNode>(() => voucher.name, <Skeleton />)} {maybe<React.ReactNode>(() => voucher.code, <Skeleton />)}
</TableCell> </TableCell>
<TableCell className={classes.colMinSpent}> <TableCell className={classes.colMinSpent}>
{voucher && voucher.minAmountSpent ? ( {voucher && voucher.minAmountSpent ? (
@ -202,7 +210,7 @@ const VoucherList = withStyles(styles, {
voucher.discountValueType && voucher.discountValueType &&
voucher.discountValue ? ( voucher.discountValue ? (
voucher.discountValueType === voucher.discountValueType ===
VoucherDiscountValueType.FIXED ? ( DiscountValueTypeEnum.FIXED ? (
<Money <Money
money={{ money={{
amount: voucher.discountValue, amount: voucher.discountValue,
@ -230,7 +238,9 @@ const VoucherList = withStyles(styles, {
}, },
() => ( () => (
<TableRow> <TableRow>
<TableCell colSpan={7}>{i18n.t("No vouchers found")}</TableCell> <TableCell colSpan={numberOfColumns}>
{i18n.t("No vouchers found")}
</TableCell>
</TableRow> </TableRow>
) )
)} )}

View file

@ -4,8 +4,8 @@ import React from "react";
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 i18n from "../../../i18n"; import i18n from "@saleor/i18n";
import { ListActions, PageListProps } from "../../../types"; import { ListActions, PageListProps } from "@saleor/types";
import { VoucherList_vouchers_edges_node } from "../../types/VoucherList"; import { VoucherList_vouchers_edges_node } from "../../types/VoucherList";
import VoucherList from "../VoucherList"; import VoucherList from "../VoucherList";
@ -17,9 +17,11 @@ export interface VoucherListPageProps extends PageListProps, ListActions {
const VoucherListPage: React.StatelessComponent<VoucherListPageProps> = ({ const VoucherListPage: React.StatelessComponent<VoucherListPageProps> = ({
defaultCurrency, defaultCurrency,
disabled, disabled,
settings,
onAdd, onAdd,
onNextPage, onNextPage,
onPreviousPage, onPreviousPage,
onUpdateListSettings,
onRowClick, onRowClick,
pageInfo, pageInfo,
vouchers, vouchers,
@ -38,9 +40,11 @@ const VoucherListPage: React.StatelessComponent<VoucherListPageProps> = ({
</PageHeader> </PageHeader>
<VoucherList <VoucherList
defaultCurrency={defaultCurrency} defaultCurrency={defaultCurrency}
settings={settings}
disabled={disabled} disabled={disabled}
onNextPage={onNextPage} onNextPage={onNextPage}
onPreviousPage={onPreviousPage} onPreviousPage={onPreviousPage}
onUpdateListSettings={onUpdateListSettings}
onRowClick={onRowClick} onRowClick={onRowClick}
pageInfo={pageInfo} pageInfo={pageInfo}
vouchers={vouchers} vouchers={vouchers}

View file

@ -13,7 +13,7 @@ import Percent from "@saleor/components/Percent";
import Skeleton from "@saleor/components/Skeleton"; import Skeleton from "@saleor/components/Skeleton";
import i18n from "../../../i18n"; import i18n from "../../../i18n";
import { maybe } from "../../../misc"; import { maybe } from "../../../misc";
import { VoucherDiscountValueType } from "../../../types/globalTypes"; import { DiscountValueTypeEnum } from "../../../types/globalTypes";
import { translateVoucherTypes } from "../../translations"; import { translateVoucherTypes } from "../../translations";
import { VoucherDetails_voucher } from "../../types/VoucherDetails"; import { VoucherDetails_voucher } from "../../types/VoucherDetails";
@ -32,9 +32,9 @@ const VoucherSummary: React.StatelessComponent<VoucherSummaryProps> = ({
<Card> <Card>
<CardTitle title={i18n.t("Summary")} /> <CardTitle title={i18n.t("Summary")} />
<CardContent> <CardContent>
<Typography variant="caption">{i18n.t("Name")}</Typography> <Typography variant="caption">{i18n.t("Code")}</Typography>
<Typography> <Typography>
{maybe<React.ReactNode>(() => voucher.name, <Skeleton />)} {maybe<React.ReactNode>(() => voucher.code, <Skeleton />)}
</Typography> </Typography>
<FormSpacer /> <FormSpacer />
@ -51,7 +51,7 @@ const VoucherSummary: React.StatelessComponent<VoucherSummaryProps> = ({
<Typography> <Typography>
{maybe<React.ReactNode>( {maybe<React.ReactNode>(
() => () =>
voucher.discountValueType === VoucherDiscountValueType.FIXED ? ( voucher.discountValueType === DiscountValueTypeEnum.FIXED ? (
<Money <Money
money={{ money={{
amount: voucher.discountValue, amount: voucher.discountValue,

View file

@ -1,8 +1,8 @@
import placeholderImage from "@assets/images/placeholder60x60.png"; import placeholderImage from "@assets/images/placeholder60x60.png";
import { import {
DiscountValueTypeEnum,
SaleType, SaleType,
VoucherDiscountValueType, VoucherTypeEnum
VoucherType
} from "../types/globalTypes"; } from "../types/globalTypes";
import { SaleDetails_sale } from "./types/SaleDetails"; import { SaleDetails_sale } from "./types/SaleDetails";
import { SaleList_sales_edges_node } from "./types/SaleList"; import { SaleList_sales_edges_node } from "./types/SaleList";
@ -60,6 +60,7 @@ export const saleList: SaleList_sales_edges_node[] = [
export const voucherList: VoucherList_vouchers_edges_node[] = [ export const voucherList: VoucherList_vouchers_edges_node[] = [
{ {
__typename: "Voucher" as "Voucher", __typename: "Voucher" as "Voucher",
code: "FREE2019",
countries: [ countries: [
{ {
__typename: "CountryDisplay", __typename: "CountryDisplay",
@ -68,19 +69,20 @@ export const voucherList: VoucherList_vouchers_edges_node[] = [
} }
], ],
discountValue: 100, discountValue: 100,
discountValueType: "PERCENTAGE" as VoucherDiscountValueType, discountValueType: "PERCENTAGE" as DiscountValueTypeEnum,
endDate: null, endDate: null,
id: "Vm91Y2hlcjox", id: "Vm91Y2hlcjox",
minAmountSpent: null, minAmountSpent: null,
name: "Free shipping", minCheckoutItemsQuantity: null,
startDate: "2019-01-03", startDate: "2019-01-03",
usageLimit: null usageLimit: null
}, },
{ {
__typename: "Voucher" as "Voucher", __typename: "Voucher" as "Voucher",
code: "FREE2020",
countries: [], countries: [],
discountValue: 25, discountValue: 25,
discountValueType: "FIXED" as VoucherDiscountValueType, discountValueType: "FIXED" as DiscountValueTypeEnum,
endDate: null, endDate: null,
id: "Vm91Y2hlcjoy", id: "Vm91Y2hlcjoy",
minAmountSpent: { minAmountSpent: {
@ -88,7 +90,7 @@ export const voucherList: VoucherList_vouchers_edges_node[] = [
amount: 200, amount: 200,
currency: "USD" currency: "USD"
}, },
name: "Big order discount", minCheckoutItemsQuantity: 0,
startDate: "2019-01-03", startDate: "2019-01-03",
usageLimit: 150 usageLimit: 150
} }
@ -241,6 +243,7 @@ export const sale: SaleDetails_sale = {
export const voucherDetails: VoucherDetails_voucher = { export const voucherDetails: VoucherDetails_voucher = {
__typename: "Voucher", __typename: "Voucher",
applyOncePerCustomer: false,
applyOncePerOrder: false, applyOncePerOrder: false,
categories: { categories: {
__typename: "CategoryCountableConnection", __typename: "CategoryCountableConnection",
@ -275,7 +278,7 @@ export const voucherDetails: VoucherDetails_voucher = {
} }
], ],
discountValue: 25, discountValue: 25,
discountValueType: VoucherDiscountValueType.FIXED, discountValueType: DiscountValueTypeEnum.FIXED,
endDate: null, endDate: null,
id: "Vm91Y2hlcjoy", id: "Vm91Y2hlcjoy",
minAmountSpent: { minAmountSpent: {
@ -283,7 +286,7 @@ export const voucherDetails: VoucherDetails_voucher = {
amount: 200, amount: 200,
currency: "USD" currency: "USD"
}, },
name: "Big order discount", minCheckoutItemsQuantity: 0,
products: { products: {
__typename: "ProductCountableConnection", __typename: "ProductCountableConnection",
edges: [], edges: [],
@ -297,7 +300,7 @@ export const voucherDetails: VoucherDetails_voucher = {
totalCount: 0 totalCount: 0
}, },
startDate: "2018-11-27", startDate: "2018-11-27",
type: VoucherType.VALUE, type: VoucherTypeEnum.ENTIRE_ORDER,
usageLimit: null, usageLimit: null,
used: 0 used: 0
}; };

View file

@ -81,7 +81,7 @@ export const saleDetailsFragment = gql`
export const voucherFragment = gql` export const voucherFragment = gql`
fragment VoucherFragment on Voucher { fragment VoucherFragment on Voucher {
id id
name code
startDate startDate
endDate endDate
usageLimit usageLimit
@ -95,6 +95,7 @@ export const voucherFragment = gql`
currency currency
amount amount
} }
minCheckoutItemsQuantity
} }
`; `;
@ -108,6 +109,7 @@ export const voucherDetailsFragment = gql`
usageLimit usageLimit
used used
applyOncePerOrder applyOncePerOrder
applyOncePerCustomer
products(after: $after, before: $before, first: $first, last: $last) { products(after: $after, before: $before, first: $first, last: $last) {
edges { edges {
node { node {

View file

@ -1,10 +1,8 @@
import i18n from "../i18n"; import i18n from "../i18n";
import { VoucherType } from "../types/globalTypes"; import { VoucherTypeEnum } from "../types/globalTypes";
export const translateVoucherTypes = () => ({ export const translateVoucherTypes = () => ({
[VoucherType.CATEGORY]: i18n.t("Selected Categories"), [VoucherTypeEnum.SHIPPING]: i18n.t("Shipment"),
[VoucherType.COLLECTION]: i18n.t("Selected Collections"), [VoucherTypeEnum.ENTIRE_ORDER]: i18n.t("Entire order"),
[VoucherType.PRODUCT]: i18n.t("Selected Products"), [VoucherTypeEnum.SPECIFIC_PRODUCT]: i18n.t("Specific Products")
[VoucherType.SHIPPING]: i18n.t("Shipment"),
[VoucherType.VALUE]: i18n.t("All Products")
}); });

View file

@ -2,7 +2,7 @@
/* eslint-disable */ /* eslint-disable */
// This file was automatically generated and should not be edited. // This file was automatically generated and should not be edited.
import { CatalogueInput, VoucherDiscountValueType, VoucherType } from "./../../types/globalTypes"; import { CatalogueInput, DiscountValueTypeEnum, VoucherTypeEnum } from "./../../types/globalTypes";
// ==================================================== // ====================================================
// GraphQL mutation operation: VoucherCataloguesAdd // GraphQL mutation operation: VoucherCataloguesAdd
@ -133,18 +133,19 @@ export interface VoucherCataloguesAdd_voucherCataloguesAdd_voucher_categories {
export interface VoucherCataloguesAdd_voucherCataloguesAdd_voucher { export interface VoucherCataloguesAdd_voucherCataloguesAdd_voucher {
__typename: "Voucher"; __typename: "Voucher";
id: string; id: string;
name: string | null; code: string;
startDate: any; startDate: any;
endDate: any | null; endDate: any | null;
usageLimit: number | null; usageLimit: number | null;
discountValueType: VoucherDiscountValueType; discountValueType: DiscountValueTypeEnum;
discountValue: number; discountValue: number;
countries: (VoucherCataloguesAdd_voucherCataloguesAdd_voucher_countries | null)[] | null; countries: (VoucherCataloguesAdd_voucherCataloguesAdd_voucher_countries | null)[] | null;
minAmountSpent: VoucherCataloguesAdd_voucherCataloguesAdd_voucher_minAmountSpent | null; minAmountSpent: VoucherCataloguesAdd_voucherCataloguesAdd_voucher_minAmountSpent | null;
type: VoucherType; minCheckoutItemsQuantity: number | null;
code: string; type: VoucherTypeEnum;
used: number; used: number;
applyOncePerOrder: boolean; applyOncePerOrder: boolean;
applyOncePerCustomer: boolean;
products: VoucherCataloguesAdd_voucherCataloguesAdd_voucher_products | null; products: VoucherCataloguesAdd_voucherCataloguesAdd_voucher_products | null;
collections: VoucherCataloguesAdd_voucherCataloguesAdd_voucher_collections | null; collections: VoucherCataloguesAdd_voucherCataloguesAdd_voucher_collections | null;
categories: VoucherCataloguesAdd_voucherCataloguesAdd_voucher_categories | null; categories: VoucherCataloguesAdd_voucherCataloguesAdd_voucher_categories | null;

View file

@ -2,7 +2,7 @@
/* eslint-disable */ /* eslint-disable */
// This file was automatically generated and should not be edited. // This file was automatically generated and should not be edited.
import { CatalogueInput, VoucherDiscountValueType, VoucherType } from "./../../types/globalTypes"; import { CatalogueInput, DiscountValueTypeEnum, VoucherTypeEnum } from "./../../types/globalTypes";
// ==================================================== // ====================================================
// GraphQL mutation operation: VoucherCataloguesRemove // GraphQL mutation operation: VoucherCataloguesRemove
@ -133,18 +133,19 @@ export interface VoucherCataloguesRemove_voucherCataloguesRemove_voucher_categor
export interface VoucherCataloguesRemove_voucherCataloguesRemove_voucher { export interface VoucherCataloguesRemove_voucherCataloguesRemove_voucher {
__typename: "Voucher"; __typename: "Voucher";
id: string; id: string;
name: string | null; code: string;
startDate: any; startDate: any;
endDate: any | null; endDate: any | null;
usageLimit: number | null; usageLimit: number | null;
discountValueType: VoucherDiscountValueType; discountValueType: DiscountValueTypeEnum;
discountValue: number; discountValue: number;
countries: (VoucherCataloguesRemove_voucherCataloguesRemove_voucher_countries | null)[] | null; countries: (VoucherCataloguesRemove_voucherCataloguesRemove_voucher_countries | null)[] | null;
minAmountSpent: VoucherCataloguesRemove_voucherCataloguesRemove_voucher_minAmountSpent | null; minAmountSpent: VoucherCataloguesRemove_voucherCataloguesRemove_voucher_minAmountSpent | null;
type: VoucherType; minCheckoutItemsQuantity: number | null;
code: string; type: VoucherTypeEnum;
used: number; used: number;
applyOncePerOrder: boolean; applyOncePerOrder: boolean;
applyOncePerCustomer: boolean;
products: VoucherCataloguesRemove_voucherCataloguesRemove_voucher_products | null; products: VoucherCataloguesRemove_voucherCataloguesRemove_voucher_products | null;
collections: VoucherCataloguesRemove_voucherCataloguesRemove_voucher_collections | null; collections: VoucherCataloguesRemove_voucherCataloguesRemove_voucher_collections | null;
categories: VoucherCataloguesRemove_voucherCataloguesRemove_voucher_categories | null; categories: VoucherCataloguesRemove_voucherCataloguesRemove_voucher_categories | null;

View file

@ -2,7 +2,7 @@
/* eslint-disable */ /* eslint-disable */
// This file was automatically generated and should not be edited. // This file was automatically generated and should not be edited.
import { VoucherInput, VoucherDiscountValueType } from "./../../types/globalTypes"; import { VoucherInput, DiscountValueTypeEnum } from "./../../types/globalTypes";
// ==================================================== // ====================================================
// GraphQL mutation operation: VoucherCreate // GraphQL mutation operation: VoucherCreate
@ -29,14 +29,15 @@ export interface VoucherCreate_voucherCreate_voucher_minAmountSpent {
export interface VoucherCreate_voucherCreate_voucher { export interface VoucherCreate_voucherCreate_voucher {
__typename: "Voucher"; __typename: "Voucher";
id: string; id: string;
name: string | null; code: string;
startDate: any; startDate: any;
endDate: any | null; endDate: any | null;
usageLimit: number | null; usageLimit: number | null;
discountValueType: VoucherDiscountValueType; discountValueType: DiscountValueTypeEnum;
discountValue: number; discountValue: number;
countries: (VoucherCreate_voucherCreate_voucher_countries | null)[] | null; countries: (VoucherCreate_voucherCreate_voucher_countries | null)[] | null;
minAmountSpent: VoucherCreate_voucherCreate_voucher_minAmountSpent | null; minAmountSpent: VoucherCreate_voucherCreate_voucher_minAmountSpent | null;
minCheckoutItemsQuantity: number | null;
} }
export interface VoucherCreate_voucherCreate { export interface VoucherCreate_voucherCreate {

View file

@ -2,7 +2,7 @@
/* eslint-disable */ /* eslint-disable */
// This file was automatically generated and should not be edited. // This file was automatically generated and should not be edited.
import { VoucherDiscountValueType, VoucherType } from "./../../types/globalTypes"; import { DiscountValueTypeEnum, VoucherTypeEnum } from "./../../types/globalTypes";
// ==================================================== // ====================================================
// GraphQL query operation: VoucherDetails // GraphQL query operation: VoucherDetails
@ -127,18 +127,19 @@ export interface VoucherDetails_voucher_categories {
export interface VoucherDetails_voucher { export interface VoucherDetails_voucher {
__typename: "Voucher"; __typename: "Voucher";
id: string; id: string;
name: string | null; code: string;
startDate: any; startDate: any;
endDate: any | null; endDate: any | null;
usageLimit: number | null; usageLimit: number | null;
discountValueType: VoucherDiscountValueType; discountValueType: DiscountValueTypeEnum;
discountValue: number; discountValue: number;
countries: (VoucherDetails_voucher_countries | null)[] | null; countries: (VoucherDetails_voucher_countries | null)[] | null;
minAmountSpent: VoucherDetails_voucher_minAmountSpent | null; minAmountSpent: VoucherDetails_voucher_minAmountSpent | null;
type: VoucherType; minCheckoutItemsQuantity: number | null;
code: string; type: VoucherTypeEnum;
used: number; used: number;
applyOncePerOrder: boolean; applyOncePerOrder: boolean;
applyOncePerCustomer: boolean;
products: VoucherDetails_voucher_products | null; products: VoucherDetails_voucher_products | null;
collections: VoucherDetails_voucher_collections | null; collections: VoucherDetails_voucher_collections | null;
categories: VoucherDetails_voucher_categories | null; categories: VoucherDetails_voucher_categories | null;

View file

@ -2,7 +2,7 @@
/* eslint-disable */ /* eslint-disable */
// This file was automatically generated and should not be edited. // This file was automatically generated and should not be edited.
import { VoucherDiscountValueType, VoucherType } from "./../../types/globalTypes"; import { DiscountValueTypeEnum, VoucherTypeEnum } from "./../../types/globalTypes";
// ==================================================== // ====================================================
// GraphQL fragment: VoucherDetailsFragment // GraphQL fragment: VoucherDetailsFragment
@ -127,18 +127,19 @@ export interface VoucherDetailsFragment_categories {
export interface VoucherDetailsFragment { export interface VoucherDetailsFragment {
__typename: "Voucher"; __typename: "Voucher";
id: string; id: string;
name: string | null; code: string;
startDate: any; startDate: any;
endDate: any | null; endDate: any | null;
usageLimit: number | null; usageLimit: number | null;
discountValueType: VoucherDiscountValueType; discountValueType: DiscountValueTypeEnum;
discountValue: number; discountValue: number;
countries: (VoucherDetailsFragment_countries | null)[] | null; countries: (VoucherDetailsFragment_countries | null)[] | null;
minAmountSpent: VoucherDetailsFragment_minAmountSpent | null; minAmountSpent: VoucherDetailsFragment_minAmountSpent | null;
type: VoucherType; minCheckoutItemsQuantity: number | null;
code: string; type: VoucherTypeEnum;
used: number; used: number;
applyOncePerOrder: boolean; applyOncePerOrder: boolean;
applyOncePerCustomer: boolean;
products: VoucherDetailsFragment_products | null; products: VoucherDetailsFragment_products | null;
collections: VoucherDetailsFragment_collections | null; collections: VoucherDetailsFragment_collections | null;
categories: VoucherDetailsFragment_categories | null; categories: VoucherDetailsFragment_categories | null;

View file

@ -2,7 +2,7 @@
/* eslint-disable */ /* eslint-disable */
// This file was automatically generated and should not be edited. // This file was automatically generated and should not be edited.
import { VoucherDiscountValueType } from "./../../types/globalTypes"; import { DiscountValueTypeEnum } from "./../../types/globalTypes";
// ==================================================== // ====================================================
// GraphQL fragment: VoucherFragment // GraphQL fragment: VoucherFragment
@ -23,12 +23,13 @@ export interface VoucherFragment_minAmountSpent {
export interface VoucherFragment { export interface VoucherFragment {
__typename: "Voucher"; __typename: "Voucher";
id: string; id: string;
name: string | null; code: string;
startDate: any; startDate: any;
endDate: any | null; endDate: any | null;
usageLimit: number | null; usageLimit: number | null;
discountValueType: VoucherDiscountValueType; discountValueType: DiscountValueTypeEnum;
discountValue: number; discountValue: number;
countries: (VoucherFragment_countries | null)[] | null; countries: (VoucherFragment_countries | null)[] | null;
minAmountSpent: VoucherFragment_minAmountSpent | null; minAmountSpent: VoucherFragment_minAmountSpent | null;
minCheckoutItemsQuantity: number | null;
} }

View file

@ -2,7 +2,7 @@
/* eslint-disable */ /* eslint-disable */
// This file was automatically generated and should not be edited. // This file was automatically generated and should not be edited.
import { VoucherDiscountValueType } from "./../../types/globalTypes"; import { DiscountValueTypeEnum } from "./../../types/globalTypes";
// ==================================================== // ====================================================
// GraphQL query operation: VoucherList // GraphQL query operation: VoucherList
@ -23,14 +23,15 @@ export interface VoucherList_vouchers_edges_node_minAmountSpent {
export interface VoucherList_vouchers_edges_node { export interface VoucherList_vouchers_edges_node {
__typename: "Voucher"; __typename: "Voucher";
id: string; id: string;
name: string | null; code: string;
startDate: any; startDate: any;
endDate: any | null; endDate: any | null;
usageLimit: number | null; usageLimit: number | null;
discountValueType: VoucherDiscountValueType; discountValueType: DiscountValueTypeEnum;
discountValue: number; discountValue: number;
countries: (VoucherList_vouchers_edges_node_countries | null)[] | null; countries: (VoucherList_vouchers_edges_node_countries | null)[] | null;
minAmountSpent: VoucherList_vouchers_edges_node_minAmountSpent | null; minAmountSpent: VoucherList_vouchers_edges_node_minAmountSpent | null;
minCheckoutItemsQuantity: number | null;
} }
export interface VoucherList_vouchers_edges { export interface VoucherList_vouchers_edges {

Some files were not shown because too many files have changed in this diff Show more