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 SVG from "react-inlinesvg";
import * as backgroundArt from "@assets/images/login-background.svg";
import * as saleorDarkLogo from "@assets/images/logo-dark.svg";
import * as saleorLightLogo from "@assets/images/logo-light.svg";
import backgroundArt from "@assets/images/login-background.svg";
import saleorDarkLogo from "@assets/images/logo-dark.svg";
import saleorLightLogo from "@assets/images/logo-light.svg";
import { ControlledCheckbox } from "@saleor/components/ControlledCheckbox";
import Form from "@saleor/components/Form";
import { FormSpacer } from "@saleor/components/FormSpacer";
import useTheme from "@saleor/hooks/useTheme";
import i18n from "../../../i18n";
import i18n from "@saleor/i18n";
export interface FormData {
email: string;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -22,6 +22,46 @@ export const collections: CollectionList_collections_edges_node[] = [
__typename: "ProductCountableConnection",
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: (

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -7,8 +7,11 @@ import {
import Typography from "@material-ui/core/Typography";
import classNames from "classnames";
import React from "react";
import SVG from "react-inlinesvg";
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 { configurationMenu, configurationMenuUrl } from "../../configuration";
import i18n from "../../i18n";
@ -19,6 +22,44 @@ import { IMenuItem } from "./menuStructure";
const styles = (theme: Theme) =>
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: {
display: "flex",
flexDirection: "column",
@ -28,24 +69,33 @@ const styles = (theme: Theme) =>
paddingBottom: theme.spacing.unit * 3
},
menuListItem: {
"&:hover": {
color: theme.palette.primary.main
},
alignItems: "center",
display: "block",
marginTop: theme.spacing.unit * 2,
marginBottom: theme.spacing.unit * 5,
paddingLeft: 0,
textDecoration: "none",
transition: theme.transitions.duration.standard + "ms"
},
menuListItemActive: {
"&:before": {
background: theme.palette.primary.main,
"& $menuListItemText": {
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: "''",
height: "100%",
left: -32,
height: 0,
position: "absolute",
width: 5
right: -35,
top: 15,
width: 0
},
position: "relative"
},
@ -54,50 +104,96 @@ const styles = (theme: Theme) =>
color: theme.palette.primary.main
},
cursor: "pointer",
display: "inline-block",
fontSize: "1rem",
fontWeight: 500,
opacity: 1,
paddingLeft: 16,
textTransform: "uppercase",
transition: theme.transitions.duration.standard + "ms"
transition: `opacity ${theme.transitions.duration.shorter}ms ease 0.1s`
},
menuListNested: {
"& $menuListItemActive": {
"& $menuListItemText": {
color: theme.palette.primary.main
menuListItemTextHide: {
opacity: 0,
position: "absolute",
transition: `opacity ${theme.transitions.duration.shorter}ms ease`
},
"&:before": {
borderRadius: "100%",
height: 8,
marginLeft: 9,
marginTop: 7,
width: 8
}
subMenu: {
padding: "0 15px"
},
"& $menuListItemText": {
textTransform: "none"
subMenuDrawer: {
background: "#000",
cursor: "pointer",
height: "100vh",
left: 0,
opacity: 0.2,
position: "absolute",
top: 0,
width: 0,
zIndex: -2
},
marginLeft: theme.spacing.unit * 3
subMenuDrawerOpen: {
width: `100vw`
}
});
interface MenuListProps {
className?: string;
menuItems: IMenuItem[];
isMenuSmall: boolean;
location: string;
user: User;
renderConfigure: boolean;
onMenuItemClick: (url: string, event: React.MouseEvent<any>) => void;
}
export interface IActiveSubMenu {
isActive: boolean;
label: string | null;
}
const MenuList = withStyles(styles, { name: "MenuList" })(
({
classes,
className,
menuItems,
isMenuSmall,
location,
user,
renderConfigure,
onMenuItemClick
}: MenuListProps & WithStyles<typeof styles>) => (
<div className={className}>
}: MenuListProps & WithStyles<typeof styles>) => {
const { isDark } = useTheme();
const [activeSubMenu, setActiveSubMenu] = React.useState<IActiveSubMenu>({
isActive: false,
label: null
});
const handleSubMenu = itemLabel => {
setActiveSubMenu({
isActive:
itemLabel === activeSubMenu.label ? !activeSubMenu.isActive : true,
label: itemLabel
});
};
const closeSubMenu = (menuItemUrl, event) => {
setActiveSubMenu({
isActive: 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) =>
@ -111,7 +207,9 @@ const MenuList = withStyles(styles, { name: "MenuList" })(
if (
menuItem.permission &&
!user.permissions.map(perm => perm.code).includes(menuItem.permission)
!user.permissions
.map(perm => perm.code)
.includes(menuItem.permission)
) {
return null;
}
@ -123,15 +221,52 @@ const MenuList = withStyles(styles, { name: "MenuList" })(
);
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
classes={classes}
isAnyChildActive={isAnyChildActive}
location={location}
activeItem={activeSubMenu}
closeSubMenu={setActiveSubMenu}
menuItem={menuItem}
onMenuItemClick={onMenuItemClick}
user={user}
key={menuItem.label}
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>
);
}
@ -141,36 +276,59 @@ const MenuList = withStyles(styles, { name: "MenuList" })(
[classes.menuListItemActive]: isActive(menuItem)
})}
href={createHref(menuItem.url)}
onClick={event => onMenuItemClick(menuItem.url, event)}
onClick={event => closeSubMenu(menuItem.url, event)}
key={menuItem.label}
>
<div className={classes.menuItemHover}>
<SVG
className={classNames(classes.menuIcon, {
[classes.menuIconDark]: isDark
})}
src={menuItem.icon}
/>
<Typography
aria-label={menuItem.ariaLabel}
className={classes.menuListItemText}
className={classNames(classes.menuListItemText, {
[classes.menuListItemTextHide]: !isMenuSmall
})}
>
{menuItem.label}
</Typography>
</div>
</a>
);
})}
{renderConfigure &&
configurationMenu.filter(menuItem =>
user.permissions.map(perm => perm.code).includes(menuItem.permission)
user.permissions
.map(perm => perm.code)
.includes(menuItem.permission)
).length > 0 && (
<a
className={classes.menuListItem}
href={createHref(configurationMenuUrl)}
onClick={event => onMenuItemClick(configurationMenuUrl, event)}
>
<div className={classes.menuItemHover}>
<SVG
className={classNames(classes.menuIcon, {
[classes.menuIconDark]: isDark
})}
src={configureIcon}
/>
<Typography
aria-label="configure"
className={classes.menuListItemText}
className={classNames(classes.menuListItemText, {
[classes.menuListItemTextHide]: !isMenuSmall
})}
>
{i18n.t("Configure")}
</Typography>
</div>
</a>
)}
</div>
)
);
}
);
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 classNames from "classnames";
import React from "react";
import SVG from "react-inlinesvg";
import { User } from "../../auth/types/User";
import MenuList from "./MenuList";
import menuArrowIcon from "@assets/images/menu-arrow-icon.svg";
import useTheme from "@saleor/hooks/useTheme";
import { createHref } from "@saleor/misc";
import { drawerWidthExpanded } from "./consts";
import { IActiveSubMenu } from "./MenuList";
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 {
classes: Record<
| "menuListItem"
| "menuListItemActive"
| "menuListItemText"
| "menuListNested",
string
>;
isAnyChildActive: boolean;
activeItem: IActiveSubMenu;
ariaLabel: string;
closeSubMenu: ({ isActive, label }: IActiveSubMenu) => void;
icon: string;
menuItem: IMenuItem;
location: string;
user: User;
title: string;
handleSubMenu: (itemLabel: string) => void;
onMenuItemClick: (url: string, event: React.MouseEvent<any>) => void;
}
const MenuNested: React.FC<MenuNestedProps> = ({
const MenuNested = withStyles(styles, { name: "MenuNested" })(
({
activeItem,
ariaLabel,
classes,
isAnyChildActive,
location,
closeSubMenu,
icon,
menuItem,
onMenuItemClick,
user
}) => {
const [isOpened, setOpenStatus] = React.useState(false);
title
}: MenuNestedProps & WithStyles<typeof styles>) => {
const menuItems = menuItem.children;
const { isDark } = useTheme();
const closeMenu = (menuItemUrl, event) => {
onMenuItemClick(menuItemUrl, event);
closeSubMenu({
isActive: false,
label: null
});
event.stopPropagation();
event.preventDefault();
};
return (
<>
<div
onClick={() => setOpenStatus(!isOpened)}
className={classNames(classes.menuListItem, {
[classes.menuListItemActive]: isAnyChildActive
className={classNames(classes.menuListNested, {
[classes.menuListNestedOpen]:
activeItem.label === ariaLabel && activeItem.isActive
})}
>
<Typography
aria-label={menuItem.ariaLabel}
className={classes.menuListItemText}
className={classNames(classes.subHeader, {
[classes.subHeaderDark]: isDark
})}
variant="h5"
>
{menuItem.label}
</Typography>
{isOpened && (
<div className={classes.menuListNested}>
<MenuList
menuItems={menuItem.children}
location={location}
user={user}
renderConfigure={false}
onMenuItemClick={onMenuItemClick}
<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>
)}
</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>
</>
);
}
);
};
export default MenuNested;

View file

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

View file

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

View file

@ -15,8 +15,8 @@ import React from "react";
import ConfirmButton, {
ConfirmButtonTransitionState
} from "@saleor/components/ConfirmButton";
import Form from "@saleor/components/Form";
import FormSpacer from "@saleor/components/FormSpacer";
import useSearchQuery from "@saleor/hooks/useSearchQuery";
import { SearchCategories_categories_edges_node } from "../../containers/SearchCategories/types/SearchCategories";
import i18n from "../../i18n";
import Checkbox from "../Checkbox";
@ -50,13 +50,28 @@ interface AssignCategoriesDialogProps extends WithStyles<typeof styles> {
loading: boolean;
onClose: () => 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, {
name: "AssignCategoriesDialog"
})(
@ -69,7 +84,15 @@ const AssignCategoriesDialog = withStyles(styles, {
onClose,
onFetch,
onSubmit
}: AssignCategoriesDialogProps) => (
}: AssignCategoriesDialogProps) => {
const [query, onQueryChange] = useSearchQuery(onFetch);
const [selectedCategories, setSelectedCategories] = React.useState<
SearchCategories_categories_edges_node[]
>([]);
const handleSubmit = () => onSubmit(selectedCategories);
return (
<Dialog
open={open}
onClose={onClose}
@ -77,24 +100,18 @@ const AssignCategoriesDialog = withStyles(styles, {
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))}
value={query}
onChange={onQueryChange}
label={i18n.t("Search Categories", {
context: "product search input label"
context: "category search input label"
})}
placeholder={i18n.t("Search by category name, etc...", {
context: "category search input placeholder"
})}
placeholder={i18n.t(
"Search by product name, attribute, product type etc...",
{
context: "product search input placeholder"
}
)}
fullWidth
InputProps={{
autoComplete: "off",
@ -106,9 +123,8 @@ const AssignCategoriesDialog = withStyles(styles, {
<TableBody>
{categories &&
categories.map(category => {
const isChecked = !!data.categories.find(
selectedCategories =>
selectedCategories.id === category.id
const isSelected = !!selectedCategories.find(
selectedCategories => selectedCategories.id === category.id
);
return (
@ -118,26 +134,15 @@ const AssignCategoriesDialog = withStyles(styles, {
className={classes.checkboxCell}
>
<Checkbox
checked={isChecked}
checked={isSelected}
onChange={() =>
isChecked
? change({
target: {
name: "categories",
value: data.categories.filter(
selectedCategories =>
selectedCategories.id !==
category.id
handleCategoryAssign(
category,
isSelected,
selectedCategories,
setSelectedCategories
)
}
} as any)
: change({
target: {
name: "categories",
value: [...data.categories, category]
}
} as any)
}
/>
</TableCell>
<TableCell className={classes.wideCell}>
@ -158,15 +163,14 @@ const AssignCategoriesDialog = withStyles(styles, {
color="primary"
variant="contained"
type="submit"
onClick={handleSubmit}
>
{i18n.t("Assign categories", { context: "button" })}
</ConfirmButton>
</DialogActions>
</>
)}
</Form>
</Dialog>
)
);
}
);
AssignCategoriesDialog.displayName = "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 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 i18n from "../../i18n";
import Checkbox from "../Checkbox";
import ConfirmButton, {
ConfirmButtonTransitionState
} from "../ConfirmButton/ConfirmButton";
import Form from "../Form";
import FormSpacer from "../FormSpacer";
export interface FormData {
@ -50,13 +50,28 @@ interface AssignCollectionDialogProps extends WithStyles<typeof styles> {
loading: boolean;
onClose: () => 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, {
name: "AssignCollectionDialog"
})(
@ -69,7 +84,15 @@ const AssignCollectionDialog = withStyles(styles, {
onClose,
onFetch,
onSubmit
}: AssignCollectionDialogProps) => (
}: AssignCollectionDialogProps) => {
const [query, onQueryChange] = useSearchQuery(onFetch);
const [selectedCollections, setSelectedCollections] = React.useState<
SearchCollections_collections_edges_node[]
>([]);
const handleSubmit = () => onSubmit(selectedCollections);
return (
<Dialog
onClose={onClose}
open={open}
@ -77,24 +100,18 @@ const AssignCollectionDialog = withStyles(styles, {
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))}
value={query}
onChange={onQueryChange}
label={i18n.t("Search Collection", {
context: "product search input label"
})}
placeholder={i18n.t(
"Search by product name, attribute, product type etc...",
{
placeholder={i18n.t("Search by collection name, etc...", {
context: "product search input placeholder"
}
)}
})}
fullWidth
InputProps={{
autoComplete: "off",
@ -105,43 +122,32 @@ const AssignCollectionDialog = withStyles(styles, {
<Table>
<TableBody>
{collections &&
collections.map(category => {
const isChecked = !!data.collections.find(
collections.map(collection => {
const isSelected = !!selectedCollections.find(
selectedCollection =>
selectedCollection.id === category.id
selectedCollection.id === collection.id
);
return (
<TableRow key={category.id}>
<TableRow key={collection.id}>
<TableCell
padding="checkbox"
className={classes.checkboxCell}
>
<Checkbox
checked={isChecked}
checked={isSelected}
onChange={() =>
isChecked
? change({
target: {
name: "collections",
value: data.collections.filter(
selectedCollection =>
selectedCollection.id !==
category.id
handleCollectionAssign(
collection,
isSelected,
selectedCollections,
setSelectedCollections
)
}
} as any)
: change({
target: {
name: "collections",
value: [...data.collections, category]
}
} as any)
}
/>
</TableCell>
<TableCell className={classes.wideCell}>
{category.name}
{collection.name}
</TableCell>
</TableRow>
);
@ -158,15 +164,14 @@ const AssignCollectionDialog = withStyles(styles, {
color="primary"
variant="contained"
type="submit"
onClick={handleSubmit}
>
{i18n.t("Assign collections", { context: "button" })}
</ConfirmButton>
</DialogActions>
</>
)}
</Form>
</Dialog>
)
);
}
);
AssignCollectionDialog.displayName = "AssignCollectionDialog";
export default AssignCollectionDialog;

View file

@ -15,12 +15,12 @@ import React from "react";
import ConfirmButton, {
ConfirmButtonTransitionState
} from "@saleor/components/ConfirmButton";
import Form from "@saleor/components/Form";
import FormSpacer from "@saleor/components/FormSpacer";
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 i18n from "../../i18n";
import { maybe } from "../../misc";
import Checkbox from "../Checkbox";
export interface FormData {
@ -40,25 +40,41 @@ const styles = createStyles({
overflow: {
overflowY: "visible"
},
scrollArea: {
overflowY: "scroll"
},
wideCell: {
width: "100%"
}
});
interface AssignProductDialogProps extends WithStyles<typeof styles> {
export interface AssignProductDialogProps {
confirmButtonState: ConfirmButtonTransitionState;
open: boolean;
products: SearchProducts_products_edges_node[];
loading: boolean;
onClose: () => 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, {
name: "AssignProductDialog"
})(
@ -71,7 +87,15 @@ const AssignProductDialog = withStyles(styles, {
onClose,
onFetch,
onSubmit
}: AssignProductDialogProps) => (
}: AssignProductDialogProps & WithStyles<typeof styles>) => {
const [query, onQueryChange] = useSearchQuery(onFetch);
const [selectedProducts, setSelectedProducts] = React.useState<
SearchProducts_products_edges_node[]
>([]);
const handleSubmit = () => onSubmit(selectedProducts);
return (
<Dialog
onClose={onClose}
open={open}
@ -79,15 +103,12 @@ const AssignProductDialog = withStyles(styles, {
fullWidth
maxWidth="sm"
>
<Form initial={initialForm} onSubmit={onSubmit}>
{({ data, change }) => (
<>
<DialogTitle>{i18n.t("Assign Product")}</DialogTitle>
<DialogContent className={classes.overflow}>
<DialogContent>
<TextField
name="query"
value={data.query}
onChange={event => change(event, () => onFetch(data.query))}
value={query}
onChange={onQueryChange}
label={i18n.t("Search Products", {
context: "product search input label"
})}
@ -104,11 +125,12 @@ const AssignProductDialog = withStyles(styles, {
}}
/>
<FormSpacer />
<div className={classes.scrollArea}>
<Table>
<TableBody>
{products &&
products.map(product => {
const isChecked = !!data.products.find(
const isSelected = selectedProducts.some(
selectedProduct => selectedProduct.id === product.id
);
@ -126,25 +148,15 @@ const AssignProductDialog = withStyles(styles, {
className={classes.checkboxCell}
>
<Checkbox
checked={isChecked}
checked={isSelected}
onChange={() =>
isChecked
? change({
target: {
name: "products",
value: data.products.filter(
selectedProduct =>
selectedProduct.id !== product.id
handleProductAssign(
product,
isSelected,
selectedProducts,
setSelectedProducts
)
}
} as any)
: change({
target: {
name: "products",
value: [...data.products, product]
}
} as any)
}
/>
</TableCell>
</TableRow>
@ -152,6 +164,7 @@ const AssignProductDialog = withStyles(styles, {
})}
</TableBody>
</Table>
</div>
</DialogContent>
<DialogActions>
<Button onClick={onClose}>
@ -162,15 +175,14 @@ const AssignProductDialog = withStyles(styles, {
color="primary"
variant="contained"
type="submit"
onClick={handleSubmit}
>
{i18n.t("Assign products", { context: "button" })}
</ConfirmButton>
</DialogActions>
</>
)}
</Form>
</Dialog>
)
);
}
);
AssignProductDialog.displayName = "AssignProductDialog";
export default AssignProductDialog;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,170 +1,42 @@
import React from "react";
import { UserError } from "../../types";
export interface FormProps<T extends {}> {
children: (props: {
data: T;
hasChanged: boolean;
errors: { [key: string]: string };
change(event: React.ChangeEvent<any>, cb?: () => void);
reset();
submit(event?: React.FormEvent<any>);
}) => React.ReactElement<any>;
import useForm, { UseFormResult } from "@saleor/hooks/useForm";
import { UserError } from "@saleor/types";
export interface FormProps<T> {
children: (props: UseFormResult<T>) => React.ReactNode;
confirmLeave?: boolean;
errors?: UserError[];
initial?: T;
confirmLeave?: boolean;
useForm?: boolean;
resetOnSubmit?: boolean;
onSubmit?(data: T);
onSubmit?: (data: T) => void;
}
interface FormComponentProps<T extends {}> extends FormProps<T> {
hasChanged: boolean;
toggleFormChangeState: () => void;
}
function Form<T>(props: FormProps<T>) {
const { children, errors, initial, resetOnSubmit, onSubmit } = props;
const renderProps = useForm(initial, errors, onSubmit);
interface FormState<T extends {}> {
initial: T;
fields: T;
hasChanged: boolean;
}
function handleSubmit(event?: React.FormEvent<any>, cb?: () => void) {
const { reset, submit } = renderProps;
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) {
event.stopPropagation();
event.preventDefault();
}
if (onSubmit !== undefined) {
onSubmit(this.state.fields);
}
if (cb) {
cb();
}
if (resetOnSubmit) {
this.setState({
fields: this.state.initial
});
reset();
}
};
render() {
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>
);
submit();
}
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 { default } from "./Form";
export { default as FormActions } from "./FormActions";
export * from "./FormActions";
export * from "./FormContext";
export { default } from "./FormContext";

View file

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

View file

@ -1,4 +1,5 @@
import CircularProgress from "@material-ui/core/CircularProgress";
import IconButton from "@material-ui/core/IconButton";
import MenuItem from "@material-ui/core/MenuItem";
import Paper from "@material-ui/core/Paper";
import {
@ -8,27 +9,77 @@ import {
WithStyles
} from "@material-ui/core/styles";
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 React from "react";
import { compareTwoStrings } from "string-similarity";
import i18n from "../../i18n";
import ArrowDropdownIcon from "../../icons/ArrowDropdown";
import Debounce, { DebounceProps } from "../Debounce";
import { fade } from "@material-ui/core/styles/colorManipulator";
import Checkbox from "@saleor/components/Checkbox";
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;
value: string;
}
const styles = (theme: Theme) =>
createStyles({
checkbox: {
height: 24,
width: 20
},
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: {
flexGrow: 1,
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: {
left: 0,
marginTop: theme.spacing.unit,
@ -39,39 +90,32 @@ const styles = (theme: Theme) =>
}
});
export interface MultiAutocompleteSelectFieldChildrenFunc {
deleteItem: (item: ChoiceType) => void;
items: ChoiceType[];
}
export type MultiAutocompleteSelectFieldChildren = (
props: MultiAutocompleteSelectFieldChildrenFunc
) => React.ReactNode;
export interface MultiAutocompleteSelectFieldProps
extends WithStyles<typeof styles> {
export interface MultiAutocompleteSelectFieldProps {
allowCustomValues?: boolean;
displayValues: MultiAutocompleteChoiceType[];
name: string;
children: MultiAutocompleteSelectFieldChildren;
choices: ChoiceType[];
value?: ChoiceType[];
choices: MultiAutocompleteChoiceType[];
value: string[];
loading?: boolean;
placeholder?: string;
helperText?: string;
label?: string;
fetchChoices(value: string);
onChange(event);
fetchChoices?: (value: string) => void;
onChange: (event: React.ChangeEvent<any>) => void;
}
const DebounceAutocomplete: React.ComponentType<
DebounceProps<string>
> = Debounce;
export const MultiAutocompleteSelectField = withStyles(styles, {
export const MultiAutocompleteSelectFieldComponent = withStyles(styles, {
name: "MultiAutocompleteSelectField"
})(
({
children,
allowCustomValues,
choices,
classes,
displayValues,
helperText,
label,
loading,
@ -80,47 +124,35 @@ export const MultiAutocompleteSelectField = withStyles(styles, {
value,
fetchChoices,
onChange
}: MultiAutocompleteSelectFieldProps) => {
}: MultiAutocompleteSelectFieldProps & WithStyles<typeof styles>) => {
const handleSelect = (
item: ChoiceType,
{ reset }: ControllerStateAndHelpers
item: string,
downshiftOpts?: ControllerStateAndHelpers
) => {
reset({ inputValue: "" });
onChange({ target: { name, value: [...value, item] } });
if (downshiftOpts) {
downshiftOpts.reset({ inputValue: "" });
}
onChange({
target: { name, value: item }
} as any);
};
const handleDelete = (item: ChoiceType) => {
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
);
const suggestions = choices.filter(choice => !value.includes(choice.value));
return (
<DebounceAutocomplete debounceFn={fetchChoices}>
{debounce => (
<>
<Downshift
selectedItem={value}
itemToString={item => (item ? item.label : "")}
onInputValueChange={fetchChoices}
onSelect={handleSelect}
onInputValueChange={value => debounce(value)}
itemToString={() => ""}
>
{({
getInputProps,
getItemProps,
isOpen,
selectedItem,
toggleMenu,
closeMenu,
openMenu,
highlightedIndex
}) => {
return (
highlightedIndex,
inputValue
}) => (
<div className={classes.container}>
<TextField
InputProps={{
@ -137,45 +169,155 @@ export const MultiAutocompleteSelectField = withStyles(styles, {
</div>
),
id: undefined,
onBlur: closeMenu,
onFocus: openMenu
onClick: toggleMenu
}}
helperText={helperText}
label={label}
fullWidth={true}
/>
{isOpen && (
{isOpen && (!!inputValue || !!choices.length) && (
<Paper className={classes.paper} square>
{!loading && filteredChoices.length > 0
? filteredChoices.map((suggestion, index) => (
{choices.length > 0 ||
displayValues.length > 0 ||
allowCustomValues ? (
<>
{displayValues.map(value => (
<MenuItem
key={suggestion.value}
selected={highlightedIndex === index}
className={classes.menuItem}
key={value.value}
selected={true}
component="div"
{...getItemProps({ item: suggestion })}
{...getItemProps({
item: value.value
})}
>
{suggestion.label}
<Checkbox
className={classes.checkbox}
checked={true}
disableRipple
/>
<span className={classes.menuItemLabel}>
{value.label}
</span>
</MenuItem>
))
: !loading && (
))}
{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>
)
)}
</Paper>
)}
{children({
deleteItem: handleDelete,
items: selectedItem
})}
</div>
);
}}
)}
</Downshift>
<div className={classes.chipContainer}>
{displayValues.map(value => (
<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>
);
}
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";
export default MultiAutocompleteSelectField;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,4 +1,5 @@
import { SearchQueryVariables } from "./containers/BaseSearch";
import { ListSettings, ListViews } from "./types";
export const APP_MOUNT_URI = process.env.APP_MOUNT_URI || "/";
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 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 { attributeListUrl } from "@saleor/attributes/urls";
import { WindowTitle } from "@saleor/components/WindowTitle";
import useNavigator from "@saleor/hooks/useNavigator";
import useUser from "@saleor/hooks/useUser";
import { WindowTitle } from "../components/WindowTitle";
import i18n from "../i18n";
import Navigation from "../icons/Navigation";
import Pages from "../icons/Pages";
import ProductTypes from "../icons/ProductTypes";
import ShippingMethods from "../icons/ShippingMethods";
import SiteSettings from "../icons/SiteSettings";
import StaffMembers from "../icons/StaffMembers";
import Taxes from "../icons/Taxes";
import { maybe } from "../misc";
import { menuListUrl } from "../navigation/urls";
import { pageListUrl } from "../pages/urls";
import { productTypeListUrl } from "../productTypes/urls";
import { shippingZonesListUrl } from "../shipping/urls";
import { siteSettingsUrl } from "../siteSettings/urls";
import { staffListUrl } from "../staff/urls";
import { taxSection } from "../taxes/urls";
import { PermissionEnum } from "../types/globalTypes";
import i18n from "@saleor/i18n";
import Navigation from "@saleor/icons/Navigation";
import Pages from "@saleor/icons/Pages";
import ProductTypes from "@saleor/icons/ProductTypes";
import ShippingMethods from "@saleor/icons/ShippingMethods";
import SiteSettings from "@saleor/icons/SiteSettings";
import StaffMembers from "@saleor/icons/StaffMembers";
import Taxes from "@saleor/icons/Taxes";
import { maybe } from "@saleor/misc";
import { menuListUrl } from "@saleor/navigation/urls";
import { pageListUrl } from "@saleor/pages/urls";
import { productTypeListUrl } from "@saleor/productTypes/urls";
import { shippingZonesListUrl } from "@saleor/shipping/urls";
import { siteSettingsUrl } from "@saleor/siteSettings/urls";
import { staffListUrl } from "@saleor/staff/urls";
import { taxSection } from "@saleor/taxes/urls";
import { PermissionEnum } from "@saleor/types/globalTypes";
import ConfigurationPage, { MenuItem } from "./ConfigurationPage";
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"),
icon: <ProductTypes fontSize="inherit" viewBox="0 0 44 44" />,

View file

@ -12,9 +12,11 @@ import ConfirmButton, {
ConfirmButtonTransitionState
} from "@saleor/components/ConfirmButton";
import Form from "@saleor/components/Form";
import i18n from "../../../i18n";
import { maybe } from "../../../misc";
import { UserError } from "../../../types";
import useStateFromProps from "@saleor/hooks/useStateFromProps";
import i18n from "@saleor/i18n";
import { maybe } from "@saleor/misc";
import { UserError } from "@saleor/types";
import createSingleAutocompleteSelectHandler from "@saleor/utils/handlers/singleAutocompleteSelectChangeHandler";
import { AddressTypeInput } from "../../types";
import { CustomerAddresses_user_addresses } from "../../types/CustomerAddresses";
@ -50,14 +52,14 @@ const CustomerAddressDialog = withStyles(styles, {})(
onClose,
onConfirm
}: CustomerAddressDialogProps & WithStyles<typeof styles>) => {
const [countryDisplayName, setCountryDisplayName] = useStateFromProps(
maybe(() => address.country.country, "")
);
const initialForm: AddressTypeInput = {
city: maybe(() => address.city, ""),
cityArea: maybe(() => address.cityArea, ""),
companyName: maybe(() => address.companyName, ""),
country: {
label: maybe(() => address.country.country, ""),
value: maybe(() => address.country.code, "")
},
country: maybe(() => address.country.code, ""),
countryArea: maybe(() => address.countryArea, ""),
firstName: maybe(() => address.firstName, ""),
lastName: maybe(() => address.lastName, ""),
@ -66,6 +68,16 @@ const CustomerAddressDialog = withStyles(styles, {})(
streetAddress1: maybe(() => address.streetAddress1, ""),
streetAddress2: maybe(() => address.streetAddress2, "")
};
const countryChoices = maybe(
() =>
countries.map(country => ({
label: country.label,
value: country.code
})),
[]
);
return (
<Dialog
onClose={onClose}
@ -75,7 +87,14 @@ const CustomerAddressDialog = withStyles(styles, {})(
maxWidth="sm"
>
<Form initial={initialForm} errors={errors} onSubmit={onConfirm}>
{({ change, data, errors, submit }) => (
{({ change, data, errors, submit }) => {
const handleCountrySelect = createSingleAutocompleteSelectHandler(
change,
setCountryDisplayName,
countryChoices
);
return (
<>
<DialogTitle>
{variant === "create"
@ -84,10 +103,12 @@ const CustomerAddressDialog = withStyles(styles, {})(
</DialogTitle>
<DialogContent className={classes.overflow}>
<AddressEdit
countries={countries}
countries={countryChoices}
data={data}
countryDisplayValue={countryDisplayName}
errors={errors}
onChange={change}
onCountryChange={handleCountrySelect}
/>
</DialogContent>
<DialogActions>
@ -106,7 +127,8 @@ const CustomerAddressDialog = withStyles(styles, {})(
</ConfirmButton>
</DialogActions>
</>
)}
);
}}
</Form>
</Dialog>
);

View file

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

View file

@ -8,6 +8,7 @@ import Form from "@saleor/components/Form";
import Grid from "@saleor/components/Grid";
import PageHeader from "@saleor/components/PageHeader";
import SaveButtonBar from "@saleor/components/SaveButtonBar";
import createSingleAutocompleteSelectHandler from "@saleor/utils/handlers/singleAutocompleteSelectChangeHandler";
import i18n from "../../../i18n";
import { UserError } from "../../../types";
import { AddressTypeInput } from "../../types";
@ -27,10 +28,7 @@ const initialForm: CustomerCreatePageFormData = {
city: "",
cityArea: "",
companyName: "",
country: {
label: "",
value: ""
},
country: "",
countryArea: "",
customerFirstName: "",
customerLastName: "",
@ -60,9 +58,28 @@ const CustomerCreatePage: React.StatelessComponent<CustomerCreatePageProps> = ({
saveButtonBar,
onBack,
onSubmit
}: CustomerCreatePageProps) => (
<Form initial={initialForm} onSubmit={onSubmit} errors={errors} confirmLeave>
{({ change, data, errors: formErrors, hasChanged, submit }) => (
}: CustomerCreatePageProps) => {
const [countryDisplayName, setCountryDisplayName] = React.useState("");
const countryChoices = countries.map(country => ({
label: country.country,
value: country.code
}));
return (
<Form
initial={initialForm}
onSubmit={onSubmit}
errors={errors}
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")} />
@ -76,11 +93,13 @@ const CustomerCreatePage: React.StatelessComponent<CustomerCreatePageProps> = ({
/>
<CardSpacer />
<CustomerCreateAddress
countries={countries}
countries={countryChoices}
countryDisplayName={countryDisplayName}
data={data}
disabled={disabled}
errors={formErrors}
onChange={change}
onCountryChange={handleCountrySelect}
/>
<CardSpacer />
<CustomerCreateNote
@ -98,8 +117,10 @@ const CustomerCreatePage: React.StatelessComponent<CustomerCreatePageProps> = ({
onCancel={onBack}
/>
</Container>
)}
);
}}
</Form>
);
};
CustomerCreatePage.displayName = "CustomerCreatePage";
export default CustomerCreatePage;

View file

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

View file

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

View file

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

View file

@ -4,8 +4,8 @@ import React from "react";
import Container from "@saleor/components/Container";
import PageHeader from "@saleor/components/PageHeader";
import i18n from "../../../i18n";
import { ListActions, PageListProps } from "../../../types";
import i18n from "@saleor/i18n";
import { ListActions, PageListProps } from "@saleor/types";
import { ListCustomers_customers_edges_node } from "../../types/ListCustomers";
import CustomerList from "../CustomerList/CustomerList";

View file

@ -2,10 +2,7 @@ export interface AddressTypeInput {
city: string;
cityArea?: string;
companyName?: string;
country: {
label: string;
value: string;
};
country: string;
countryArea?: string;
firstName: 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 (
<>
<WindowTitle
@ -156,14 +166,7 @@ const CustomerAddresses: React.FC<CustomerAddressesProps> = ({
<CustomerAddressDialog
address={undefined}
confirmButtonState={createAddressTransitionState}
countries={maybe(
() =>
shop.countries.map(country => ({
code: country.code,
label: country.country
})),
[]
)}
countries={countryChoices}
errors={maybe(
() =>
createCustomerAddressOpts.data.addressCreate
@ -173,14 +176,11 @@ const CustomerAddresses: React.FC<CustomerAddressesProps> = ({
open={params.action === "add"}
variant="create"
onClose={closeModal}
onConfirm={formData =>
onConfirm={input =>
createCustomerAddress({
variables: {
id,
input: {
...formData,
country: formData.country.value
}
input
}
})
}
@ -192,7 +192,7 @@ const CustomerAddresses: React.FC<CustomerAddressesProps> = ({
)
)}
confirmButtonState={updateAddressTransitionState}
countries={[]}
countries={countryChoices}
errors={maybe(
() =>
updateCustomerAddressOpts.data.addressUpdate
@ -202,14 +202,11 @@ const CustomerAddresses: React.FC<CustomerAddressesProps> = ({
open={params.action === "edit"}
variant="edit"
onClose={closeModal}
onConfirm={formData =>
onConfirm={input =>
updateCustomerAddress({
variables: {
id: params.id,
input: {
...formData,
country: formData.country.value
}
input
}
})
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -4,8 +4,8 @@ import React from "react";
import Container from "@saleor/components/Container";
import PageHeader from "@saleor/components/PageHeader";
import i18n from "../../../i18n";
import { ListActions, PageListProps } from "../../../types";
import i18n from "@saleor/i18n";
import { ListActions, PageListProps } from "@saleor/types";
import { SaleList_sales_edges_node } from "../../types/SaleList";
import SaleList from "../SaleList";

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -2,7 +2,7 @@
/* eslint-disable */
// 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
@ -133,18 +133,19 @@ export interface VoucherCataloguesAdd_voucherCataloguesAdd_voucher_categories {
export interface VoucherCataloguesAdd_voucherCataloguesAdd_voucher {
__typename: "Voucher";
id: string;
name: string | null;
code: string;
startDate: any;
endDate: any | null;
usageLimit: number | null;
discountValueType: VoucherDiscountValueType;
discountValueType: DiscountValueTypeEnum;
discountValue: number;
countries: (VoucherCataloguesAdd_voucherCataloguesAdd_voucher_countries | null)[] | null;
minAmountSpent: VoucherCataloguesAdd_voucherCataloguesAdd_voucher_minAmountSpent | null;
type: VoucherType;
code: string;
minCheckoutItemsQuantity: number | null;
type: VoucherTypeEnum;
used: number;
applyOncePerOrder: boolean;
applyOncePerCustomer: boolean;
products: VoucherCataloguesAdd_voucherCataloguesAdd_voucher_products | null;
collections: VoucherCataloguesAdd_voucherCataloguesAdd_voucher_collections | null;
categories: VoucherCataloguesAdd_voucherCataloguesAdd_voucher_categories | null;

View file

@ -2,7 +2,7 @@
/* eslint-disable */
// 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
@ -133,18 +133,19 @@ export interface VoucherCataloguesRemove_voucherCataloguesRemove_voucher_categor
export interface VoucherCataloguesRemove_voucherCataloguesRemove_voucher {
__typename: "Voucher";
id: string;
name: string | null;
code: string;
startDate: any;
endDate: any | null;
usageLimit: number | null;
discountValueType: VoucherDiscountValueType;
discountValueType: DiscountValueTypeEnum;
discountValue: number;
countries: (VoucherCataloguesRemove_voucherCataloguesRemove_voucher_countries | null)[] | null;
minAmountSpent: VoucherCataloguesRemove_voucherCataloguesRemove_voucher_minAmountSpent | null;
type: VoucherType;
code: string;
minCheckoutItemsQuantity: number | null;
type: VoucherTypeEnum;
used: number;
applyOncePerOrder: boolean;
applyOncePerCustomer: boolean;
products: VoucherCataloguesRemove_voucherCataloguesRemove_voucher_products | null;
collections: VoucherCataloguesRemove_voucherCataloguesRemove_voucher_collections | null;
categories: VoucherCataloguesRemove_voucherCataloguesRemove_voucher_categories | null;

View file

@ -2,7 +2,7 @@
/* eslint-disable */
// 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
@ -29,14 +29,15 @@ export interface VoucherCreate_voucherCreate_voucher_minAmountSpent {
export interface VoucherCreate_voucherCreate_voucher {
__typename: "Voucher";
id: string;
name: string | null;
code: string;
startDate: any;
endDate: any | null;
usageLimit: number | null;
discountValueType: VoucherDiscountValueType;
discountValueType: DiscountValueTypeEnum;
discountValue: number;
countries: (VoucherCreate_voucherCreate_voucher_countries | null)[] | null;
minAmountSpent: VoucherCreate_voucherCreate_voucher_minAmountSpent | null;
minCheckoutItemsQuantity: number | null;
}
export interface VoucherCreate_voucherCreate {

View file

@ -2,7 +2,7 @@
/* eslint-disable */
// 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
@ -127,18 +127,19 @@ export interface VoucherDetails_voucher_categories {
export interface VoucherDetails_voucher {
__typename: "Voucher";
id: string;
name: string | null;
code: string;
startDate: any;
endDate: any | null;
usageLimit: number | null;
discountValueType: VoucherDiscountValueType;
discountValueType: DiscountValueTypeEnum;
discountValue: number;
countries: (VoucherDetails_voucher_countries | null)[] | null;
minAmountSpent: VoucherDetails_voucher_minAmountSpent | null;
type: VoucherType;
code: string;
minCheckoutItemsQuantity: number | null;
type: VoucherTypeEnum;
used: number;
applyOncePerOrder: boolean;
applyOncePerCustomer: boolean;
products: VoucherDetails_voucher_products | null;
collections: VoucherDetails_voucher_collections | null;
categories: VoucherDetails_voucher_categories | null;

View file

@ -2,7 +2,7 @@
/* eslint-disable */
// 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
@ -127,18 +127,19 @@ export interface VoucherDetailsFragment_categories {
export interface VoucherDetailsFragment {
__typename: "Voucher";
id: string;
name: string | null;
code: string;
startDate: any;
endDate: any | null;
usageLimit: number | null;
discountValueType: VoucherDiscountValueType;
discountValueType: DiscountValueTypeEnum;
discountValue: number;
countries: (VoucherDetailsFragment_countries | null)[] | null;
minAmountSpent: VoucherDetailsFragment_minAmountSpent | null;
type: VoucherType;
code: string;
minCheckoutItemsQuantity: number | null;
type: VoucherTypeEnum;
used: number;
applyOncePerOrder: boolean;
applyOncePerCustomer: boolean;
products: VoucherDetailsFragment_products | null;
collections: VoucherDetailsFragment_collections | null;
categories: VoucherDetailsFragment_categories | null;

View file

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

View file

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

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