Handle Cloud limits (#1053)
* Handle limit reached error * Update changelog * Move notifications to higher layer * Handle limits top-level * Remove unused imports * Add alert component * Fetch limits from API * Display limits in staff list * Fix disabled text button color * Display limits in products pages * Display limits in channel list * Display limits in warehouse list * Display limits in order list * Add stories for limits * Move alert to corresponding section * Update schema * Update changelog * Fetch only needed limitations * Fix types * Fix story * Display limits in variant creator * Improve type consistency * Update snapshots * Update limit counter after object deletion * Fix copy * Improve code readability * Lint files * Fix dark mode alerts * Improve contrast * Fix sku limit in product variant list * Update stories * Update messages * Extract messages
This commit is contained in:
parent
20fbc19739
commit
a3abb9c476
53 changed files with 15543 additions and 138 deletions
|
@ -31,6 +31,8 @@ All notable, unreleased changes to this project will be documented in this file.
|
|||
- Update schema to contain email plugin changes - #1029 by @dominik-zeglen
|
||||
- Unconfirmed order manipulation - #967 by @tomaszszymanski129
|
||||
- Add multiline field plugins - #974 by @dominik-zeglen
|
||||
- Handle limit reached error - #990 by @dominik-zeglen
|
||||
- Display Cloud limits - #1004 by @dominik-zeglen
|
||||
|
||||
# 2.11.1
|
||||
|
||||
|
|
5
assets/images/alert-dark.svg
Normal file
5
assets/images/alert-dark.svg
Normal file
|
@ -0,0 +1,5 @@
|
|||
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="20" cy="20" r="20" fill="#202124"/>
|
||||
<circle cx="20" cy="20" r="14" stroke="currentColor" stroke-width="4"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M19.5 11C18.6716 11 18 11.6716 18 12.5V20.5C18 21.3284 18.6716 22 19.5 22H20.5C21.3284 22 22 21.3284 22 20.5V12.5C22 11.6716 21.3284 11 20.5 11H19.5ZM19.5 25C18.6716 25 18 25.6716 18 26.5V27.5C18 28.3284 18.6716 29 19.5 29H20.5C21.3284 29 22 28.3284 22 27.5V26.5C22 25.6716 21.3284 25 20.5 25H19.5Z" fill="currentColor"/>
|
||||
</svg>
|
After Width: | Height: | Size: 594 B |
5
assets/images/alert.svg
Normal file
5
assets/images/alert.svg
Normal file
|
@ -0,0 +1,5 @@
|
|||
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="20" cy="20" r="20" fill="white"/>
|
||||
<circle cx="20" cy="20" r="14" stroke="currentColor" stroke-width="4"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M19.5 11C18.6716 11 18 11.6716 18 12.5V20.5C18 21.3284 18.6716 22 19.5 22H20.5C21.3284 22 22 21.3284 22 20.5V12.5C22 11.6716 21.3284 11 20.5 11H19.5ZM19.5 25C18.6716 25 18 25.6716 18 26.5V27.5C18 28.3284 18.6716 29 19.5 29H20.5C21.3284 29 22 28.3284 22 27.5V26.5C22 25.6716 21.3284 25 20.5 25H19.5Z" fill="#FFB84E"/>
|
||||
</svg>
|
After Width: | Height: | Size: 587 B |
|
@ -1363,6 +1363,10 @@
|
|||
"context": "activate",
|
||||
"string": "Activate"
|
||||
},
|
||||
"src_dot_channels_dot_pages_dot_ChannelsListPage_dot_2754800034": {
|
||||
"context": "alert",
|
||||
"string": "Channel limit reached"
|
||||
},
|
||||
"src_dot_channels_dot_pages_dot_ChannelsListPage_dot_3511613983": {
|
||||
"context": "channel name",
|
||||
"string": "Channel Name"
|
||||
|
@ -1378,6 +1382,9 @@
|
|||
"context": "button",
|
||||
"string": "Create Channel"
|
||||
},
|
||||
"src_dot_channels_dot_pages_dot_ChannelsListPage_dot_85767761": {
|
||||
"string": "You have reached your channel limit, you will be no longer able to add channels to your store. If you would like to up your limit, contact your administration staff about raising your limits."
|
||||
},
|
||||
"src_dot_channels_dot_views_dot_ChannelCreate_dot_2071399707": {
|
||||
"context": "currency code select",
|
||||
"string": "{code} - {countries}"
|
||||
|
@ -3168,6 +3175,9 @@
|
|||
"src_dot_lastName": {
|
||||
"string": "Last Name"
|
||||
},
|
||||
"src_dot_limitReached": {
|
||||
"string": "Reached limit for this plan"
|
||||
},
|
||||
"src_dot_manage": {
|
||||
"context": "button",
|
||||
"string": "Manage"
|
||||
|
@ -3712,6 +3722,10 @@
|
|||
"context": "generate invoice button",
|
||||
"string": "Generate"
|
||||
},
|
||||
"src_dot_orders_dot_components_dot_OrderListPage_dot_2214147779": {
|
||||
"context": "alert",
|
||||
"string": "Order limit reached"
|
||||
},
|
||||
"src_dot_orders_dot_components_dot_OrderListPage_dot_2225897825": {
|
||||
"context": "button",
|
||||
"string": "Order Settings"
|
||||
|
@ -3723,6 +3737,9 @@
|
|||
"src_dot_orders_dot_components_dot_OrderListPage_dot_355376157": {
|
||||
"string": "Search Orders..."
|
||||
},
|
||||
"src_dot_orders_dot_components_dot_OrderListPage_dot_3769643084": {
|
||||
"string": "You have reached your order limit, you will be billed extra for orders above limit. If you would like to up your limit, contact your administration staff about raising your limits."
|
||||
},
|
||||
"src_dot_orders_dot_components_dot_OrderListPage_dot_875489544": {
|
||||
"context": "tab name",
|
||||
"string": "All Orders"
|
||||
|
@ -5047,6 +5064,10 @@
|
|||
"context": "export products to csv file, button",
|
||||
"string": "Export Products"
|
||||
},
|
||||
"src_dot_products_dot_components_dot_ProductListPage_dot_3024822321": {
|
||||
"context": "alert",
|
||||
"string": "SKU limit reached"
|
||||
},
|
||||
"src_dot_products_dot_components_dot_ProductListPage_dot_3550330425": {
|
||||
"string": "Search Products..."
|
||||
},
|
||||
|
@ -5054,6 +5075,9 @@
|
|||
"context": "tab name",
|
||||
"string": "All Products"
|
||||
},
|
||||
"src_dot_products_dot_components_dot_ProductListPage_dot_911702010": {
|
||||
"string": "You have reached your SKU limit, you will be no longer able to add SKUs to your store. If you would like to up your limit, contact your administration staff about raising your limits."
|
||||
},
|
||||
"src_dot_products_dot_components_dot_ProductListPage_dot_available": {
|
||||
"context": "product status",
|
||||
"string": "Available"
|
||||
|
@ -5245,6 +5269,9 @@
|
|||
"src_dot_products_dot_components_dot_ProductVariantCreatorPage_dot_1346828628": {
|
||||
"string": "Selected values will be used to create variants for the configurable product."
|
||||
},
|
||||
"src_dot_products_dot_components_dot_ProductVariantCreatorPage_dot_1419085897": {
|
||||
"string": "You choices will add {variantsNumber} SKUs to your catalog which will exceed your limit by {aboveLimitVariantsNumber}. If you would like to up your limit, contact your administration staff about raising your limits."
|
||||
},
|
||||
"src_dot_products_dot_components_dot_ProductVariantCreatorPage_dot_168343345": {
|
||||
"context": "variant attribute",
|
||||
"string": "Attribute"
|
||||
|
@ -5287,6 +5314,10 @@
|
|||
"src_dot_products_dot_components_dot_ProductVariantCreatorPage_dot_2877822536": {
|
||||
"string": "Skip pricing for now"
|
||||
},
|
||||
"src_dot_products_dot_components_dot_ProductVariantCreatorPage_dot_3024822321": {
|
||||
"context": "alert",
|
||||
"string": "SKU limit reached"
|
||||
},
|
||||
"src_dot_products_dot_components_dot_ProductVariantCreatorPage_dot_3387090508": {
|
||||
"string": "Apply unique stock by attribute to each SKU"
|
||||
},
|
||||
|
@ -5456,6 +5487,10 @@
|
|||
"src_dot_products_dot_components_dot_ProductVariants_dot_277989856": {
|
||||
"string": "Use variants for products that come in a variety of versions for example different sizes or colors"
|
||||
},
|
||||
"src_dot_products_dot_components_dot_ProductVariants_dot_3024822321": {
|
||||
"context": "alert",
|
||||
"string": "SKU limit reached"
|
||||
},
|
||||
"src_dot_products_dot_components_dot_ProductVariants_dot_3203451491": {
|
||||
"context": "filtering option",
|
||||
"string": "All Warehouses"
|
||||
|
@ -5475,6 +5510,9 @@
|
|||
"src_dot_products_dot_components_dot_ProductVariants_dot_693960049": {
|
||||
"string": "SKU"
|
||||
},
|
||||
"src_dot_products_dot_components_dot_ProductVariants_dot_911702010": {
|
||||
"string": "You have reached your SKU limit, you will be no longer able to add SKUs to your store. If you would like to up your limit, contact your administration staff about raising your limits."
|
||||
},
|
||||
"src_dot_products_dot_components_dot_ProductVariants_dot_998917294": {
|
||||
"context": "product variant name",
|
||||
"string": "Variant"
|
||||
|
@ -6077,6 +6115,13 @@
|
|||
"context": "tab name",
|
||||
"string": "All Staff Members"
|
||||
},
|
||||
"src_dot_staff_dot_components_dot_StaffListPage_dot_378240223": {
|
||||
"string": "You have reached your staff member limit, you will be no longer able to add staff members to your store. If you would like to up your limit, contact your administration staff about raising your limits."
|
||||
},
|
||||
"src_dot_staff_dot_components_dot_StaffListPage_dot_4002475974": {
|
||||
"context": "alert",
|
||||
"string": "Staff Member limit reached"
|
||||
},
|
||||
"src_dot_staff_dot_components_dot_StaffListPage_dot_61043583": {
|
||||
"string": "Search Staff Member"
|
||||
},
|
||||
|
@ -6777,6 +6822,13 @@
|
|||
"context": "tab name",
|
||||
"string": "All Warehouses"
|
||||
},
|
||||
"src_dot_warehouses_dot_components_dot_WarehouseListPage_dot_3562691996": {
|
||||
"context": "alert",
|
||||
"string": "Warehouse limit reached"
|
||||
},
|
||||
"src_dot_warehouses_dot_components_dot_WarehouseListPage_dot_478107533": {
|
||||
"string": "You have reached your warehouse limit, you will be no longer able to add warehouses to your store. If you would like to up your limit, contact your administration staff about raising your limits."
|
||||
},
|
||||
"src_dot_warehouses_dot_components_dot_WarehouseListPage_dot_934572530": {
|
||||
"context": "button",
|
||||
"string": "Create Warehouse"
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { limits, limitsReached } from "@saleor/fixtures";
|
||||
import Decorator from "@saleor/storybook/Decorator";
|
||||
import { storiesOf } from "@storybook/react";
|
||||
import React from "react";
|
||||
|
@ -7,6 +8,7 @@ import ChannelsListPage, { ChannelsListPageProps } from "./ChannelsListPage";
|
|||
|
||||
const props: ChannelsListPageProps = {
|
||||
channelsList,
|
||||
limits,
|
||||
navigateToChannelCreate: () => undefined,
|
||||
onBack: () => undefined,
|
||||
onRemove: () => undefined,
|
||||
|
@ -16,4 +18,8 @@ const props: ChannelsListPageProps = {
|
|||
storiesOf("Views / Channels / Channels list", module)
|
||||
.addDecorator(Decorator)
|
||||
.add("default", () => <ChannelsListPage {...props} />)
|
||||
.add("empty", () => <ChannelsListPage {...props} channelsList={[]} />);
|
||||
.add("empty", () => <ChannelsListPage {...props} channelsList={[]} />)
|
||||
.add("no limits", () => <ChannelsListPage {...props} limits={undefined} />)
|
||||
.add("limits reached", () => (
|
||||
<ChannelsListPage {...props} limits={limitsReached} />
|
||||
));
|
||||
|
|
|
@ -6,14 +6,17 @@ import TableCell from "@material-ui/core/TableCell";
|
|||
import TableHead from "@material-ui/core/TableHead";
|
||||
import TableRow from "@material-ui/core/TableRow";
|
||||
import DeleteIcon from "@material-ui/icons/Delete";
|
||||
import Alert from "@saleor/components/Alert/Alert";
|
||||
import AppHeader from "@saleor/components/AppHeader";
|
||||
import Container from "@saleor/components/Container";
|
||||
import PageHeader from "@saleor/components/PageHeader";
|
||||
import ResponsiveTable from "@saleor/components/ResponsiveTable";
|
||||
import { RefreshLimits_shop_limits } from "@saleor/components/Shop/types/RefreshLimits";
|
||||
import Skeleton from "@saleor/components/Skeleton";
|
||||
import TableCellHeader from "@saleor/components/TableCellHeader";
|
||||
import { sectionNames } from "@saleor/intl";
|
||||
import { renderCollection, stopPropagation } from "@saleor/misc";
|
||||
import { hasLimits, isLimitReached } from "@saleor/utils/limits";
|
||||
import React from "react";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
|
||||
|
@ -22,6 +25,7 @@ import { useStyles } from "./styles";
|
|||
|
||||
export interface ChannelsListPageProps {
|
||||
channelsList: Channels_channels[] | undefined;
|
||||
limits: RefreshLimits_shop_limits;
|
||||
navigateToChannelCreate: () => void;
|
||||
onBack: () => void;
|
||||
onRowClick: (id: string) => () => void;
|
||||
|
@ -32,6 +36,7 @@ const numberOfColumns = 2;
|
|||
|
||||
export const ChannelsListPage: React.FC<ChannelsListPageProps> = ({
|
||||
channelsList,
|
||||
limits,
|
||||
navigateToChannelCreate,
|
||||
onBack,
|
||||
onRemove,
|
||||
|
@ -40,13 +45,25 @@ export const ChannelsListPage: React.FC<ChannelsListPageProps> = ({
|
|||
const intl = useIntl();
|
||||
const classes = useStyles({});
|
||||
|
||||
const limitReached = isLimitReached(limits, "channels");
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<AppHeader onBack={onBack}>
|
||||
{intl.formatMessage(sectionNames.configuration)}
|
||||
</AppHeader>
|
||||
<PageHeader title={intl.formatMessage(sectionNames.channels)}>
|
||||
<PageHeader
|
||||
title={intl.formatMessage(sectionNames.channels)}
|
||||
limit={
|
||||
hasLimits(limits, "channels") && {
|
||||
data: limits,
|
||||
key: "channels",
|
||||
text: "channels used"
|
||||
}
|
||||
}
|
||||
>
|
||||
<Button
|
||||
disabled={limitReached}
|
||||
onClick={navigateToChannelCreate}
|
||||
color="primary"
|
||||
variant="contained"
|
||||
|
@ -58,6 +75,15 @@ export const ChannelsListPage: React.FC<ChannelsListPageProps> = ({
|
|||
/>
|
||||
</Button>
|
||||
</PageHeader>
|
||||
<Alert
|
||||
show={limitReached}
|
||||
title={intl.formatMessage({
|
||||
defaultMessage: "Channel limit reached",
|
||||
description: "alert"
|
||||
})}
|
||||
>
|
||||
<FormattedMessage defaultMessage="You have reached your channel limit, you will be no longer able to add channels to your store. If you would like to up your limit, contact your administration staff about raising your limits." />
|
||||
</Alert>
|
||||
<Card>
|
||||
<ResponsiveTable>
|
||||
<TableHead>
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { getChannelsCurrencyChoices } from "@saleor/channels/utils";
|
||||
import { useShopLimitsQuery } from "@saleor/components/Shop/query";
|
||||
import { configurationMenuUrl } from "@saleor/configuration";
|
||||
import useNavigator from "@saleor/hooks/useNavigator";
|
||||
import useNotifier from "@saleor/hooks/useNotifier";
|
||||
|
@ -30,8 +31,13 @@ export const ChannelsList: React.FC<ChannelsListProps> = ({ params }) => {
|
|||
const intl = useIntl();
|
||||
|
||||
const { data, refetch } = useChannelsList({ displayLoader: true });
|
||||
const limitOpts = useShopLimitsQuery({
|
||||
variables: {
|
||||
channels: true
|
||||
}
|
||||
});
|
||||
|
||||
const selectedChannel = data?.channels.find(
|
||||
const selectedChannel = data?.channels?.find(
|
||||
channel => channel.id === params?.id
|
||||
);
|
||||
|
||||
|
@ -50,6 +56,7 @@ export const ChannelsList: React.FC<ChannelsListProps> = ({ params }) => {
|
|||
})
|
||||
});
|
||||
refetch();
|
||||
limitOpts.refetch();
|
||||
closeModal();
|
||||
} else {
|
||||
errors.map(error =>
|
||||
|
@ -89,6 +96,7 @@ export const ChannelsList: React.FC<ChannelsListProps> = ({ params }) => {
|
|||
<>
|
||||
<ChannelsListPage
|
||||
channelsList={data?.channels}
|
||||
limits={limitOpts.data?.shop.limits}
|
||||
navigateToChannelCreate={navigateToChannelCreate}
|
||||
onBack={() => navigate(configurationMenuUrl)}
|
||||
onRowClick={id => () => navigate(channelUrl(id))}
|
||||
|
|
13
src/components/Alert/Alert.stories.tsx
Normal file
13
src/components/Alert/Alert.stories.tsx
Normal file
|
@ -0,0 +1,13 @@
|
|||
import Decorator from "@saleor/storybook/Decorator";
|
||||
import { storiesOf } from "@storybook/react";
|
||||
import React from "react";
|
||||
|
||||
import Alert from "./Alert";
|
||||
|
||||
storiesOf("Generics / Alert", module)
|
||||
.addDecorator(Decorator)
|
||||
.add("default", () => (
|
||||
<Alert show={true} title="Title">
|
||||
Content
|
||||
</Alert>
|
||||
));
|
84
src/components/Alert/Alert.tsx
Normal file
84
src/components/Alert/Alert.tsx
Normal file
|
@ -0,0 +1,84 @@
|
|||
import alertIconDark from "@assets/images/alert-dark.svg";
|
||||
import alertIcon from "@assets/images/alert.svg";
|
||||
import Card from "@material-ui/core/Card";
|
||||
import CardContent from "@material-ui/core/CardContent";
|
||||
import IconButton from "@material-ui/core/IconButton";
|
||||
import Typography from "@material-ui/core/Typography";
|
||||
import CloseIcon from "@material-ui/icons/Close";
|
||||
import useStateFromProps from "@saleor/hooks/useStateFromProps";
|
||||
import useTheme from "@saleor/hooks/useTheme";
|
||||
import { makeStyles } from "@saleor/theme";
|
||||
import classNames from "classnames";
|
||||
import React from "react";
|
||||
import SVG from "react-inlinesvg";
|
||||
|
||||
const useStyles = makeStyles(
|
||||
theme => ({
|
||||
close: {
|
||||
color: theme.palette.common.black,
|
||||
position: "absolute",
|
||||
right: theme.spacing(-1),
|
||||
top: theme.spacing(-2)
|
||||
},
|
||||
content: {
|
||||
color: theme.palette.getContrastText(theme.palette.alert.paper.warning),
|
||||
columnGap: theme.spacing(2) + "px",
|
||||
display: "grid",
|
||||
gridTemplateColumns: "40px 1fr"
|
||||
},
|
||||
icon: {
|
||||
color: theme.palette.alert.icon.warning
|
||||
},
|
||||
root: {
|
||||
background: theme.palette.alert.paper.warning,
|
||||
marginBottom: theme.spacing(3)
|
||||
},
|
||||
titleBar: {
|
||||
margin: theme.spacing(1, 0),
|
||||
position: "relative"
|
||||
}
|
||||
}),
|
||||
{ name: "Alert" }
|
||||
);
|
||||
|
||||
export interface AlertProps {
|
||||
className?: string;
|
||||
show: boolean;
|
||||
title: string;
|
||||
}
|
||||
|
||||
const Alert: React.FC<AlertProps> = ({ children, className, show, title }) => {
|
||||
const classes = useStyles({});
|
||||
const [visible, setVisible] = useStateFromProps(show);
|
||||
const { isDark } = useTheme();
|
||||
|
||||
return visible ? (
|
||||
<Card elevation={0} className={classNames(classes.root, className)}>
|
||||
<CardContent>
|
||||
<div className={classes.content}>
|
||||
<div>
|
||||
<SVG
|
||||
className={classes.icon}
|
||||
src={isDark ? alertIconDark : alertIcon}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div className={classes.titleBar}>
|
||||
<Typography variant="h5">{title}</Typography>
|
||||
<IconButton
|
||||
className={classes.close}
|
||||
onClick={() => setVisible(false)}
|
||||
>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : null;
|
||||
};
|
||||
|
||||
Alert.displayName = "Alert";
|
||||
export default Alert;
|
0
src/components/Alert/index.ts
Normal file
0
src/components/Alert/index.ts
Normal file
|
@ -1,13 +1,19 @@
|
|||
import Typography from "@material-ui/core/Typography";
|
||||
import { LimitInfoFragment } from "@saleor/fragments/types/LimitInfoFragment";
|
||||
import { makeStyles } from "@saleor/theme";
|
||||
import React from "react";
|
||||
|
||||
import ExtendedPageHeader from "../ExtendedPageHeader";
|
||||
import { RefreshLimits_shop_limits } from "../Shop/types/RefreshLimits";
|
||||
import Skeleton from "../Skeleton";
|
||||
|
||||
const useStyles = makeStyles(
|
||||
theme => ({
|
||||
limit: {
|
||||
marginRight: theme.spacing(3)
|
||||
},
|
||||
root: {
|
||||
alignItems: "center",
|
||||
display: "flex"
|
||||
},
|
||||
title: {
|
||||
|
@ -24,15 +30,27 @@ const useStyles = makeStyles(
|
|||
{ name: "PageHeader" }
|
||||
);
|
||||
|
||||
interface LimitInfo {
|
||||
data: RefreshLimits_shop_limits;
|
||||
key: keyof LimitInfoFragment;
|
||||
text: string;
|
||||
}
|
||||
interface PageHeaderProps {
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
inline?: boolean;
|
||||
limit?: LimitInfo;
|
||||
title?: React.ReactNode;
|
||||
}
|
||||
|
||||
function formatLimit(limit: LimitInfo): string {
|
||||
return `${limit.data.currentUsage[limit.key]}/${
|
||||
limit.data.allowedUsage[limit.key]
|
||||
} ${limit.text}`;
|
||||
}
|
||||
|
||||
const PageHeader: React.FC<PageHeaderProps> = props => {
|
||||
const { children, className, inline, title } = props;
|
||||
const { children, className, inline, limit, title } = props;
|
||||
|
||||
const classes = useStyles(props);
|
||||
|
||||
|
@ -47,7 +65,14 @@ const PageHeader: React.FC<PageHeaderProps> = props => {
|
|||
</Typography>
|
||||
}
|
||||
>
|
||||
<div className={classes.root}>{children}</div>
|
||||
<div className={classes.root}>
|
||||
{limit && (
|
||||
<Typography className={classes.limit} color="textSecondary">
|
||||
{formatLimit(limit)}
|
||||
</Typography>
|
||||
)}
|
||||
{children}
|
||||
</div>
|
||||
</ExtendedPageHeader>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
import { limitFragment } from "@saleor/fragments/shop";
|
||||
import makeQuery from "@saleor/hooks/makeQuery";
|
||||
import gql from "graphql-tag";
|
||||
|
||||
import { TypedQuery } from "../../queries";
|
||||
import { RefreshLimits, RefreshLimitsVariables } from "./types/RefreshLimits";
|
||||
import { ShopInfo } from "./types/ShopInfo";
|
||||
|
||||
const shopInfo = gql`
|
||||
|
@ -35,3 +38,22 @@ const shopInfo = gql`
|
|||
}
|
||||
`;
|
||||
export const TypedShopInfoQuery = TypedQuery<ShopInfo, {}>(shopInfo);
|
||||
|
||||
const limitInfo = gql`
|
||||
${limitFragment}
|
||||
query RefreshLimits(
|
||||
$channels: Boolean = false
|
||||
$orders: Boolean = false
|
||||
$productVariants: Boolean = false
|
||||
$staffUsers: Boolean = false
|
||||
$warehouses: Boolean = false
|
||||
) {
|
||||
shop {
|
||||
...ShopLimitFragment
|
||||
}
|
||||
}
|
||||
`;
|
||||
export const useShopLimitsQuery = makeQuery<
|
||||
RefreshLimits,
|
||||
RefreshLimitsVariables
|
||||
>(limitInfo);
|
||||
|
|
49
src/components/Shop/types/RefreshLimits.ts
Normal file
49
src/components/Shop/types/RefreshLimits.ts
Normal file
|
@ -0,0 +1,49 @@
|
|||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
// @generated
|
||||
// This file was automatically generated and should not be edited.
|
||||
|
||||
// ====================================================
|
||||
// GraphQL query operation: RefreshLimits
|
||||
// ====================================================
|
||||
|
||||
export interface RefreshLimits_shop_limits_currentUsage {
|
||||
__typename: "Limits";
|
||||
channels: number | null;
|
||||
orders: number | null;
|
||||
productVariants: number | null;
|
||||
staffUsers: number | null;
|
||||
warehouses: number | null;
|
||||
}
|
||||
|
||||
export interface RefreshLimits_shop_limits_allowedUsage {
|
||||
__typename: "Limits";
|
||||
channels: number | null;
|
||||
orders: number | null;
|
||||
productVariants: number | null;
|
||||
staffUsers: number | null;
|
||||
warehouses: number | null;
|
||||
}
|
||||
|
||||
export interface RefreshLimits_shop_limits {
|
||||
__typename: "LimitInfo";
|
||||
currentUsage: RefreshLimits_shop_limits_currentUsage;
|
||||
allowedUsage: RefreshLimits_shop_limits_allowedUsage;
|
||||
}
|
||||
|
||||
export interface RefreshLimits_shop {
|
||||
__typename: "Shop";
|
||||
limits: RefreshLimits_shop_limits;
|
||||
}
|
||||
|
||||
export interface RefreshLimits {
|
||||
shop: RefreshLimits_shop;
|
||||
}
|
||||
|
||||
export interface RefreshLimitsVariables {
|
||||
channels?: boolean | null;
|
||||
orders?: boolean | null;
|
||||
productVariants?: boolean | null;
|
||||
staffUsers?: boolean | null;
|
||||
warehouses?: boolean | null;
|
||||
}
|
|
@ -1,3 +1,5 @@
|
|||
import { AlertColors } from "@saleor/theme";
|
||||
|
||||
export type IThemeColors = Record<
|
||||
"primary" | "secondary" | "error" | "paperBorder" | "autofill",
|
||||
string
|
||||
|
@ -28,11 +30,27 @@ export type IThemeColors = Record<
|
|||
| "textHover",
|
||||
string
|
||||
>;
|
||||
} & {
|
||||
alert: AlertColors;
|
||||
} & {
|
||||
theme: "dark" | "light";
|
||||
};
|
||||
|
||||
export const dark: IThemeColors = {
|
||||
alert: {
|
||||
paper: {
|
||||
error: "#E0444E",
|
||||
info: "#2E2F31",
|
||||
success: "#5DC292",
|
||||
warning: "#E29A2E"
|
||||
},
|
||||
icon: {
|
||||
error: "#FE6E76",
|
||||
info: "#FAFAFA",
|
||||
success: "#5DC292",
|
||||
warning: "#FFB84E"
|
||||
}
|
||||
},
|
||||
highlightInactive: {
|
||||
default: "#78797A"
|
||||
},
|
||||
|
@ -73,6 +91,20 @@ export const dark: IThemeColors = {
|
|||
theme: "dark"
|
||||
};
|
||||
export const light: IThemeColors = {
|
||||
alert: {
|
||||
paper: {
|
||||
error: "#FFD6D9",
|
||||
info: "#FFFFFF",
|
||||
success: "#DFF3E9",
|
||||
warning: "#FFF4E4"
|
||||
},
|
||||
icon: {
|
||||
error: "#FE6E76",
|
||||
info: "#28234A",
|
||||
success: "#5DC292",
|
||||
warning: "#FFB84E"
|
||||
}
|
||||
},
|
||||
highlightInactive: {
|
||||
default: "#C8C8C8"
|
||||
},
|
||||
|
|
|
@ -42,7 +42,7 @@ export const useStyles = makeStyles(
|
|||
position: "fixed",
|
||||
top: 0,
|
||||
width: "100%",
|
||||
zIndex: 1200
|
||||
zIndex: 10000
|
||||
},
|
||||
error: {
|
||||
"& > div": {
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { ShopInfo_shop_permissions } from "./components/Shop/types/ShopInfo";
|
||||
import { ShopLimitFragment_limits } from "./fragments/types/ShopLimitFragment";
|
||||
import { User_userPermissions } from "./fragments/types/User";
|
||||
import {
|
||||
FetchMoreProps,
|
||||
|
@ -503,3 +504,42 @@ export const address = {
|
|||
streetAddress1: "419 Ruiz Orchard Apt. 199",
|
||||
streetAddress2: "0238 Cremin Freeway"
|
||||
};
|
||||
|
||||
export const limits: ShopLimitFragment_limits = {
|
||||
__typename: "LimitInfo",
|
||||
allowedUsage: {
|
||||
__typename: "Limits",
|
||||
channels: 5,
|
||||
orders: 500,
|
||||
productVariants: 1000,
|
||||
staffUsers: 10,
|
||||
warehouses: 3
|
||||
},
|
||||
currentUsage: {
|
||||
__typename: "Limits",
|
||||
channels: 2,
|
||||
orders: 230,
|
||||
productVariants: 100,
|
||||
staffUsers: 7,
|
||||
warehouses: 1
|
||||
}
|
||||
};
|
||||
export const limitsReached: ShopLimitFragment_limits = {
|
||||
__typename: "LimitInfo",
|
||||
allowedUsage: {
|
||||
__typename: "Limits",
|
||||
channels: 5,
|
||||
orders: 500,
|
||||
productVariants: 1000,
|
||||
staffUsers: 10,
|
||||
warehouses: 3
|
||||
},
|
||||
currentUsage: {
|
||||
__typename: "Limits",
|
||||
channels: 5,
|
||||
orders: 500,
|
||||
productVariants: 1000,
|
||||
staffUsers: 10,
|
||||
warehouses: 3
|
||||
}
|
||||
};
|
||||
|
|
|
@ -2,6 +2,27 @@ import gql from "graphql-tag";
|
|||
|
||||
import { fragmentAddress } from "./address";
|
||||
|
||||
export const limitFragment = gql`
|
||||
fragment LimitInfoFragment on Limits {
|
||||
channels @include(if: $channels)
|
||||
orders @include(if: $orders)
|
||||
productVariants @include(if: $productVariants)
|
||||
staffUsers @include(if: $staffUsers)
|
||||
warehouses @include(if: $warehouses)
|
||||
}
|
||||
|
||||
fragment ShopLimitFragment on Shop {
|
||||
limits {
|
||||
currentUsage {
|
||||
...LimitInfoFragment
|
||||
}
|
||||
allowedUsage {
|
||||
...LimitInfoFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const shopFragment = gql`
|
||||
${fragmentAddress}
|
||||
fragment ShopFragment on Shop {
|
||||
|
|
17
src/fragments/types/LimitInfoFragment.ts
Normal file
17
src/fragments/types/LimitInfoFragment.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
// @generated
|
||||
// This file was automatically generated and should not be edited.
|
||||
|
||||
// ====================================================
|
||||
// GraphQL fragment: LimitInfoFragment
|
||||
// ====================================================
|
||||
|
||||
export interface LimitInfoFragment {
|
||||
__typename: "Limits";
|
||||
channels: number | null;
|
||||
orders: number | null;
|
||||
productVariants: number | null;
|
||||
staffUsers: number | null;
|
||||
warehouses: number | null;
|
||||
}
|
37
src/fragments/types/ShopLimitFragment.ts
Normal file
37
src/fragments/types/ShopLimitFragment.ts
Normal file
|
@ -0,0 +1,37 @@
|
|||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
// @generated
|
||||
// This file was automatically generated and should not be edited.
|
||||
|
||||
// ====================================================
|
||||
// GraphQL fragment: ShopLimitFragment
|
||||
// ====================================================
|
||||
|
||||
export interface ShopLimitFragment_limits_currentUsage {
|
||||
__typename: "Limits";
|
||||
channels: number | null;
|
||||
orders: number | null;
|
||||
productVariants: number | null;
|
||||
staffUsers: number | null;
|
||||
warehouses: number | null;
|
||||
}
|
||||
|
||||
export interface ShopLimitFragment_limits_allowedUsage {
|
||||
__typename: "Limits";
|
||||
channels: number | null;
|
||||
orders: number | null;
|
||||
productVariants: number | null;
|
||||
staffUsers: number | null;
|
||||
warehouses: number | null;
|
||||
}
|
||||
|
||||
export interface ShopLimitFragment_limits {
|
||||
__typename: "LimitInfo";
|
||||
currentUsage: ShopLimitFragment_limits_currentUsage;
|
||||
allowedUsage: ShopLimitFragment_limits_allowedUsage;
|
||||
}
|
||||
|
||||
export interface ShopLimitFragment {
|
||||
__typename: "Shop";
|
||||
limits: ShopLimitFragment_limits;
|
||||
}
|
|
@ -1,7 +1,8 @@
|
|||
import { isJwtError } from "@saleor/auth/errors";
|
||||
import { commonMessages } from "@saleor/intl";
|
||||
import { getMutationStatus, maybe } from "@saleor/misc";
|
||||
import { getMutationStatus } from "@saleor/misc";
|
||||
import { MutationResultAdditionalProps } from "@saleor/types";
|
||||
import { GqlErrors, hasError } from "@saleor/utils/api";
|
||||
import { ApolloError } from "apollo-client";
|
||||
import { DocumentNode } from "graphql";
|
||||
import {
|
||||
|
@ -40,13 +41,7 @@ function makeMutation<TData, TVariables>(
|
|||
const [mutateFn, result] = useBaseMutation(mutation, {
|
||||
onCompleted,
|
||||
onError: (err: ApolloError) => {
|
||||
if (
|
||||
maybe(
|
||||
() =>
|
||||
err.graphQLErrors[0].extensions.exception.code ===
|
||||
"ReadOnlyException"
|
||||
)
|
||||
) {
|
||||
if (hasError(err, GqlErrors.ReadOnlyException)) {
|
||||
notify({
|
||||
status: "error",
|
||||
text: intl.formatMessage(commonMessages.readOnly)
|
||||
|
@ -57,7 +52,7 @@ function makeMutation<TData, TVariables>(
|
|||
status: "error",
|
||||
text: intl.formatMessage(commonMessages.sessionExpired)
|
||||
});
|
||||
} else {
|
||||
} else if (!hasError(err, GqlErrors.LimitReachedException)) {
|
||||
notify({
|
||||
status: "error",
|
||||
text: intl.formatMessage(commonMessages.somethingWentWrong)
|
||||
|
|
|
@ -112,7 +112,7 @@ function useForm<T extends FormData>(
|
|||
const result = onSubmit(data);
|
||||
if (result) {
|
||||
const errors = await result;
|
||||
if (errors.length === 0) {
|
||||
if (errors?.length === 0) {
|
||||
setChanged(false);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -55,6 +55,9 @@ export const commonMessages = defineMessages({
|
|||
lastName: {
|
||||
defaultMessage: "Last Name"
|
||||
},
|
||||
limitReached: {
|
||||
defaultMessage: "Reached limit for this plan"
|
||||
},
|
||||
no: {
|
||||
defaultMessage: "No"
|
||||
},
|
||||
|
|
|
@ -10,6 +10,7 @@ import useUser from "./hooks/useUser";
|
|||
import { commonMessages } from "./intl";
|
||||
import { getMutationStatus } from "./misc";
|
||||
import { MutationResultAdditionalProps } from "./types";
|
||||
import { GqlErrors, hasError } from "./utils/api";
|
||||
|
||||
export interface TypedMutationInnerProps<TData, TVariables> {
|
||||
children: (
|
||||
|
@ -43,10 +44,7 @@ export function TypedMutation<TData, TVariables>(
|
|||
text: intl.formatMessage(commonMessages.somethingWentWrong)
|
||||
});
|
||||
}
|
||||
if (
|
||||
err.graphQLErrors[0].extensions.exception?.code ===
|
||||
"ReadOnlyException"
|
||||
) {
|
||||
if (hasError(err, GqlErrors.ReadOnlyException)) {
|
||||
notify({
|
||||
status: "error",
|
||||
text: intl.formatMessage(commonMessages.readOnly)
|
||||
|
@ -57,7 +55,7 @@ export function TypedMutation<TData, TVariables>(
|
|||
status: "error",
|
||||
text: intl.formatMessage(commonMessages.sessionExpired)
|
||||
});
|
||||
} else {
|
||||
} else if (!hasError(err, GqlErrors.LimitReachedException)) {
|
||||
notify({
|
||||
status: "error",
|
||||
text: intl.formatMessage(commonMessages.somethingWentWrong)
|
||||
|
|
|
@ -1,13 +1,16 @@
|
|||
import Button from "@material-ui/core/Button";
|
||||
import Card from "@material-ui/core/Card";
|
||||
import Alert from "@saleor/components/Alert/Alert";
|
||||
import CardMenu from "@saleor/components/CardMenu";
|
||||
import Container from "@saleor/components/Container";
|
||||
import FilterBar from "@saleor/components/FilterBar";
|
||||
import PageHeader from "@saleor/components/PageHeader";
|
||||
import { RefreshLimits_shop_limits } from "@saleor/components/Shop/types/RefreshLimits";
|
||||
import { sectionNames } from "@saleor/intl";
|
||||
import { OrderListUrlSortField } from "@saleor/orders/urls";
|
||||
import { makeStyles } from "@saleor/theme";
|
||||
import { FilterPageProps, PageListProps, SortPage } from "@saleor/types";
|
||||
import { isLimitReached } from "@saleor/utils/limits";
|
||||
import React from "react";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
|
||||
|
@ -23,6 +26,7 @@ export interface OrderListPageProps
|
|||
extends PageListProps,
|
||||
FilterPageProps<OrderFilterKeys, OrderListFilterOpts>,
|
||||
SortPage<OrderListUrlSortField> {
|
||||
limits: RefreshLimits_shop_limits;
|
||||
orders: OrderList_orders_edges_node[];
|
||||
onSettingsOpen: () => void;
|
||||
}
|
||||
|
@ -40,6 +44,7 @@ const OrderListPage: React.FC<OrderListPageProps> = ({
|
|||
currentTab,
|
||||
initialSearch,
|
||||
filterOpts,
|
||||
limits,
|
||||
tabs,
|
||||
onAdd,
|
||||
onAll,
|
||||
|
@ -84,6 +89,15 @@ const OrderListPage: React.FC<OrderListPageProps> = ({
|
|||
/>
|
||||
</Button>
|
||||
</PageHeader>
|
||||
<Alert
|
||||
show={isLimitReached(limits, "orders")}
|
||||
title={intl.formatMessage({
|
||||
defaultMessage: "Order limit reached",
|
||||
description: "alert"
|
||||
})}
|
||||
>
|
||||
<FormattedMessage defaultMessage="You have reached your order limit, you will be billed extra for orders above limit. If you would like to up your limit, contact your administration staff about raising your limits." />
|
||||
</Alert>
|
||||
<Card>
|
||||
<FilterBar
|
||||
currentTab={currentTab}
|
||||
|
|
|
@ -4,6 +4,7 @@ import DeleteFilterTabDialog from "@saleor/components/DeleteFilterTabDialog";
|
|||
import SaveFilterTabDialog, {
|
||||
SaveFilterTabDialogFormData
|
||||
} from "@saleor/components/SaveFilterTabDialog";
|
||||
import { useShopLimitsQuery } from "@saleor/components/Shop/query";
|
||||
import useListSettings from "@saleor/hooks/useListSettings";
|
||||
import useNavigator from "@saleor/hooks/useNavigator";
|
||||
import useNotifier from "@saleor/hooks/useNotifier";
|
||||
|
@ -71,6 +72,11 @@ export const OrderList: React.FC<OrderListProps> = ({ params }) => {
|
|||
});
|
||||
|
||||
const { channel, availableChannels } = useAppChannel();
|
||||
const limitOpts = useShopLimitsQuery({
|
||||
variables: {
|
||||
orders: true
|
||||
}
|
||||
});
|
||||
|
||||
const noChannel = !channel && typeof channel !== "undefined";
|
||||
const channelOpts = availableChannels
|
||||
|
@ -150,6 +156,7 @@ export const OrderList: React.FC<OrderListProps> = ({ params }) => {
|
|||
currentTab={currentTab}
|
||||
disabled={loading}
|
||||
filterOpts={getFilterOpts(params, channelOpts)}
|
||||
limits={limitOpts.data?.shop.limits}
|
||||
orders={maybe(() => data.orders.edges.map(edge => edge.node))}
|
||||
pageInfo={pageInfo}
|
||||
sort={getSortParams(params)}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import Button from "@material-ui/core/Button";
|
||||
import Card from "@material-ui/core/Card";
|
||||
import Alert from "@saleor/components/Alert/Alert";
|
||||
import CardMenu from "@saleor/components/CardMenu";
|
||||
import ColumnPicker, {
|
||||
ColumnPickerChoice
|
||||
|
@ -7,6 +8,7 @@ import ColumnPicker, {
|
|||
import Container from "@saleor/components/Container";
|
||||
import FilterBar from "@saleor/components/FilterBar";
|
||||
import PageHeader from "@saleor/components/PageHeader";
|
||||
import { RefreshLimits_shop_limits } from "@saleor/components/Shop/types/RefreshLimits";
|
||||
import { ProductListColumns } from "@saleor/config";
|
||||
import { sectionNames } from "@saleor/intl";
|
||||
import {
|
||||
|
@ -23,6 +25,7 @@ import {
|
|||
PageListProps,
|
||||
SortPage
|
||||
} from "@saleor/types";
|
||||
import { hasLimits, isLimitReached } from "@saleor/utils/limits";
|
||||
import React from "react";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
|
||||
|
@ -46,6 +49,7 @@ export interface ProductListPageProps
|
|||
channelsCount: number;
|
||||
currencySymbol: string;
|
||||
gridAttributes: GridAttributes_grid_edges_node[];
|
||||
limits: RefreshLimits_shop_limits;
|
||||
totalGridAttributes: number;
|
||||
products: ProductList_products_edges_node[];
|
||||
onExport: () => void;
|
||||
|
@ -70,6 +74,7 @@ export const ProductListPage: React.FC<ProductListPageProps> = props => {
|
|||
currentTab,
|
||||
defaultSettings,
|
||||
gridAttributes,
|
||||
limits,
|
||||
availableInGridAttributes,
|
||||
filterOpts,
|
||||
hasMore,
|
||||
|
@ -120,9 +125,20 @@ export const ProductListPage: React.FC<ProductListPageProps> = props => {
|
|||
}))
|
||||
];
|
||||
|
||||
const limitReached = isLimitReached(limits, "productVariants");
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<PageHeader title={intl.formatMessage(sectionNames.products)}>
|
||||
<PageHeader
|
||||
title={intl.formatMessage(sectionNames.products)}
|
||||
limit={
|
||||
hasLimits(limits, "productVariants") && {
|
||||
data: limits,
|
||||
key: "productVariants",
|
||||
text: "SKUs used"
|
||||
}
|
||||
}
|
||||
>
|
||||
<CardMenu
|
||||
className={classes.settings}
|
||||
menuItems={[
|
||||
|
@ -153,6 +169,7 @@ export const ProductListPage: React.FC<ProductListPageProps> = props => {
|
|||
onSave={handleSave}
|
||||
/>
|
||||
<Button
|
||||
disabled={limitReached}
|
||||
onClick={onAdd}
|
||||
color="primary"
|
||||
variant="contained"
|
||||
|
@ -164,6 +181,15 @@ export const ProductListPage: React.FC<ProductListPageProps> = props => {
|
|||
/>
|
||||
</Button>
|
||||
</PageHeader>
|
||||
<Alert
|
||||
show={limitReached}
|
||||
title={intl.formatMessage({
|
||||
defaultMessage: "SKU limit reached",
|
||||
description: "alert"
|
||||
})}
|
||||
>
|
||||
<FormattedMessage defaultMessage="You have reached your SKU limit, you will be no longer able to add SKUs to your store. If you would like to up your limit, contact your administration staff about raising your limits." />
|
||||
</Alert>
|
||||
<Card>
|
||||
<FilterBar
|
||||
currencySymbol={currencySymbol}
|
||||
|
|
|
@ -2,7 +2,7 @@ import placeholderImage from "@assets/images/placeholder255x255.png";
|
|||
import { channelsList } from "@saleor/channels/fixtures";
|
||||
import { createChannelsData } from "@saleor/channels/utils";
|
||||
import { collections } from "@saleor/collections/fixtures";
|
||||
import { fetchMoreProps, listActionsProps } from "@saleor/fixtures";
|
||||
import { fetchMoreProps, limits, listActionsProps } from "@saleor/fixtures";
|
||||
import { product as productFixture } from "@saleor/products/fixtures";
|
||||
import { taxTypes } from "@saleor/storybook/stories/taxes/fixtures";
|
||||
import { warehouseList } from "@saleor/warehouses/fixtures";
|
||||
|
@ -42,6 +42,7 @@ const props: ProductUpdatePageProps = {
|
|||
hasChannelChanged: false,
|
||||
header: product.name,
|
||||
media: product.media,
|
||||
limits,
|
||||
onAssignReferencesClick: () => undefined,
|
||||
onBack: () => undefined,
|
||||
onChannelsChange: () => undefined,
|
||||
|
|
|
@ -16,6 +16,7 @@ import Metadata from "@saleor/components/Metadata/Metadata";
|
|||
import PageHeader from "@saleor/components/PageHeader";
|
||||
import SaveButtonBar from "@saleor/components/SaveButtonBar";
|
||||
import SeoForm from "@saleor/components/SeoForm";
|
||||
import { RefreshLimits_shop_limits } from "@saleor/components/Shop/types/RefreshLimits";
|
||||
import { SingleAutocompleteChoiceType } from "@saleor/components/SingleAutocompleteSelectField";
|
||||
import { ProductChannelListingErrorFragment } from "@saleor/fragments/types/ProductChannelListingErrorFragment";
|
||||
import { ProductErrorWithAttributesFragment } from "@saleor/fragments/types/ProductErrorWithAttributesFragment";
|
||||
|
@ -73,6 +74,7 @@ export interface ProductUpdatePageProps extends ListActions, ChannelProps {
|
|||
fetchMoreCategories: FetchMoreProps;
|
||||
fetchMoreCollections: FetchMoreProps;
|
||||
isMediaUrlModalVisible?: boolean;
|
||||
limits: RefreshLimits_shop_limits;
|
||||
variants: ProductDetails_product_variants[];
|
||||
media: ProductDetails_product_media[];
|
||||
hasChannelChanged: boolean;
|
||||
|
@ -139,6 +141,7 @@ export const ProductUpdatePage: React.FC<ProductUpdatePageProps> = ({
|
|||
media,
|
||||
hasChannelChanged,
|
||||
header,
|
||||
limits,
|
||||
placeholderImage,
|
||||
product,
|
||||
saveButtonBarState,
|
||||
|
@ -310,6 +313,7 @@ export const ProductUpdatePage: React.FC<ProductUpdatePageProps> = ({
|
|||
{hasVariants ? (
|
||||
<ProductVariants
|
||||
disabled={disabled}
|
||||
limits={limits}
|
||||
variants={variants}
|
||||
product={product}
|
||||
onRowClick={onVariantShow}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { attributes } from "@saleor/attributes/fixtures";
|
||||
import { productChannels } from "@saleor/channels/fixtures";
|
||||
import Container from "@saleor/components/Container";
|
||||
import { limitsReached } from "@saleor/fixtures";
|
||||
import { ProductVariantBulkCreate_productVariantBulkCreate_errors } from "@saleor/products/types/ProductVariantBulkCreate";
|
||||
import { ProductErrorCode } from "@saleor/types/globalTypes";
|
||||
import { warehouseList } from "@saleor/warehouses/fixtures";
|
||||
|
@ -98,6 +99,7 @@ const props: ProductVariantCreatorContentProps = {
|
|||
},
|
||||
dispatchFormDataAction: () => undefined,
|
||||
errors: [],
|
||||
variantsLeft: 6,
|
||||
step: ProductVariantCreatorStep.values,
|
||||
warehouses: warehouseList
|
||||
};
|
||||
|
@ -190,5 +192,15 @@ storiesOf("Views / Products / Create multiple variants / summary", module)
|
|||
storiesOf("Views / Products / Create multiple variants", module)
|
||||
.addDecorator(Decorator)
|
||||
.add("interactive", () => (
|
||||
<ProductVariantCreatorPage {...props} onSubmit={() => undefined} />
|
||||
<ProductVariantCreatorPage
|
||||
{...props}
|
||||
limits={{
|
||||
...limitsReached,
|
||||
currentUsage: {
|
||||
...limitsReached.currentUsage,
|
||||
productVariants: limitsReached.currentUsage.productVariants - 6
|
||||
}
|
||||
}}
|
||||
onSubmit={() => undefined}
|
||||
/>
|
||||
));
|
||||
|
|
|
@ -22,6 +22,7 @@ export interface ProductVariantCreatorContentProps {
|
|||
dispatchFormDataAction: React.Dispatch<ProductVariantCreateReducerAction>;
|
||||
errors: ProductVariantBulkCreate_productVariantBulkCreate_errors[];
|
||||
step: ProductVariantCreatorStep;
|
||||
variantsLeft: number | null;
|
||||
warehouses: WarehouseFragment[];
|
||||
}
|
||||
|
||||
|
@ -32,6 +33,7 @@ const ProductVariantCreatorContent: React.FC<ProductVariantCreatorContentProps>
|
|||
dispatchFormDataAction,
|
||||
errors,
|
||||
step,
|
||||
variantsLeft,
|
||||
warehouses
|
||||
}) => {
|
||||
const selectedAttributes = attributes.filter(attribute =>
|
||||
|
@ -48,6 +50,7 @@ const ProductVariantCreatorContent: React.FC<ProductVariantCreatorContentProps>
|
|||
<ProductVariantCreateValues
|
||||
attributes={selectedAttributes}
|
||||
data={data}
|
||||
variantsLeft={variantsLeft}
|
||||
onValueClick={(attributeId, valueId) =>
|
||||
dispatchFormDataAction({
|
||||
selectValue: {
|
||||
|
|
|
@ -3,6 +3,7 @@ import Typography from "@material-ui/core/Typography";
|
|||
import Container from "@saleor/components/Container";
|
||||
import Hr from "@saleor/components/Hr";
|
||||
import PageHeader from "@saleor/components/PageHeader";
|
||||
import { RefreshLimits_shop_limits } from "@saleor/components/Shop/types/RefreshLimits";
|
||||
import useWizard from "@saleor/hooks/useWizard";
|
||||
import { validatePrice } from "@saleor/products/utils/validation";
|
||||
import { makeStyles } from "@saleor/theme";
|
||||
|
@ -15,6 +16,7 @@ import ProductVariantCreatorContent, {
|
|||
ProductVariantCreatorContentProps
|
||||
} from "./ProductVariantCreatorContent";
|
||||
import ProductVariantCreateTabs from "./ProductVariantCreatorTabs";
|
||||
import { getVariantsNumber } from "./ProductVariantCreatorValues";
|
||||
import reduceProductVariantCreateFormData, {
|
||||
ProductVariantCreateReducerActionType
|
||||
} from "./reducer";
|
||||
|
@ -42,11 +44,16 @@ const useStyles = makeStyles(
|
|||
|
||||
function canHitNext(
|
||||
step: ProductVariantCreatorStep,
|
||||
data: ProductVariantCreateFormData
|
||||
data: ProductVariantCreateFormData,
|
||||
variantsLeft: number | null
|
||||
): boolean {
|
||||
switch (step) {
|
||||
case ProductVariantCreatorStep.values:
|
||||
return data.attributes.every(attribute => attribute.values.length > 0);
|
||||
return (
|
||||
(data.attributes.every(attribute => attribute.values.length > 0) &&
|
||||
variantsLeft === null) ||
|
||||
getVariantsNumber(data) <= variantsLeft
|
||||
);
|
||||
case ProductVariantCreatorStep.prices:
|
||||
if (data.price.mode === "all") {
|
||||
if (data.price.channels.some(channel => validatePrice(channel.price))) {
|
||||
|
@ -85,8 +92,9 @@ function canHitNext(
|
|||
export interface ProductVariantCreatePageProps
|
||||
extends Omit<
|
||||
ProductVariantCreatorContentProps,
|
||||
"data" | "dispatchFormDataAction" | "step" | "onStepClick"
|
||||
"data" | "dispatchFormDataAction" | "step" | "variantsLeft" | "onStepClick"
|
||||
> {
|
||||
limits: RefreshLimits_shop_limits;
|
||||
onSubmit: (data: ProductVariantBulkCreateInput[]) => void;
|
||||
}
|
||||
|
||||
|
@ -138,6 +146,7 @@ const ProductVariantCreatePage: React.FC<ProductVariantCreatePageProps> = props
|
|||
attributes,
|
||||
channelListings,
|
||||
errors,
|
||||
limits,
|
||||
onSubmit,
|
||||
warehouses,
|
||||
...contentProps
|
||||
|
@ -177,6 +186,10 @@ const ProductVariantCreatePage: React.FC<ProductVariantCreatePageProps> = props
|
|||
|
||||
React.useEffect(reloadForm, [attributes.length, warehouses.length]);
|
||||
|
||||
const variantsLeft = limits.allowedUsage.productVariants
|
||||
? limits.allowedUsage.productVariants - limits.currentUsage.productVariants
|
||||
: null;
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<ProductVariantCreateTabs step={step} onStepClick={setStep} />
|
||||
|
@ -203,7 +216,7 @@ const ProductVariantCreatePage: React.FC<ProductVariantCreatePageProps> = props
|
|||
data-test-id="next-step"
|
||||
className={classes.button}
|
||||
color="primary"
|
||||
disabled={!canHitNext(step, wizardData)}
|
||||
disabled={!canHitNext(step, wizardData, variantsLeft)}
|
||||
variant="contained"
|
||||
onClick={nextStep}
|
||||
>
|
||||
|
@ -213,7 +226,7 @@ const ProductVariantCreatePage: React.FC<ProductVariantCreatePageProps> = props
|
|||
<Button
|
||||
className={classes.button}
|
||||
color="primary"
|
||||
disabled={!canHitNext(step, wizardData)}
|
||||
disabled={!canHitNext(step, wizardData, variantsLeft)}
|
||||
variant="contained"
|
||||
onClick={() => onSubmit(wizardData.variants)}
|
||||
>
|
||||
|
@ -232,6 +245,7 @@ const ProductVariantCreatePage: React.FC<ProductVariantCreatePageProps> = props
|
|||
data={wizardData}
|
||||
dispatchFormDataAction={dispatchFormDataAction}
|
||||
errors={errors}
|
||||
variantsLeft={variantsLeft}
|
||||
step={step}
|
||||
warehouses={warehouses}
|
||||
/>
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import Card from "@material-ui/core/Card";
|
||||
import CardContent from "@material-ui/core/CardContent";
|
||||
import Alert from "@saleor/components/Alert/Alert";
|
||||
import CardSpacer from "@saleor/components/CardSpacer";
|
||||
import CardTitle from "@saleor/components/CardTitle";
|
||||
import ControlledCheckbox from "@saleor/components/ControlledCheckbox";
|
||||
|
@ -9,12 +10,21 @@ import { ProductDetails_product_productType_variantAttributes } from "@saleor/pr
|
|||
import { makeStyles } from "@saleor/theme";
|
||||
import { isSelected } from "@saleor/utils/lists";
|
||||
import React from "react";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
|
||||
import { ProductVariantCreateFormData } from "./form";
|
||||
|
||||
export function getVariantsNumber(data: ProductVariantCreateFormData): number {
|
||||
return data.attributes.reduce(
|
||||
(variants, attribute) => variants * attribute.values.length,
|
||||
1
|
||||
);
|
||||
}
|
||||
|
||||
export interface ProductVariantCreatorValuesProps {
|
||||
attributes: ProductDetails_product_productType_variantAttributes[];
|
||||
data: ProductVariantCreateFormData;
|
||||
variantsLeft: number | null;
|
||||
onValueClick: (attributeId: string, valueId: string) => void;
|
||||
}
|
||||
|
||||
|
@ -30,11 +40,30 @@ const useStyles = makeStyles(
|
|||
);
|
||||
|
||||
const ProductVariantCreatorValues: React.FC<ProductVariantCreatorValuesProps> = props => {
|
||||
const { attributes, data, onValueClick } = props;
|
||||
const { attributes, data, variantsLeft, onValueClick } = props;
|
||||
const classes = useStyles(props);
|
||||
const intl = useIntl();
|
||||
const variantsNumber = getVariantsNumber(data);
|
||||
|
||||
return (
|
||||
<>
|
||||
{variantsLeft !== null && (
|
||||
<Alert
|
||||
show={variantsNumber > variantsLeft}
|
||||
title={intl.formatMessage({
|
||||
defaultMessage: "SKU limit reached",
|
||||
description: "alert"
|
||||
})}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="You choices will add {variantsNumber} SKUs to your catalog which will exceed your limit by {aboveLimitVariantsNumber}. If you would like to up your limit, contact your administration staff about raising your limits."
|
||||
values={{
|
||||
variantsNumber,
|
||||
aboveLimitVariantsNumber: variantsNumber - variantsLeft
|
||||
}}
|
||||
/>
|
||||
</Alert>
|
||||
)}
|
||||
{attributes.map(attribute => (
|
||||
<React.Fragment key={attribute.id}>
|
||||
<Card>
|
||||
|
|
|
@ -5,11 +5,13 @@ import Hidden from "@material-ui/core/Hidden";
|
|||
import { fade } from "@material-ui/core/styles/colorManipulator";
|
||||
import TableCell from "@material-ui/core/TableCell";
|
||||
import Typography from "@material-ui/core/Typography";
|
||||
import Alert from "@saleor/components/Alert/Alert";
|
||||
import CardTitle from "@saleor/components/CardTitle";
|
||||
import Checkbox from "@saleor/components/Checkbox";
|
||||
import LinkChoice from "@saleor/components/LinkChoice";
|
||||
import Money from "@saleor/components/Money";
|
||||
import ResponsiveTable from "@saleor/components/ResponsiveTable";
|
||||
import { RefreshLimits_shop_limits } from "@saleor/components/Shop/types/RefreshLimits";
|
||||
import { SingleAutocompleteChoiceType } from "@saleor/components/SingleAutocompleteSelectField";
|
||||
import Skeleton from "@saleor/components/Skeleton";
|
||||
import {
|
||||
|
@ -18,6 +20,7 @@ import {
|
|||
} from "@saleor/components/SortableTable";
|
||||
import TableHead from "@saleor/components/TableHead";
|
||||
import { makeStyles } from "@saleor/theme";
|
||||
import { isLimitReached } from "@saleor/utils/limits";
|
||||
import React from "react";
|
||||
import { FormattedMessage, IntlShape, useIntl } from "react-intl";
|
||||
|
||||
|
@ -82,6 +85,9 @@ const useStyles = makeStyles(
|
|||
width: 200
|
||||
}
|
||||
},
|
||||
alert: {
|
||||
margin: theme.spacing(3, 3, 0)
|
||||
},
|
||||
colGrab: {
|
||||
width: 60
|
||||
},
|
||||
|
@ -180,6 +186,7 @@ function getAvailabilityLabel(
|
|||
|
||||
interface ProductVariantsProps extends ListActions, ChannelProps {
|
||||
disabled: boolean;
|
||||
limits: RefreshLimits_shop_limits;
|
||||
product: ProductDetails_product;
|
||||
variants: ProductDetails_product_variants[];
|
||||
onVariantReorder: ReorderAction;
|
||||
|
@ -194,6 +201,7 @@ const numberOfColumns = 7;
|
|||
export const ProductVariants: React.FC<ProductVariantsProps> = props => {
|
||||
const {
|
||||
disabled,
|
||||
limits,
|
||||
variants,
|
||||
product,
|
||||
onRowClick,
|
||||
|
@ -213,6 +221,7 @@ export const ProductVariants: React.FC<ProductVariantsProps> = props => {
|
|||
const intl = useIntl();
|
||||
const [warehouse, setWarehouse] = React.useState<string>(null);
|
||||
const hasVariants = maybe(() => variants.length > 0, true);
|
||||
const limitReached = isLimitReached(limits, "productVariants");
|
||||
|
||||
return (
|
||||
<Card>
|
||||
|
@ -224,6 +233,7 @@ export const ProductVariants: React.FC<ProductVariantsProps> = props => {
|
|||
toolbar={
|
||||
hasVariants ? (
|
||||
<Button
|
||||
disabled={limitReached}
|
||||
onClick={onVariantAdd}
|
||||
variant="text"
|
||||
color="primary"
|
||||
|
@ -236,6 +246,7 @@ export const ProductVariants: React.FC<ProductVariantsProps> = props => {
|
|||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
disabled={limitReached}
|
||||
onClick={onVariantsAdd}
|
||||
variant="text"
|
||||
color="primary"
|
||||
|
@ -250,6 +261,17 @@ export const ProductVariants: React.FC<ProductVariantsProps> = props => {
|
|||
}
|
||||
/>
|
||||
|
||||
<Alert
|
||||
className={classes.alert}
|
||||
show={limitReached}
|
||||
title={intl.formatMessage({
|
||||
defaultMessage: "SKU limit reached",
|
||||
description: "alert"
|
||||
})}
|
||||
>
|
||||
<FormattedMessage defaultMessage="You have reached your SKU limit, you will be no longer able to add SKUs to your store. If you would like to up your limit, contact your administration staff about raising your limits." />
|
||||
</Alert>
|
||||
|
||||
{variants.length > 0 ? (
|
||||
<CardContent className={classes.warehouseSelectContainer}>
|
||||
<Typography className={classes.warehouseLabel}>
|
||||
|
|
|
@ -7,6 +7,7 @@ import DeleteFilterTabDialog from "@saleor/components/DeleteFilterTabDialog";
|
|||
import SaveFilterTabDialog, {
|
||||
SaveFilterTabDialogFormData
|
||||
} from "@saleor/components/SaveFilterTabDialog";
|
||||
import { useShopLimitsQuery } from "@saleor/components/Shop/query";
|
||||
import {
|
||||
DEFAULT_INITIAL_PAGINATION_DATA,
|
||||
DEFAULT_INITIAL_SEARCH_DATA,
|
||||
|
@ -150,6 +151,11 @@ export const ProductList: React.FC<ProductListProps> = ({ params }) => {
|
|||
skip: params.action !== "export"
|
||||
});
|
||||
const { availableChannels, channel } = useAppChannel();
|
||||
const limitOpts = useShopLimitsQuery({
|
||||
variables: {
|
||||
productVariants: true
|
||||
}
|
||||
});
|
||||
|
||||
const noChannel = !channel && typeof channel !== "undefined";
|
||||
|
||||
|
@ -303,6 +309,7 @@ export const ProductList: React.FC<ProductListProps> = ({ params }) => {
|
|||
});
|
||||
reset();
|
||||
refetch();
|
||||
limitOpts.refetch();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -370,6 +377,7 @@ export const ProductList: React.FC<ProductListProps> = ({ params }) => {
|
|||
)}
|
||||
onAdd={() => navigate(productAddUrl())}
|
||||
disabled={loading}
|
||||
limits={limitOpts.data?.shop.limits}
|
||||
products={maybe(() => data.products.edges.map(edge => edge.node))}
|
||||
onFetchMore={() =>
|
||||
attributes.loadMore(
|
||||
|
|
|
@ -14,6 +14,7 @@ import useAppChannel from "@saleor/components/AppLayout/AppChannelContext";
|
|||
import { AttributeInput } from "@saleor/components/Attributes";
|
||||
import ChannelsAvailabilityDialog from "@saleor/components/ChannelsAvailabilityDialog";
|
||||
import NotFoundPage from "@saleor/components/NotFoundPage";
|
||||
import { useShopLimitsQuery } from "@saleor/components/Shop/query";
|
||||
import { WindowTitle } from "@saleor/components/WindowTitle";
|
||||
import { DEFAULT_INITIAL_SEARCH_DATA } from "@saleor/config";
|
||||
import { useFileUploadMutation } from "@saleor/files/mutations";
|
||||
|
@ -136,6 +137,11 @@ export const ProductUpdate: React.FC<ProductUpdateProps> = ({ id, params }) => {
|
|||
variables: { id }
|
||||
});
|
||||
const { channel } = useAppChannel();
|
||||
const limitOpts = useShopLimitsQuery({
|
||||
variables: {
|
||||
productVariants: true
|
||||
}
|
||||
});
|
||||
|
||||
const [uploadFile, uploadFileOpts] = useFileUploadMutation({});
|
||||
|
||||
|
@ -208,6 +214,7 @@ export const ProductUpdate: React.FC<ProductUpdateProps> = ({ id, params }) => {
|
|||
closeModal();
|
||||
reset();
|
||||
refetch();
|
||||
limitOpts.refetch();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -459,6 +466,7 @@ export const ProductUpdate: React.FC<ProductUpdateProps> = ({ id, params }) => {
|
|||
channelsErrors={channelsErrors}
|
||||
fetchCategories={searchCategories}
|
||||
fetchCollections={searchCollections}
|
||||
limits={limitOpts.data?.shop.limits}
|
||||
saveButtonBarState={formTransitionState}
|
||||
media={data?.product?.media}
|
||||
header={product?.name}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { useShopLimitsQuery } from "@saleor/components/Shop/query";
|
||||
import { WindowTitle } from "@saleor/components/WindowTitle";
|
||||
import useNavigator from "@saleor/hooks/useNavigator";
|
||||
import useNotifier from "@saleor/hooks/useNotifier";
|
||||
|
@ -40,6 +41,11 @@ const ProductVariantCreator: React.FC<ProductVariantCreatorProps> = ({
|
|||
}
|
||||
}
|
||||
});
|
||||
const limitOpts = useShopLimitsQuery({
|
||||
variables: {
|
||||
productVariants: true
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -61,6 +67,7 @@ const ProductVariantCreator: React.FC<ProductVariantCreatorProps> = ({
|
|||
price: ""
|
||||
}))}
|
||||
attributes={data?.product?.productType?.variantAttributes || []}
|
||||
limits={limitOpts.data?.shop?.limits}
|
||||
onSubmit={inputs =>
|
||||
bulkProductVariantCreate({
|
||||
variables: { id, inputs }
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import {
|
||||
filterPageProps,
|
||||
limits,
|
||||
limitsReached,
|
||||
pageListProps,
|
||||
searchPageProps,
|
||||
sortPageProps,
|
||||
|
@ -26,6 +28,7 @@ const props: StaffListPageProps = {
|
|||
value: StaffMemberStatus.ACTIVE
|
||||
}
|
||||
},
|
||||
limits,
|
||||
onAdd: undefined,
|
||||
onBack: () => undefined,
|
||||
sort: {
|
||||
|
@ -40,4 +43,8 @@ storiesOf("Views / Staff / Staff members", module)
|
|||
.add("default", () => <StaffListPage {...props} />)
|
||||
.add("when loading", () => (
|
||||
<StaffListPage {...props} disabled={true} staffMembers={undefined} />
|
||||
))
|
||||
.add("no limits", () => <StaffListPage {...props} limits={undefined} />)
|
||||
.add("limits reached", () => (
|
||||
<StaffListPage {...props} limits={limitsReached} />
|
||||
));
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
import Button from "@material-ui/core/Button";
|
||||
import Card from "@material-ui/core/Card";
|
||||
import Alert from "@saleor/components/Alert/Alert";
|
||||
import AppHeader from "@saleor/components/AppHeader";
|
||||
import { Container } from "@saleor/components/Container";
|
||||
import FilterBar from "@saleor/components/FilterBar";
|
||||
import PageHeader from "@saleor/components/PageHeader";
|
||||
import { RefreshLimits_shop_limits } from "@saleor/components/Shop/types/RefreshLimits";
|
||||
import { sectionNames } from "@saleor/intl";
|
||||
import { StaffListUrlSortField } from "@saleor/staff/urls";
|
||||
import {
|
||||
|
@ -12,6 +14,7 @@ import {
|
|||
SortPage,
|
||||
TabPageProps
|
||||
} from "@saleor/types";
|
||||
import { hasLimits, isLimitReached } from "@saleor/utils/limits";
|
||||
import React from "react";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
|
||||
|
@ -28,6 +31,7 @@ export interface StaffListPageProps
|
|||
FilterPageProps<StaffFilterKeys, StaffListFilterOpts>,
|
||||
SortPage<StaffListUrlSortField>,
|
||||
TabPageProps {
|
||||
limits: RefreshLimits_shop_limits;
|
||||
staffMembers: StaffList_staffUsers_edges_node[];
|
||||
onAdd: () => void;
|
||||
onBack: () => void;
|
||||
|
@ -37,6 +41,7 @@ const StaffListPage: React.FC<StaffListPageProps> = ({
|
|||
currentTab,
|
||||
filterOpts,
|
||||
initialSearch,
|
||||
limits,
|
||||
onAdd,
|
||||
onAll,
|
||||
onBack,
|
||||
|
@ -51,20 +56,44 @@ const StaffListPage: React.FC<StaffListPageProps> = ({
|
|||
const intl = useIntl();
|
||||
|
||||
const structure = createFilterStructure(intl, filterOpts);
|
||||
const reachedLimit = isLimitReached(limits, "staffUsers");
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<AppHeader onBack={onBack}>
|
||||
{intl.formatMessage(sectionNames.configuration)}
|
||||
</AppHeader>
|
||||
<PageHeader title={intl.formatMessage(sectionNames.staff)}>
|
||||
<Button color="primary" variant="contained" onClick={onAdd}>
|
||||
<PageHeader
|
||||
title={intl.formatMessage(sectionNames.staff)}
|
||||
limit={
|
||||
hasLimits(limits, "staffUsers") && {
|
||||
data: limits,
|
||||
key: "staffUsers",
|
||||
text: "members"
|
||||
}
|
||||
}
|
||||
>
|
||||
<Button
|
||||
color="primary"
|
||||
disabled={reachedLimit}
|
||||
variant="contained"
|
||||
onClick={onAdd}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Invite staff member"
|
||||
description="button"
|
||||
/>
|
||||
</Button>
|
||||
</PageHeader>
|
||||
<Alert
|
||||
title={intl.formatMessage({
|
||||
defaultMessage: "Staff Member limit reached",
|
||||
description: "alert"
|
||||
})}
|
||||
show={reachedLimit}
|
||||
>
|
||||
<FormattedMessage defaultMessage="You have reached your staff member limit, you will be no longer able to add staff members to your store. If you would like to up your limit, contact your administration staff about raising your limits." />
|
||||
</Alert>
|
||||
<Card>
|
||||
<FilterBar
|
||||
allTabLabel={intl.formatMessage({
|
||||
|
|
|
@ -43,7 +43,7 @@ const staffMemberAddMutation = gql`
|
|||
}
|
||||
}
|
||||
`;
|
||||
export const TypedStaffMemberAddMutation = TypedMutation<
|
||||
export const useStaffMemberAddMutation = makeMutation<
|
||||
StaffMemberAdd,
|
||||
StaffMemberAddVariables
|
||||
>(staffMemberAddMutation);
|
||||
|
|
|
@ -3,6 +3,7 @@ import DeleteFilterTabDialog from "@saleor/components/DeleteFilterTabDialog";
|
|||
import SaveFilterTabDialog, {
|
||||
SaveFilterTabDialogFormData
|
||||
} from "@saleor/components/SaveFilterTabDialog";
|
||||
import { useShopLimitsQuery } from "@saleor/components/Shop/query";
|
||||
import { APP_MOUNT_URI, DEFAULT_INITIAL_SEARCH_DATA } from "@saleor/config";
|
||||
import { configurationMenuUrl } from "@saleor/configuration";
|
||||
import useListSettings from "@saleor/hooks/useListSettings";
|
||||
|
@ -27,9 +28,8 @@ import StaffAddMemberDialog, {
|
|||
AddMemberFormData
|
||||
} from "../../components/StaffAddMemberDialog";
|
||||
import StaffListPage from "../../components/StaffListPage";
|
||||
import { TypedStaffMemberAddMutation } from "../../mutations";
|
||||
import { useStaffMemberAddMutation } from "../../mutations";
|
||||
import { useStaffListQuery } from "../../queries";
|
||||
import { StaffMemberAdd } from "../../types/StaffMemberAdd";
|
||||
import {
|
||||
staffListUrl,
|
||||
StaffListUrlDialog,
|
||||
|
@ -74,6 +74,23 @@ export const StaffList: React.FC<StaffListProps> = ({ params }) => {
|
|||
displayLoader: true,
|
||||
variables: queryVariables
|
||||
});
|
||||
const limitOpts = useShopLimitsQuery({
|
||||
variables: {
|
||||
staffUsers: true
|
||||
}
|
||||
});
|
||||
|
||||
const [addStaffMember, addStaffMemberData] = useStaffMemberAddMutation({
|
||||
onCompleted: data => {
|
||||
if (data.staffCreate.errors.length === 0) {
|
||||
notify({
|
||||
status: "success",
|
||||
text: intl.formatMessage(commonMessages.savedChanges)
|
||||
});
|
||||
navigate(staffMemberDetailsUrl(data.staffCreate.user.id));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const { loadNextPage, loadPreviousPage, pageInfo } = paginate(
|
||||
staffQueryData?.staffUsers.pageInfo,
|
||||
|
@ -127,16 +144,6 @@ export const StaffList: React.FC<StaffListProps> = ({ params }) => {
|
|||
handleTabChange(tabs.length + 1);
|
||||
};
|
||||
|
||||
const handleStaffMemberAddSuccess = (data: StaffMemberAdd) => {
|
||||
if (data.staffCreate.errors.length === 0) {
|
||||
notify({
|
||||
status: "success",
|
||||
text: intl.formatMessage(commonMessages.savedChanges)
|
||||
});
|
||||
navigate(staffMemberDetailsUrl(data.staffCreate.user.id));
|
||||
}
|
||||
};
|
||||
|
||||
const {
|
||||
loadMore: loadMorePermissionGroups,
|
||||
search: searchPermissionGroups,
|
||||
|
@ -145,90 +152,82 @@ export const StaffList: React.FC<StaffListProps> = ({ params }) => {
|
|||
variables: DEFAULT_INITIAL_SEARCH_DATA
|
||||
});
|
||||
|
||||
return (
|
||||
<TypedStaffMemberAddMutation onCompleted={handleStaffMemberAddSuccess}>
|
||||
{(addStaffMember, addStaffMemberData) => {
|
||||
const handleStaffMemberAdd = (variables: AddMemberFormData) =>
|
||||
addStaffMember({
|
||||
variables: {
|
||||
input: {
|
||||
addGroups: variables.permissionGroups,
|
||||
email: variables.email,
|
||||
firstName: variables.firstName,
|
||||
lastName: variables.lastName,
|
||||
redirectUrl: urlJoin(
|
||||
window.location.origin,
|
||||
APP_MOUNT_URI === "/" ? "" : APP_MOUNT_URI,
|
||||
newPasswordUrl().replace(/\?/, "")
|
||||
)
|
||||
}
|
||||
}
|
||||
});
|
||||
const handleStaffMemberAdd = (variables: AddMemberFormData) =>
|
||||
addStaffMember({
|
||||
variables: {
|
||||
input: {
|
||||
addGroups: variables.permissionGroups,
|
||||
email: variables.email,
|
||||
firstName: variables.firstName,
|
||||
lastName: variables.lastName,
|
||||
redirectUrl: urlJoin(
|
||||
window.location.origin,
|
||||
APP_MOUNT_URI === "/" ? "" : APP_MOUNT_URI,
|
||||
newPasswordUrl().replace(/\?/, "")
|
||||
)
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<StaffListPage
|
||||
currentTab={currentTab}
|
||||
filterOpts={getFilterOpts(params)}
|
||||
initialSearch={params.query || ""}
|
||||
onSearchChange={handleSearchChange}
|
||||
onFilterChange={changeFilters}
|
||||
onAll={resetFilters}
|
||||
onTabChange={handleTabChange}
|
||||
onTabDelete={() => openModal("delete-search")}
|
||||
onTabSave={() => openModal("save-search")}
|
||||
tabs={tabs.map(tab => tab.name)}
|
||||
disabled={loading || addStaffMemberData.loading}
|
||||
settings={settings}
|
||||
pageInfo={pageInfo}
|
||||
sort={getSortParams(params)}
|
||||
staffMembers={staffQueryData?.staffUsers.edges.map(
|
||||
edge => edge.node
|
||||
)}
|
||||
onAdd={() => openModal("add")}
|
||||
onBack={() => navigate(configurationMenuUrl)}
|
||||
onNextPage={loadNextPage}
|
||||
onPreviousPage={loadPreviousPage}
|
||||
onUpdateListSettings={updateListSettings}
|
||||
onRowClick={id => () => navigate(staffMemberDetailsUrl(id))}
|
||||
onSort={handleSort}
|
||||
/>
|
||||
<StaffAddMemberDialog
|
||||
availablePermissionGroups={searchPermissionGroupsOpts.data?.search.edges.map(
|
||||
edge => edge.node
|
||||
)}
|
||||
confirmButtonState={addStaffMemberData.status}
|
||||
initialSearch=""
|
||||
disabled={loading}
|
||||
errors={addStaffMemberData.data?.staffCreate.errors || []}
|
||||
open={params.action === "add"}
|
||||
onClose={closeModal}
|
||||
onConfirm={handleStaffMemberAdd}
|
||||
fetchMorePermissionGroups={{
|
||||
hasMore:
|
||||
searchPermissionGroupsOpts.data?.search.pageInfo.hasNextPage,
|
||||
loading: searchPermissionGroupsOpts.loading,
|
||||
onFetchMore: loadMorePermissionGroups
|
||||
}}
|
||||
onSearchChange={searchPermissionGroups}
|
||||
/>
|
||||
<SaveFilterTabDialog
|
||||
open={params.action === "save-search"}
|
||||
confirmButtonState="default"
|
||||
onClose={closeModal}
|
||||
onSubmit={handleTabSave}
|
||||
/>
|
||||
<DeleteFilterTabDialog
|
||||
open={params.action === "delete-search"}
|
||||
confirmButtonState="default"
|
||||
onClose={closeModal}
|
||||
onSubmit={handleTabDelete}
|
||||
tabName={getStringOrPlaceholder(tabs[currentTab - 1]?.name)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</TypedStaffMemberAddMutation>
|
||||
return (
|
||||
<>
|
||||
<StaffListPage
|
||||
currentTab={currentTab}
|
||||
filterOpts={getFilterOpts(params)}
|
||||
initialSearch={params.query || ""}
|
||||
onSearchChange={handleSearchChange}
|
||||
onFilterChange={changeFilters}
|
||||
onAll={resetFilters}
|
||||
onTabChange={handleTabChange}
|
||||
onTabDelete={() => openModal("delete-search")}
|
||||
onTabSave={() => openModal("save-search")}
|
||||
tabs={tabs.map(tab => tab.name)}
|
||||
disabled={loading || addStaffMemberData.loading || limitOpts.loading}
|
||||
limits={limitOpts.data?.shop.limits}
|
||||
settings={settings}
|
||||
pageInfo={pageInfo}
|
||||
sort={getSortParams(params)}
|
||||
staffMembers={staffQueryData?.staffUsers.edges.map(edge => edge.node)}
|
||||
onAdd={() => openModal("add")}
|
||||
onBack={() => navigate(configurationMenuUrl)}
|
||||
onNextPage={loadNextPage}
|
||||
onPreviousPage={loadPreviousPage}
|
||||
onUpdateListSettings={updateListSettings}
|
||||
onRowClick={id => () => navigate(staffMemberDetailsUrl(id))}
|
||||
onSort={handleSort}
|
||||
/>
|
||||
<StaffAddMemberDialog
|
||||
availablePermissionGroups={searchPermissionGroupsOpts.data?.search.edges.map(
|
||||
edge => edge.node
|
||||
)}
|
||||
confirmButtonState={addStaffMemberData.status}
|
||||
initialSearch=""
|
||||
disabled={loading}
|
||||
errors={addStaffMemberData.data?.staffCreate.errors || []}
|
||||
open={params.action === "add"}
|
||||
onClose={closeModal}
|
||||
onConfirm={handleStaffMemberAdd}
|
||||
fetchMorePermissionGroups={{
|
||||
hasMore: searchPermissionGroupsOpts.data?.search.pageInfo.hasNextPage,
|
||||
loading: searchPermissionGroupsOpts.loading,
|
||||
onFetchMore: loadMorePermissionGroups
|
||||
}}
|
||||
onSearchChange={searchPermissionGroups}
|
||||
/>
|
||||
<SaveFilterTabDialog
|
||||
open={params.action === "save-search"}
|
||||
confirmButtonState="default"
|
||||
onClose={closeModal}
|
||||
onSubmit={handleTabSave}
|
||||
/>
|
||||
<DeleteFilterTabDialog
|
||||
open={params.action === "delete-search"}
|
||||
confirmButtonState="default"
|
||||
onClose={closeModal}
|
||||
onSubmit={handleTabDelete}
|
||||
tabName={getStringOrPlaceholder(tabs[currentTab - 1]?.name)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -8,6 +8,8 @@ import React from "react";
|
|||
|
||||
import {
|
||||
filterPageProps,
|
||||
limits,
|
||||
limitsReached,
|
||||
listActionsProps,
|
||||
pageListProps,
|
||||
sortPageProps
|
||||
|
@ -46,6 +48,7 @@ const props: OrderListPageProps = {
|
|||
value: [OrderStatusFilter.CANCELED, OrderStatusFilter.FULFILLED]
|
||||
}
|
||||
},
|
||||
limits,
|
||||
onSettingsOpen: () => undefined,
|
||||
orders,
|
||||
sort: {
|
||||
|
@ -65,4 +68,8 @@ storiesOf("Views / Orders / Order list", module)
|
|||
disabled={true}
|
||||
/>
|
||||
))
|
||||
.add("when no data", () => <OrderListPage {...props} orders={[]} />);
|
||||
.add("when no data", () => <OrderListPage {...props} orders={[]} />)
|
||||
.add("no limits", () => <OrderListPage {...props} limits={undefined} />)
|
||||
.add("limits reached", () => (
|
||||
<OrderListPage {...props} limits={limitsReached} />
|
||||
));
|
||||
|
|
|
@ -11,6 +11,8 @@ import React from "react";
|
|||
import {
|
||||
fetchMoreProps,
|
||||
filterPageProps,
|
||||
limits,
|
||||
limitsReached,
|
||||
listActionsProps,
|
||||
pageListProps,
|
||||
sortPageProps
|
||||
|
@ -41,6 +43,7 @@ const props: ProductListPageProps = {
|
|||
defaultSettings: defaultListSettings[ListViews.PRODUCT_LIST],
|
||||
filterOpts: productListFilterOpts,
|
||||
gridAttributes: attributes,
|
||||
limits,
|
||||
onExport: () => undefined,
|
||||
products,
|
||||
selectedChannelId: "123",
|
||||
|
@ -71,4 +74,8 @@ storiesOf("Views / Products / Product list", module)
|
|||
selectedChannelId={""}
|
||||
products={products.map(product => ({ ...product, channelListings: [] }))}
|
||||
/>
|
||||
))
|
||||
.add("no limits", () => <ProductListPage {...props} limits={undefined} />)
|
||||
.add("limits reached", () => (
|
||||
<ProductListPage {...props} limits={limitsReached} />
|
||||
));
|
||||
|
|
|
@ -2,7 +2,12 @@ import placeholderImage from "@assets/images/placeholder255x255.png";
|
|||
import { channelsList } from "@saleor/channels/fixtures";
|
||||
import { createChannelsData } from "@saleor/channels/utils";
|
||||
import { collections } from "@saleor/collections/fixtures";
|
||||
import { fetchMoreProps, listActionsProps } from "@saleor/fixtures";
|
||||
import {
|
||||
fetchMoreProps,
|
||||
limits,
|
||||
limitsReached,
|
||||
listActionsProps
|
||||
} from "@saleor/fixtures";
|
||||
import ProductUpdatePage, {
|
||||
ProductUpdatePageProps
|
||||
} from "@saleor/products/components/ProductUpdatePage";
|
||||
|
@ -42,6 +47,7 @@ const props: ProductUpdatePageProps = {
|
|||
hasChannelChanged: false,
|
||||
header: product.name,
|
||||
media: product.media,
|
||||
limits,
|
||||
onAssignReferencesClick: () => undefined,
|
||||
onBack: () => undefined,
|
||||
onChannelsChange: () => undefined,
|
||||
|
@ -184,4 +190,8 @@ storiesOf("Views / Products / Product edit", module)
|
|||
))
|
||||
.add("with channels", () => (
|
||||
<ProductUpdatePage {...props} currentChannels={channels} />
|
||||
))
|
||||
.add("no limits", () => <ProductUpdatePage {...props} limits={undefined} />)
|
||||
.add("limits reached", () => (
|
||||
<ProductUpdatePage {...props} limits={limitsReached} />
|
||||
));
|
||||
|
|
|
@ -45,7 +45,7 @@ const buttonOverrides = (colors: IThemeColors): Overrides => ({
|
|||
}
|
||||
},
|
||||
textPrimary: {
|
||||
"& span": {
|
||||
"&:not($disabled) span": {
|
||||
color: colors.primary
|
||||
}
|
||||
}
|
||||
|
|
|
@ -217,7 +217,8 @@ const createTheme = (colors: IThemeColors): SaleorTheme =>
|
|||
color: colors.font.default
|
||||
},
|
||||
h5: {
|
||||
fontSize: "1.3125rem"
|
||||
fontSize: "1.3125rem",
|
||||
fontWeight: 500
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -6,6 +6,7 @@ const createPalette = (colors: IThemeColors): SaleorPaletteOptions => ({
|
|||
action: {
|
||||
active: colors.checkbox.default
|
||||
},
|
||||
alert: colors.alert,
|
||||
background: colors.background,
|
||||
divider: colors.divider,
|
||||
error: {
|
||||
|
|
|
@ -5,7 +5,13 @@ import {
|
|||
PaletteOptions
|
||||
} from "@material-ui/core/styles/createPalette";
|
||||
|
||||
export type AlertPalette = Record<
|
||||
"success" | "error" | "warning" | "info",
|
||||
string
|
||||
>;
|
||||
export type AlertColors = Record<"paper" | "icon", AlertPalette>;
|
||||
interface ExtraPaletteOptions {
|
||||
alert?: AlertColors;
|
||||
textHighlighted?: {
|
||||
active: string;
|
||||
inactive: string;
|
||||
|
|
12
src/utils/api.ts
Normal file
12
src/utils/api.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
import { ApolloError } from "apollo-client";
|
||||
|
||||
export enum GqlErrors {
|
||||
LimitReachedException = "LimitReachedException",
|
||||
ReadOnlyException = "ReadOnlyException"
|
||||
}
|
||||
|
||||
export function hasError(err: ApolloError, ...errorCodes: string[]): boolean {
|
||||
return err.graphQLErrors.some(gqlError =>
|
||||
errorCodes.includes(gqlError.extensions.exception.code)
|
||||
);
|
||||
}
|
24
src/utils/limits.ts
Normal file
24
src/utils/limits.ts
Normal file
|
@ -0,0 +1,24 @@
|
|||
import { RefreshLimits_shop_limits } from "@saleor/components/Shop/types/RefreshLimits";
|
||||
import { LimitInfoFragment } from "@saleor/fragments/types/LimitInfoFragment";
|
||||
|
||||
export function hasLimits(
|
||||
limits: RefreshLimits_shop_limits,
|
||||
key: keyof LimitInfoFragment
|
||||
): boolean {
|
||||
if (limits === undefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return limits.allowedUsage[key] !== null;
|
||||
}
|
||||
|
||||
export function isLimitReached(
|
||||
limits: RefreshLimits_shop_limits,
|
||||
key: keyof LimitInfoFragment
|
||||
): boolean {
|
||||
if (!hasLimits(limits, key)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return limits.currentUsage[key] >= limits.allowedUsage[key];
|
||||
}
|
|
@ -1,4 +1,6 @@
|
|||
import {
|
||||
limits,
|
||||
limitsReached,
|
||||
pageListProps,
|
||||
searchPageProps,
|
||||
sortPageProps,
|
||||
|
@ -19,6 +21,7 @@ const props: WarehouseListPageProps = {
|
|||
...searchPageProps,
|
||||
...sortPageProps,
|
||||
...tabPageProps,
|
||||
limits,
|
||||
onBack: () => undefined,
|
||||
onRemove: () => undefined,
|
||||
sort: {
|
||||
|
@ -34,4 +37,8 @@ storiesOf("Views / Warehouses / Warehouse list", module)
|
|||
.add("loading", () => (
|
||||
<WarehouseListPage {...props} disabled={true} warehouses={undefined} />
|
||||
))
|
||||
.add("no data", () => <WarehouseListPage {...props} warehouses={[]} />);
|
||||
.add("no data", () => <WarehouseListPage {...props} warehouses={[]} />)
|
||||
.add("no limits", () => <WarehouseListPage {...props} limits={undefined} />)
|
||||
.add("limits reached", () => (
|
||||
<WarehouseListPage {...props} limits={limitsReached} />
|
||||
));
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
import Button from "@material-ui/core/Button";
|
||||
import Card from "@material-ui/core/Card";
|
||||
import Alert from "@saleor/components/Alert/Alert";
|
||||
import AppHeader from "@saleor/components/AppHeader";
|
||||
import Container from "@saleor/components/Container";
|
||||
import PageHeader from "@saleor/components/PageHeader";
|
||||
import SearchBar from "@saleor/components/SearchBar";
|
||||
import { RefreshLimits_shop_limits } from "@saleor/components/Shop/types/RefreshLimits";
|
||||
import { WarehouseWithShippingFragment } from "@saleor/fragments/types/WarehouseWithShippingFragment";
|
||||
import { sectionNames } from "@saleor/intl";
|
||||
import {
|
||||
|
@ -12,6 +14,7 @@ import {
|
|||
SortPage,
|
||||
TabPageProps
|
||||
} from "@saleor/types";
|
||||
import { hasLimits, isLimitReached } from "@saleor/utils/limits";
|
||||
import { WarehouseListUrlSortField } from "@saleor/warehouses/urls";
|
||||
import React from "react";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
|
@ -23,6 +26,7 @@ export interface WarehouseListPageProps
|
|||
SearchPageProps,
|
||||
SortPage<WarehouseListUrlSortField>,
|
||||
TabPageProps {
|
||||
limits: RefreshLimits_shop_limits;
|
||||
warehouses: WarehouseWithShippingFragment[];
|
||||
onBack: () => void;
|
||||
onRemove: (id: string) => void;
|
||||
|
@ -32,6 +36,7 @@ export const WarehouseListPage: React.FC<WarehouseListPageProps> = ({
|
|||
warehouses,
|
||||
currentTab,
|
||||
disabled,
|
||||
limits,
|
||||
initialSearch,
|
||||
pageInfo,
|
||||
settings,
|
||||
|
@ -52,19 +57,44 @@ export const WarehouseListPage: React.FC<WarehouseListPageProps> = ({
|
|||
}) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const limitReached = isLimitReached(limits, "warehouses");
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<AppHeader onBack={onBack}>
|
||||
<FormattedMessage {...sectionNames.configuration} />
|
||||
</AppHeader>
|
||||
<PageHeader title={intl.formatMessage(sectionNames.warehouses)}>
|
||||
<Button color="primary" variant="contained" onClick={onAdd}>
|
||||
<PageHeader
|
||||
title={intl.formatMessage(sectionNames.warehouses)}
|
||||
limit={
|
||||
hasLimits(limits, "warehouses") && {
|
||||
data: limits,
|
||||
key: "warehouses",
|
||||
text: "warehouses used"
|
||||
}
|
||||
}
|
||||
>
|
||||
<Button
|
||||
color="primary"
|
||||
disabled={limitReached}
|
||||
variant="contained"
|
||||
onClick={onAdd}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Create Warehouse"
|
||||
description="button"
|
||||
/>
|
||||
</Button>
|
||||
</PageHeader>
|
||||
<Alert
|
||||
show={limitReached}
|
||||
title={intl.formatMessage({
|
||||
defaultMessage: "Warehouse limit reached",
|
||||
description: "alert"
|
||||
})}
|
||||
>
|
||||
<FormattedMessage defaultMessage="You have reached your warehouse limit, you will be no longer able to add warehouses to your store. If you would like to up your limit, contact your administration staff about raising your limits." />
|
||||
</Alert>
|
||||
<Card>
|
||||
<SearchBar
|
||||
allTabLabel={intl.formatMessage({
|
||||
|
|
|
@ -2,6 +2,7 @@ import DeleteFilterTabDialog from "@saleor/components/DeleteFilterTabDialog";
|
|||
import SaveFilterTabDialog, {
|
||||
SaveFilterTabDialogFormData
|
||||
} from "@saleor/components/SaveFilterTabDialog";
|
||||
import { useShopLimitsQuery } from "@saleor/components/Shop/query";
|
||||
import { WindowTitle } from "@saleor/components/WindowTitle";
|
||||
import { configurationMenuUrl } from "@saleor/configuration";
|
||||
import useListSettings from "@saleor/hooks/useListSettings";
|
||||
|
@ -67,6 +68,11 @@ const WarehouseList: React.FC<WarehouseListProps> = ({ params }) => {
|
|||
displayLoader: true,
|
||||
variables: queryVariables
|
||||
});
|
||||
const limitOpts = useShopLimitsQuery({
|
||||
variables: {
|
||||
warehouses: true
|
||||
}
|
||||
});
|
||||
const [deleteWarehouse, deleteWarehouseOpts] = useWarehouseDelete({
|
||||
onCompleted: data => {
|
||||
if (data.deleteWarehouse.errors.length === 0) {
|
||||
|
@ -75,6 +81,7 @@ const WarehouseList: React.FC<WarehouseListProps> = ({ params }) => {
|
|||
text: intl.formatMessage(commonMessages.savedChanges)
|
||||
});
|
||||
refetch();
|
||||
limitOpts.refetch();
|
||||
closeModal();
|
||||
}
|
||||
}
|
||||
|
@ -141,6 +148,7 @@ const WarehouseList: React.FC<WarehouseListProps> = ({ params }) => {
|
|||
onTabChange={handleTabChange}
|
||||
onTabDelete={() => openModal("delete-search")}
|
||||
onTabSave={() => openModal("save-search")}
|
||||
limits={limitOpts.data?.shop.limits}
|
||||
tabs={tabs.map(tab => tab.name)}
|
||||
warehouses={maybe(() => data.warehouses.edges.map(edge => edge.node))}
|
||||
settings={settings}
|
||||
|
|
Loading…
Reference in a new issue