diff --git a/CHANGELOG.md b/CHANGELOG.md index 643e15335..4888fadf7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/assets/images/alert-dark.svg b/assets/images/alert-dark.svg new file mode 100644 index 000000000..fa55b304c --- /dev/null +++ b/assets/images/alert-dark.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/images/alert.svg b/assets/images/alert.svg new file mode 100644 index 000000000..68eec809e --- /dev/null +++ b/assets/images/alert.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/locale/defaultMessages.json b/locale/defaultMessages.json index 66d4ad9a2..690cefd24 100644 --- a/locale/defaultMessages.json +++ b/locale/defaultMessages.json @@ -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" diff --git a/src/channels/pages/ChannelsListPage/ChannelsListPage.stories.tsx b/src/channels/pages/ChannelsListPage/ChannelsListPage.stories.tsx index d5050322b..6af981fd0 100644 --- a/src/channels/pages/ChannelsListPage/ChannelsListPage.stories.tsx +++ b/src/channels/pages/ChannelsListPage/ChannelsListPage.stories.tsx @@ -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", () => ) - .add("empty", () => ); + .add("empty", () => ) + .add("no limits", () => ) + .add("limits reached", () => ( + + )); diff --git a/src/channels/pages/ChannelsListPage/ChannelsListPage.tsx b/src/channels/pages/ChannelsListPage/ChannelsListPage.tsx index 06dafa9ec..f23b0db19 100644 --- a/src/channels/pages/ChannelsListPage/ChannelsListPage.tsx +++ b/src/channels/pages/ChannelsListPage/ChannelsListPage.tsx @@ -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 = ({ channelsList, + limits, navigateToChannelCreate, onBack, onRemove, @@ -40,13 +45,25 @@ export const ChannelsListPage: React.FC = ({ const intl = useIntl(); const classes = useStyles({}); + const limitReached = isLimitReached(limits, "channels"); + return ( {intl.formatMessage(sectionNames.configuration)} - + = ({ /> + + + diff --git a/src/channels/views/ChannelsList/ChannelsList.tsx b/src/channels/views/ChannelsList/ChannelsList.tsx index 1a658640b..9d11e4f6e 100644 --- a/src/channels/views/ChannelsList/ChannelsList.tsx +++ b/src/channels/views/ChannelsList/ChannelsList.tsx @@ -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 = ({ 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 = ({ params }) => { }) }); refetch(); + limitOpts.refetch(); closeModal(); } else { errors.map(error => @@ -89,6 +96,7 @@ export const ChannelsList: React.FC = ({ params }) => { <> navigate(configurationMenuUrl)} onRowClick={id => () => navigate(channelUrl(id))} diff --git a/src/components/Alert/Alert.stories.tsx b/src/components/Alert/Alert.stories.tsx new file mode 100644 index 000000000..836cd50eb --- /dev/null +++ b/src/components/Alert/Alert.stories.tsx @@ -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", () => ( + + Content + + )); diff --git a/src/components/Alert/Alert.tsx b/src/components/Alert/Alert.tsx new file mode 100644 index 000000000..88d30c3ae --- /dev/null +++ b/src/components/Alert/Alert.tsx @@ -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 = ({ children, className, show, title }) => { + const classes = useStyles({}); + const [visible, setVisible] = useStateFromProps(show); + const { isDark } = useTheme(); + + return visible ? ( + + + + + + + + + {title} + setVisible(false)} + > + + + + {children} + + + + + ) : null; +}; + +Alert.displayName = "Alert"; +export default Alert; diff --git a/src/components/Alert/index.ts b/src/components/Alert/index.ts new file mode 100644 index 000000000..e69de29bb diff --git a/src/components/PageHeader/PageHeader.tsx b/src/components/PageHeader/PageHeader.tsx index 028642851..ab75ab517 100644 --- a/src/components/PageHeader/PageHeader.tsx +++ b/src/components/PageHeader/PageHeader.tsx @@ -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 = 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 = props => { } > - {children} + + {limit && ( + + {formatLimit(limit)} + + )} + {children} + ); }; diff --git a/src/components/Shop/query.ts b/src/components/Shop/query.ts index f082c68a2..5a3b56441 100644 --- a/src/components/Shop/query.ts +++ b/src/components/Shop/query.ts @@ -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); + +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); diff --git a/src/components/Shop/types/RefreshLimits.ts b/src/components/Shop/types/RefreshLimits.ts new file mode 100644 index 000000000..f76dfa554 --- /dev/null +++ b/src/components/Shop/types/RefreshLimits.ts @@ -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; +} diff --git a/src/components/Theme/themes.ts b/src/components/Theme/themes.ts index 2c755284f..9b78cb221 100644 --- a/src/components/Theme/themes.ts +++ b/src/components/Theme/themes.ts @@ -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" }, diff --git a/src/components/messages/styles.ts b/src/components/messages/styles.ts index f880b399d..da7c70052 100644 --- a/src/components/messages/styles.ts +++ b/src/components/messages/styles.ts @@ -42,7 +42,7 @@ export const useStyles = makeStyles( position: "fixed", top: 0, width: "100%", - zIndex: 1200 + zIndex: 10000 }, error: { "& > div": { diff --git a/src/fixtures.ts b/src/fixtures.ts index f9bd23fca..cba66421c 100644 --- a/src/fixtures.ts +++ b/src/fixtures.ts @@ -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 + } +}; diff --git a/src/fragments/shop.ts b/src/fragments/shop.ts index 2ff48f432..f4da82880 100644 --- a/src/fragments/shop.ts +++ b/src/fragments/shop.ts @@ -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 { diff --git a/src/fragments/types/LimitInfoFragment.ts b/src/fragments/types/LimitInfoFragment.ts new file mode 100644 index 000000000..5113b4bc1 --- /dev/null +++ b/src/fragments/types/LimitInfoFragment.ts @@ -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; +} diff --git a/src/fragments/types/ShopLimitFragment.ts b/src/fragments/types/ShopLimitFragment.ts new file mode 100644 index 000000000..50861cc92 --- /dev/null +++ b/src/fragments/types/ShopLimitFragment.ts @@ -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; +} diff --git a/src/hooks/makeMutation.ts b/src/hooks/makeMutation.ts index 0f4867882..a65f24dbc 100644 --- a/src/hooks/makeMutation.ts +++ b/src/hooks/makeMutation.ts @@ -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( 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( status: "error", text: intl.formatMessage(commonMessages.sessionExpired) }); - } else { + } else if (!hasError(err, GqlErrors.LimitReachedException)) { notify({ status: "error", text: intl.formatMessage(commonMessages.somethingWentWrong) diff --git a/src/hooks/useForm.ts b/src/hooks/useForm.ts index 5475777e3..cb2df629e 100644 --- a/src/hooks/useForm.ts +++ b/src/hooks/useForm.ts @@ -112,7 +112,7 @@ function useForm( const result = onSubmit(data); if (result) { const errors = await result; - if (errors.length === 0) { + if (errors?.length === 0) { setChanged(false); } } diff --git a/src/intl.ts b/src/intl.ts index 3be453e95..23ba62514 100644 --- a/src/intl.ts +++ b/src/intl.ts @@ -55,6 +55,9 @@ export const commonMessages = defineMessages({ lastName: { defaultMessage: "Last Name" }, + limitReached: { + defaultMessage: "Reached limit for this plan" + }, no: { defaultMessage: "No" }, diff --git a/src/mutations.tsx b/src/mutations.tsx index 329444b12..5b68d9064 100644 --- a/src/mutations.tsx +++ b/src/mutations.tsx @@ -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 { children: ( @@ -43,10 +44,7 @@ export function TypedMutation( 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( status: "error", text: intl.formatMessage(commonMessages.sessionExpired) }); - } else { + } else if (!hasError(err, GqlErrors.LimitReachedException)) { notify({ status: "error", text: intl.formatMessage(commonMessages.somethingWentWrong) diff --git a/src/orders/components/OrderListPage/OrderListPage.tsx b/src/orders/components/OrderListPage/OrderListPage.tsx index cecb643c2..b4e642234 100644 --- a/src/orders/components/OrderListPage/OrderListPage.tsx +++ b/src/orders/components/OrderListPage/OrderListPage.tsx @@ -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, SortPage { + limits: RefreshLimits_shop_limits; orders: OrderList_orders_edges_node[]; onSettingsOpen: () => void; } @@ -40,6 +44,7 @@ const OrderListPage: React.FC = ({ currentTab, initialSearch, filterOpts, + limits, tabs, onAdd, onAll, @@ -84,6 +89,15 @@ const OrderListPage: React.FC = ({ /> + + + = ({ 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 = ({ 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)} diff --git a/src/products/components/ProductListPage/ProductListPage.tsx b/src/products/components/ProductListPage/ProductListPage.tsx index 492e28a05..ad6193847 100644 --- a/src/products/components/ProductListPage/ProductListPage.tsx +++ b/src/products/components/ProductListPage/ProductListPage.tsx @@ -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 = props => { currentTab, defaultSettings, gridAttributes, + limits, availableInGridAttributes, filterOpts, hasMore, @@ -120,9 +125,20 @@ export const ProductListPage: React.FC = props => { })) ]; + const limitReached = isLimitReached(limits, "productVariants"); + return ( - + = props => { onSave={handleSave} /> = props => { /> + + + undefined, onBack: () => undefined, onChannelsChange: () => undefined, diff --git a/src/products/components/ProductUpdatePage/ProductUpdatePage.tsx b/src/products/components/ProductUpdatePage/ProductUpdatePage.tsx index f35de5f68..b8cc69ba4 100644 --- a/src/products/components/ProductUpdatePage/ProductUpdatePage.tsx +++ b/src/products/components/ProductUpdatePage/ProductUpdatePage.tsx @@ -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 = ({ media, hasChannelChanged, header, + limits, placeholderImage, product, saveButtonBarState, @@ -310,6 +313,7 @@ export const ProductUpdatePage: React.FC = ({ {hasVariants ? ( 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", () => ( - undefined} /> + undefined} + /> )); diff --git a/src/products/components/ProductVariantCreatorPage/ProductVariantCreatorContent.tsx b/src/products/components/ProductVariantCreatorPage/ProductVariantCreatorContent.tsx index 07ea7a8b4..be56ed6d2 100644 --- a/src/products/components/ProductVariantCreatorPage/ProductVariantCreatorContent.tsx +++ b/src/products/components/ProductVariantCreatorPage/ProductVariantCreatorContent.tsx @@ -22,6 +22,7 @@ export interface ProductVariantCreatorContentProps { dispatchFormDataAction: React.Dispatch; errors: ProductVariantBulkCreate_productVariantBulkCreate_errors[]; step: ProductVariantCreatorStep; + variantsLeft: number | null; warehouses: WarehouseFragment[]; } @@ -32,6 +33,7 @@ const ProductVariantCreatorContent: React.FC dispatchFormDataAction, errors, step, + variantsLeft, warehouses }) => { const selectedAttributes = attributes.filter(attribute => @@ -48,6 +50,7 @@ const ProductVariantCreatorContent: React.FC dispatchFormDataAction({ selectValue: { diff --git a/src/products/components/ProductVariantCreatorPage/ProductVariantCreatorPage.tsx b/src/products/components/ProductVariantCreatorPage/ProductVariantCreatorPage.tsx index 22936e2f3..db5051163 100644 --- a/src/products/components/ProductVariantCreatorPage/ProductVariantCreatorPage.tsx +++ b/src/products/components/ProductVariantCreatorPage/ProductVariantCreatorPage.tsx @@ -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 = props attributes, channelListings, errors, + limits, onSubmit, warehouses, ...contentProps @@ -177,6 +186,10 @@ const ProductVariantCreatePage: React.FC = props React.useEffect(reloadForm, [attributes.length, warehouses.length]); + const variantsLeft = limits.allowedUsage.productVariants + ? limits.allowedUsage.productVariants - limits.currentUsage.productVariants + : null; + return ( @@ -203,7 +216,7 @@ const ProductVariantCreatePage: React.FC = 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 = props onSubmit(wizardData.variants)} > @@ -232,6 +245,7 @@ const ProductVariantCreatePage: React.FC = props data={wizardData} dispatchFormDataAction={dispatchFormDataAction} errors={errors} + variantsLeft={variantsLeft} step={step} warehouses={warehouses} /> diff --git a/src/products/components/ProductVariantCreatorPage/ProductVariantCreatorValues.tsx b/src/products/components/ProductVariantCreatorPage/ProductVariantCreatorValues.tsx index a4cc4cfae..689be7b2e 100644 --- a/src/products/components/ProductVariantCreatorPage/ProductVariantCreatorValues.tsx +++ b/src/products/components/ProductVariantCreatorPage/ProductVariantCreatorValues.tsx @@ -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 = 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 && ( + variantsLeft} + title={intl.formatMessage({ + defaultMessage: "SKU limit reached", + description: "alert" + })} + > + + + )} {attributes.map(attribute => ( diff --git a/src/products/components/ProductVariants/ProductVariants.tsx b/src/products/components/ProductVariants/ProductVariants.tsx index fa7a3257d..e67c316fb 100644 --- a/src/products/components/ProductVariants/ProductVariants.tsx +++ b/src/products/components/ProductVariants/ProductVariants.tsx @@ -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 = props => { const { disabled, + limits, variants, product, onRowClick, @@ -213,6 +221,7 @@ export const ProductVariants: React.FC = props => { const intl = useIntl(); const [warehouse, setWarehouse] = React.useState(null); const hasVariants = maybe(() => variants.length > 0, true); + const limitReached = isLimitReached(limits, "productVariants"); return ( @@ -224,6 +233,7 @@ export const ProductVariants: React.FC = props => { toolbar={ hasVariants ? ( = props => { ) : ( = props => { } /> + + + + {variants.length > 0 ? ( diff --git a/src/products/views/ProductList/ProductList.tsx b/src/products/views/ProductList/ProductList.tsx index 2fe238503..bf83ac5a7 100644 --- a/src/products/views/ProductList/ProductList.tsx +++ b/src/products/views/ProductList/ProductList.tsx @@ -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 = ({ 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 = ({ params }) => { }); reset(); refetch(); + limitOpts.refetch(); } } }); @@ -370,6 +377,7 @@ export const ProductList: React.FC = ({ params }) => { )} onAdd={() => navigate(productAddUrl())} disabled={loading} + limits={limitOpts.data?.shop.limits} products={maybe(() => data.products.edges.map(edge => edge.node))} onFetchMore={() => attributes.loadMore( diff --git a/src/products/views/ProductUpdate/ProductUpdate.tsx b/src/products/views/ProductUpdate/ProductUpdate.tsx index 5dcf4fef1..202ca1ea2 100644 --- a/src/products/views/ProductUpdate/ProductUpdate.tsx +++ b/src/products/views/ProductUpdate/ProductUpdate.tsx @@ -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 = ({ 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 = ({ id, params }) => { closeModal(); reset(); refetch(); + limitOpts.refetch(); } } }); @@ -459,6 +466,7 @@ export const ProductUpdate: React.FC = ({ id, params }) => { channelsErrors={channelsErrors} fetchCategories={searchCategories} fetchCollections={searchCollections} + limits={limitOpts.data?.shop.limits} saveButtonBarState={formTransitionState} media={data?.product?.media} header={product?.name} diff --git a/src/products/views/ProductVariantCreator/ProductVariantCreator.tsx b/src/products/views/ProductVariantCreator/ProductVariantCreator.tsx index 1c88400ac..331600468 100644 --- a/src/products/views/ProductVariantCreator/ProductVariantCreator.tsx +++ b/src/products/views/ProductVariantCreator/ProductVariantCreator.tsx @@ -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 = ({ } } }); + const limitOpts = useShopLimitsQuery({ + variables: { + productVariants: true + } + }); return ( <> @@ -61,6 +67,7 @@ const ProductVariantCreator: React.FC = ({ price: "" }))} attributes={data?.product?.productType?.variantAttributes || []} + limits={limitOpts.data?.shop?.limits} onSubmit={inputs => bulkProductVariantCreate({ variables: { id, inputs } diff --git a/src/staff/components/StaffListPage/StaffListPage.stories.tsx b/src/staff/components/StaffListPage/StaffListPage.stories.tsx index ac214d9c7..46b073165 100644 --- a/src/staff/components/StaffListPage/StaffListPage.stories.tsx +++ b/src/staff/components/StaffListPage/StaffListPage.stories.tsx @@ -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", () => ) .add("when loading", () => ( + )) + .add("no limits", () => ) + .add("limits reached", () => ( + )); diff --git a/src/staff/components/StaffListPage/StaffListPage.tsx b/src/staff/components/StaffListPage/StaffListPage.tsx index 47515ecc4..93f4fb1e1 100644 --- a/src/staff/components/StaffListPage/StaffListPage.tsx +++ b/src/staff/components/StaffListPage/StaffListPage.tsx @@ -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, SortPage, TabPageProps { + limits: RefreshLimits_shop_limits; staffMembers: StaffList_staffUsers_edges_node[]; onAdd: () => void; onBack: () => void; @@ -37,6 +41,7 @@ const StaffListPage: React.FC = ({ currentTab, filterOpts, initialSearch, + limits, onAdd, onAll, onBack, @@ -51,20 +56,44 @@ const StaffListPage: React.FC = ({ const intl = useIntl(); const structure = createFilterStructure(intl, filterOpts); + const reachedLimit = isLimitReached(limits, "staffUsers"); return ( {intl.formatMessage(sectionNames.configuration)} - - + + + + + (staffMemberAddMutation); diff --git a/src/staff/views/StaffList/StaffList.tsx b/src/staff/views/StaffList/StaffList.tsx index 290c2a145..f6d7e8845 100644 --- a/src/staff/views/StaffList/StaffList.tsx +++ b/src/staff/views/StaffList/StaffList.tsx @@ -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 = ({ 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 = ({ 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 = ({ params }) => { variables: DEFAULT_INITIAL_SEARCH_DATA }); - return ( - - {(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 ( - <> - 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} - /> - 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} - /> - - - > - ); - }} - + return ( + <> + 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} + /> + 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} + /> + + + > ); }; diff --git a/src/storybook/__snapshots__/Stories.test.ts.snap b/src/storybook/__snapshots__/Stories.test.ts.snap index b8e921dbe..822c6fa3e 100644 --- a/src/storybook/__snapshots__/Stories.test.ts.snap +++ b/src/storybook/__snapshots__/Stories.test.ts.snap @@ -2348,6 +2348,58 @@ exports[`Storyshots Generics / AddressFormatter when loading 1`] = ` `; +exports[`Storyshots Generics / Alert default 1`] = ` + + + + + + + + + Title + + + + + + + + + + Content + + + + + +`; + exports[`Storyshots Generics / Assign attributes dialog default 1`] = ` + + 2/5 channels used + + + 2/5 channels used + `; +exports[`Storyshots Views / Channels / Channels list limits reached 1`] = ` + + + + + Channels + + + + + 5/5 channels used + + + + Create Channel + + + + + + + + + + + + + Channel limit reached + + + + + + + + + + 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. + + + + + + + + + + + + + Channel Name + + + + + Actions + + + + + + + + Test + + + + + + + + + + + + + + + + Channel + + + + + + + + + + + + + + + + Channel test + + + + + + + + + + + + + + + + Channel USD + + + + + + + + + + + + + + + + Channel + + + + + + + + + + + + + + + + Channel test + + + + + + + + + + + + + + + + Channel USD + + + + + + + + + + + + + + + + + + +`; + +exports[`Storyshots Views / Channels / Channels list no limits 1`] = ` + + + + + Channels + + + + + + Create Channel + + + + + + + + + + + + + + Channel Name + + + + + Actions + + + + + + + + Test + + + + + + + + + + + + + + + + Channel + + + + + + + + + + + + + + + + Channel test + + + + + + + + + + + + + + + + Channel USD + + + + + + + + + + + + + + + + Channel + + + + + + + + + + + + + + + + Channel test + + + + + + + + + + + + + + + + Channel USD + + + + + + + + + + + + + + + + + + +`; + exports[`Storyshots Views / Channels / Delete channel default 1`] = ` `; +exports[`Storyshots Views / Orders / Order list limits reached 1`] = ` + + + + + Orders + + + + + + + + + + + + + + + Create order + + + + + + + + + + + + + Order limit reached + + + + + + + + + + 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. + + + + + + + + + + + All Orders + + + + + Tab X + + + + + + + + + + Filters + + + + + + + + + + + + + + + + + + + + + + + + No. of Order + + + + + + + + + + + Date + + + + + + + Customer + + + + + + + Payment + + + + + + + Fulfillment status + + + + + + + Total + + + + + + + + + + + + + No of Rows: + + + + 20 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + #20 + + + + in a month + + + + Laura Stone 1 2 + + + + Fully paid + + + + + Cancelled + + + + $305.17 + + + + + #19 + + + + in a month + + + + Elizabeth Vaughn + + + + Fully paid + + + + + Cancelled + + + + $1,215.89 + + + + + #18 + + + + in a month + + + + david.lawson@example.com + + + + Unpaid + + + + + Draft + + + + $321.71 + + + + + #17 + + + + in a month + + + + Aaron Randall + + + + Unpaid + + + + + Cancelled + + + + $271.95 + + + + + #16 + + + + in a month + + + + Laura Jensen + + + + Unpaid + + + + + Cancelled + + + + $335.84 + + + + + #15 + + + + in a month + + + + Jenna Villa + + + + Unpaid + + + + + Cancelled + + + + $1,042.15 + + + + + #14 + + + + in a month + + + + Wesley Davis + + + + Unpaid + + + + + Cancelled + + + + $213.69 + + + + + #13 + + + + in a month + + + + Anthony Gonzalez + + + + Unpaid + + + + + Cancelled + + + + $367.03 + + + + + #12 + + + + in a month + + + + Denise Freeman + + + + Unpaid + + + + + Cancelled + + + + $298.76 + + + + + #11 + + + + in a month + + + + James Ball + + + + Fully paid + + + + + Unfulfilled + + + + $663.69 + + + + + #10 + + + + in a month + + + + Michael Martinez + + + + Unpaid + + + + + Cancelled + + + + $280.41 + + + + + #9 + + + + in a month + + + + Melissa Simon + + + + Unpaid + + + + + Partially fulfilled + + + + $234.93 + + + + + #8 + + + + in a month + + + + Justin Mccoy + + + + Unpaid + + + + + Partially fulfilled + + + + $485.19 + + + + + #7 + + + + in a month + + + + Anthony Gonzalez + + + + Fully paid + + + + + Partially fulfilled + + + + $223.54 + + + + + #6 + + + + in a month + + + + Bradley Ford + + + + Unpaid + + + + + Fulfilled + + + + $237.55 + + + + + #5 + + + + in a month + + + + David Lawson + + + + Unpaid + + + + + Partially fulfilled + + + + $453.55 + + + + + #4 + + + + in a month + + + + Lauren Watson + + + + Unpaid + + + + + Partially fulfilled + + + + $812.67 + + + + + #3 + + + + in a month + + + + Mark Lee + + + + Unpaid + + + + + Partially fulfilled + + + + $481.41 + + + + + #2 + + + + in a month + + + + Kara Murphy + + + + Fully paid + + + + + Partially fulfilled + + + + $569.19 + + + + + #1 + + + + in a month + + + + Curtis Bailey + + + + Fully paid + + + + + Partially fulfilled + + + + $557.00 + + + + + + + + +`; + exports[`Storyshots Views / Orders / Order list loading 1`] = ` `; +exports[`Storyshots Views / Orders / Order list no limits 1`] = ` + + + + + Orders + + + + + + + + + + + + + + + Create order + + + + + + + + + + + + All Orders + + + + + Tab X + + + + + + + + + + Filters + + + + + + + + + + + + + + + + + + + + + + + + No. of Order + + + + + + + + + + + Date + + + + + + + Customer + + + + + + + Payment + + + + + + + Fulfillment status + + + + + + + Total + + + + + + + + + + + + + No of Rows: + + + + 20 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + #20 + + + + in a month + + + + Laura Stone 1 2 + + + + Fully paid + + + + + Cancelled + + + + $305.17 + + + + + #19 + + + + in a month + + + + Elizabeth Vaughn + + + + Fully paid + + + + + Cancelled + + + + $1,215.89 + + + + + #18 + + + + in a month + + + + david.lawson@example.com + + + + Unpaid + + + + + Draft + + + + $321.71 + + + + + #17 + + + + in a month + + + + Aaron Randall + + + + Unpaid + + + + + Cancelled + + + + $271.95 + + + + + #16 + + + + in a month + + + + Laura Jensen + + + + Unpaid + + + + + Cancelled + + + + $335.84 + + + + + #15 + + + + in a month + + + + Jenna Villa + + + + Unpaid + + + + + Cancelled + + + + $1,042.15 + + + + + #14 + + + + in a month + + + + Wesley Davis + + + + Unpaid + + + + + Cancelled + + + + $213.69 + + + + + #13 + + + + in a month + + + + Anthony Gonzalez + + + + Unpaid + + + + + Cancelled + + + + $367.03 + + + + + #12 + + + + in a month + + + + Denise Freeman + + + + Unpaid + + + + + Cancelled + + + + $298.76 + + + + + #11 + + + + in a month + + + + James Ball + + + + Fully paid + + + + + Unfulfilled + + + + $663.69 + + + + + #10 + + + + in a month + + + + Michael Martinez + + + + Unpaid + + + + + Cancelled + + + + $280.41 + + + + + #9 + + + + in a month + + + + Melissa Simon + + + + Unpaid + + + + + Partially fulfilled + + + + $234.93 + + + + + #8 + + + + in a month + + + + Justin Mccoy + + + + Unpaid + + + + + Partially fulfilled + + + + $485.19 + + + + + #7 + + + + in a month + + + + Anthony Gonzalez + + + + Fully paid + + + + + Partially fulfilled + + + + $223.54 + + + + + #6 + + + + in a month + + + + Bradley Ford + + + + Unpaid + + + + + Fulfilled + + + + $237.55 + + + + + #5 + + + + in a month + + + + David Lawson + + + + Unpaid + + + + + Partially fulfilled + + + + $453.55 + + + + + #4 + + + + in a month + + + + Lauren Watson + + + + Unpaid + + + + + Partially fulfilled + + + + $812.67 + + + + + #3 + + + + in a month + + + + Mark Lee + + + + Unpaid + + + + + Partially fulfilled + + + + $481.41 + + + + + #2 + + + + in a month + + + + Kara Murphy + + + + Fully paid + + + + + Partially fulfilled + + + + $569.19 + + + + + #1 + + + + in a month + + + + Curtis Bailey + + + + Fully paid + + + + + Partially fulfilled + + + + $557.00 + + + + + + + + +`; + exports[`Storyshots Views / Orders / Order list when no data 1`] = ` `; +exports[`Storyshots Views / Products / Product edit limits reached 1`] = ` + + + + + + Ergonomic Plastic Bacon + + + + + + + + + + + General Information + + + + + + + + + Name + + + + + + + Name + + + + + + + + + Description + + + + + + + + + + Product Rating + + + + + + + Product Rating + + + + + + + + + + + + + Media + + + + + Upload + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Attributes + + + + + + + + + + 2 Attributes + + + + + + + + + + + + + + + Borders + + + + + + + Value + + + + + + + + + + + + + + Value + + + + + + + + + + + + + Legacy + + + + + + + Values + + + + + + + + + + + + + + Values + + + + + + + + + + + Auto Loan Account + + + + + + + + + + + + + + + + + + + + Variants + + + + + Create variant + + + + + + + + + + + + + + SKU limit reached + + + + + + + + + + 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. + + + + + + + Available inventory at: + + + + All Warehouses + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Variant + + + SKU + + + Inventory + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Cordoba Oro + + Default + + + + 87192-94370 + + + 3 available at 2 locations + + + + + + + + + + + + + + + + + + + + + + + + + + + silver + + + 69055-15190 + + + 11 available at 1 location + + + + + + + + + + + + + + + + + + + + + + Search Engine Preview + + + + + Edit website SEO + + + + + + + + + Add search engine title and description to make this product easier to find + + + + + + + + Metadata + + + + + + + + + 1 Field + + + + + + + + + + + + + + + Field + + + Value + + + Actions + + + + + + + + + + + + + + + + + + + + + + + + 100023123 + + + + + + + + + + + + + + + + + + + + + + + + + + + + Add Field + + + + + + + + + Private Metadata + + + + + + + + + There is no private metadata created for this element. + + + Use the button below to add new metadata field + + + + + + Add Field + + + + + + + + + + Organize Product + + + + + + + + Product Type + + + Versatile + + + + Product Type + + + Configurable + + + + + + + + Category + + + + + + + + + + + + + + Category + + + + + + + + + + + + + Collections + + + + + + + + + + + + + + Collections + + + + + + *Optional. Adding product to collection helps users find it. + + + + + + + + Winter sale + + + + + + + + + + + + + + + + + + Availability + + + + + + + + Available at 0 out of 5 channels + + + + + + + + + Taxes + + + + + + + + + + + + + + + + + + Override the product type's tax rate + + + + + + + + + + + + + + + Charge taxes on this product + + + + + + Tax Rate + + + + + + + + + + + + + + Tax Rate + + + + + + + + + + + + + +`; + +exports[`Storyshots Views / Products / Product edit no limits 1`] = ` + + + + + + Ergonomic Plastic Bacon + + + + + + + + + + + General Information + + + + + + + + + Name + + + + + + + Name + + + + + + + + + Description + + + + + + + + + + Product Rating + + + + + + + Product Rating + + + + + + + + + + + + + Media + + + + + Upload + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Attributes + + + + + + + + + + 2 Attributes + + + + + + + + + + + + + + + Borders + + + + + + + Value + + + + + + + + + + + + + + Value + + + + + + + + + + + + + Legacy + + + + + + + Values + + + + + + + + + + + + + + Values + + + + + + + + + + + Auto Loan Account + + + + + + + + + + + + + + + + + + + + Variants + + + + + Create variant + + + + + + + + + Available inventory at: + + + + All Warehouses + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Variant + + + SKU + + + Inventory + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Cordoba Oro + + Default + + + + 87192-94370 + + + 3 available at 2 locations + + + + + + + + + + + + + + + + + + + + + + + + + + + silver + + + 69055-15190 + + + 11 available at 1 location + + + + + + + + + + + + + + + + + + + + + + Search Engine Preview + + + + + Edit website SEO + + + + + + + + + Add search engine title and description to make this product easier to find + + + + + + + + Metadata + + + + + + + + + 1 Field + + + + + + + + + + + + + + + Field + + + Value + + + Actions + + + + + + + + + + + + + + + + + + + + + + + + 100023123 + + + + + + + + + + + + + + + + + + + + + + + + + + + + Add Field + + + + + + + + + Private Metadata + + + + + + + + + There is no private metadata created for this element. + + + Use the button below to add new metadata field + + + + + + Add Field + + + + + + + + + + Organize Product + + + + + + + + Product Type + + + Versatile + + + + Product Type + + + Configurable + + + + + + + + Category + + + + + + + + + + + + + + Category + + + + + + + + + + + + + Collections + + + + + + + + + + + + + + Collections + + + + + + *Optional. Adding product to collection helps users find it. + + + + + + + + Winter sale + + + + + + + + + + + + + + + + + + Availability + + + + + + + + Available at 0 out of 5 channels + + + + + + + + + Taxes + + + + + + + + + + + + + + + + + + Override the product type's tax rate + + + + + + + + + + + + + + + Charge taxes on this product + + + + + + Tax Rate + + + + + + + + + + + + + + Tax Rate + + + + + + + + + + + + + +`; + exports[`Storyshots Views / Products / Product edit no product attributes 1`] = ` + + 100/1000 SKUs used + `; +exports[`Storyshots Views / Products / Product list limits reached 1`] = ` + + + + + Products + + + + + 1000/1000 SKUs used + + + + + + + + + + + + + + Columns + + + + + + + + + Create Product + + + + + + + + + + + + + SKU limit reached + + + + + + + + + + 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. + + + + + + + + + + + All Products + + + + + Tab X + + + + + + + + + + Filters + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Name + + + + + + + + + + + + Type + + + + + + + Availability + + + + + + + Price + + + + + + + + + + + + + No of Rows: + + + + 20 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Nebula Night Sky Paint + + + Configurable + + + + + + + Paint + + + + + + Available in 2/6 + + + + + + $1.20 - $3.50 + + + + + + + + + + + + + + + + + + + + + + Light Speed Yellow Paint + + + Configurable + + + + + + + Paint + + + + + + Available in 2/6 + + + + + + $1.20 - $3.50 + + + + + + + + + + + + + + + + + + + + + + Hyperspace Turquoise Paint + + + Configurable + + + + + + + Paint + + + + + + Available in 2/6 + + + + + + $1.20 - $3.50 + + + + + + + + + + + + + + + + + + + + + + Pineapple Juice + + + Configurable + + + + + + + Juice + + + + + + Available in 2/6 + + + + + + $1.20 - $3.50 + + + + + + + + + + + + + + + + + + + + + + Coconut Juice + + + Configurable + + + + + + + Juice + + + + + + Available in 2/6 + + + + + + $1.20 - $3.50 + + + + + + + + + + + + + + + + + + + + + + Apple Juice + + + Configurable + + + + + + + Juice + + + + + + Available in 2/6 + + + + + + $1.20 - $3.50 + + + + + + + + + + + + + + + + + + + + + + Orange Juice + + + Configurable + + + + + + + Juice + + + + + + Available in 2/6 + + + + + + $1.20 - $3.50 + + + + + + + + + + + + + + + + + + + + + + Banana Juice + + + Configurable + + + + + + + Juice + + + + + + Available in 2/6 + + + + + + $1.20 - $3.50 + + + + + + + + + + + + + + + + + + + + + + Bean Juice + + + Configurable + + + + + + + Juice + + + + + + Available in 2/6 + + + + + + $1.20 - $3.50 + + + + + + + + + + + + + + + + + + + + + + Carrot Juice + + + Configurable + + + + + + + Juice + + + + + + Available in 2/6 + + + + + + $1.20 - $3.50 + + + + + + + + + + + + + + + + + + + + + + Green Juice + + + Configurable + + + + + + + Juice + + + + + + Available in 2/6 + + + + + + $1.20 - $3.50 + + + + + + + + + + + + + + + + + + + + + + Code Division T-shirt + + + Configurable + + + + + + + Top (clothing) + + + + + + Available in 2/6 + + + + + + $1.20 - $3.50 + + + + + + + + + + + + + + + + + + + + + + Polo Shirt + + + Configurable + + + + + + + Top (clothing) + + + + + + Available in 2/6 + + + + + + $1.20 - $3.50 + + + + + + + + + + + + + + + + + + + + + + Polo Shirt + + + Configurable + + + + + + + Top (clothing) + + + + + + Available in 2/6 + + + + + + $1.20 - $3.50 + + + + + + + + + + + + + + + + + + + + + + Polo Shirt + + + Configurable + + + + + + + Top (clothing) + + + + + + Available in 2/6 + + + + + + $1.20 - $3.50 + + + + + + + + + + + + + + + + + + + + + + Polo Shirt + + + Configurable + + + + + + + Top (clothing) + + + + + + Available in 2/6 + + + + + + $1.20 - $3.50 + + + + + + + + + + + + + + + + + + + + + + Black Hoodie + + + Configurable + + + + + + + Top (clothing) + + + + + + Available in 2/6 + + + + + + $1.20 - $3.50 + + + + + + + + + + + + + + + + + + + + + + Blue Hoodie + + + Configurable + + + + + + + Top (clothing) + + + + + + Available in 2/6 + + + + + + $1.20 - $3.50 + + + + + + + + + + + + + + + + + + + + + + Mustard Hoodie + + + Configurable + + + + + + + Top (clothing) + + + + + + Available in 2/6 + + + + + + $1.20 - $3.50 + + + + + + + + + + + + + + + + + + + + + + Colored Parrot Cushion + + + Configurable + + + + + + + Cushion + + + + + + Available in 2/6 + + + + + + $1.20 - $3.50 + + + + + + + + + +`; + exports[`Storyshots Views / Products / Product list loading 1`] = ` + + 100/1000 SKUs used + + + 100/1000 SKUs used + + + 100/1000 SKUs used + `; +exports[`Storyshots Views / Products / Product list no limits 1`] = ` + + + + + Products + + + + + + + + + + + + + + + + Columns + + + + + + + + + Create Product + + + + + + + + + + + + All Products + + + + + Tab X + + + + + + + + + + Filters + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Name + + + + + + + + + + + + Type + + + + + + + Availability + + + + + + + Price + + + + + + + + + + + + + No of Rows: + + + + 20 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Nebula Night Sky Paint + + + Configurable + + + + + + + Paint + + + + + + Available in 2/6 + + + + + + $1.20 - $3.50 + + + + + + + + + + + + + + + + + + + + + + Light Speed Yellow Paint + + + Configurable + + + + + + + Paint + + + + + + Available in 2/6 + + + + + + $1.20 - $3.50 + + + + + + + + + + + + + + + + + + + + + + Hyperspace Turquoise Paint + + + Configurable + + + + + + + Paint + + + + + + Available in 2/6 + + + + + + $1.20 - $3.50 + + + + + + + + + + + + + + + + + + + + + + Pineapple Juice + + + Configurable + + + + + + + Juice + + + + + + Available in 2/6 + + + + + + $1.20 - $3.50 + + + + + + + + + + + + + + + + + + + + + + Coconut Juice + + + Configurable + + + + + + + Juice + + + + + + Available in 2/6 + + + + + + $1.20 - $3.50 + + + + + + + + + + + + + + + + + + + + + + Apple Juice + + + Configurable + + + + + + + Juice + + + + + + Available in 2/6 + + + + + + $1.20 - $3.50 + + + + + + + + + + + + + + + + + + + + + + Orange Juice + + + Configurable + + + + + + + Juice + + + + + + Available in 2/6 + + + + + + $1.20 - $3.50 + + + + + + + + + + + + + + + + + + + + + + Banana Juice + + + Configurable + + + + + + + Juice + + + + + + Available in 2/6 + + + + + + $1.20 - $3.50 + + + + + + + + + + + + + + + + + + + + + + Bean Juice + + + Configurable + + + + + + + Juice + + + + + + Available in 2/6 + + + + + + $1.20 - $3.50 + + + + + + + + + + + + + + + + + + + + + + Carrot Juice + + + Configurable + + + + + + + Juice + + + + + + Available in 2/6 + + + + + + $1.20 - $3.50 + + + + + + + + + + + + + + + + + + + + + + Green Juice + + + Configurable + + + + + + + Juice + + + + + + Available in 2/6 + + + + + + $1.20 - $3.50 + + + + + + + + + + + + + + + + + + + + + + Code Division T-shirt + + + Configurable + + + + + + + Top (clothing) + + + + + + Available in 2/6 + + + + + + $1.20 - $3.50 + + + + + + + + + + + + + + + + + + + + + + Polo Shirt + + + Configurable + + + + + + + Top (clothing) + + + + + + Available in 2/6 + + + + + + $1.20 - $3.50 + + + + + + + + + + + + + + + + + + + + + + Polo Shirt + + + Configurable + + + + + + + Top (clothing) + + + + + + Available in 2/6 + + + + + + $1.20 - $3.50 + + + + + + + + + + + + + + + + + + + + + + Polo Shirt + + + Configurable + + + + + + + Top (clothing) + + + + + + Available in 2/6 + + + + + + $1.20 - $3.50 + + + + + + + + + + + + + + + + + + + + + + Polo Shirt + + + Configurable + + + + + + + Top (clothing) + + + + + + Available in 2/6 + + + + + + $1.20 - $3.50 + + + + + + + + + + + + + + + + + + + + + + Black Hoodie + + + Configurable + + + + + + + Top (clothing) + + + + + + Available in 2/6 + + + + + + $1.20 - $3.50 + + + + + + + + + + + + + + + + + + + + + + Blue Hoodie + + + Configurable + + + + + + + Top (clothing) + + + + + + Available in 2/6 + + + + + + $1.20 - $3.50 + + + + + + + + + + + + + + + + + + + + + + Mustard Hoodie + + + Configurable + + + + + + + Top (clothing) + + + + + + Available in 2/6 + + + + + + $1.20 - $3.50 + + + + + + + + + + + + + + + + + + + + + + Colored Parrot Cushion + + + Configurable + + + + + + + Cushion + + + + + + Available in 2/6 + + + + + + $1.20 - $3.50 + + + + + + + + + +`; + exports[`Storyshots Views / Products / Product list with data 1`] = ` + + 100/1000 SKUs used + + + + + Staff Members + + + + + 7/10 members + + + + Invite staff member + + + + + + + + + + + + All Staff Members + + + + + Tab X + + + + + + + + + + Filters + + + + + + + + + + + + + + + + + + + + + + + + + + + + Name + + + + + + + + + + + Email Address + + + + + + + + + + + + + No of Rows: + + + + 20 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Chris Cooper + + + Active + + + + admin@example.com + + + + + + + + + Jacob Smith + + + Inactive + + + + admin@example.com + + + + + + + + + Jacob Smith + + + Active + + + + admin@example.com + + + + + + + + + Jacob Smith + + + Active + + + + admin@example.com + + + + + + + + + Jacob Smith + + + Active + + + + admin@example.com + + + + + + + + + Jacob Smith + + + Active + + + + admin@example.com + + + + + + + + + Jacob Smith + + + Inactive + + + + admin@example.com + + + + + + + + + Jacob Smith + + + Active + + + + admin@example.com + + + + + + + + + Jacob Smith + + + Active + + + + admin@example.com + + + + + + + + + Jacob Smith + + + Inactive + + + + admin@example.com + + + + + + + + + Jacob Smith + + + Inactive + + + + admin@example.com + + + + + + + + + Jacob Smith + + + Active + + + + admin@example.com + + + + + + + + +`; + +exports[`Storyshots Views / Staff / Staff members limits reached 1`] = ` + + + + + Staff Members + + + + + 10/10 members + + + + Invite staff member + + + + + + + + + + + + + Staff Member limit reached + + + + + + + + + + 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. + + + + + + + + + + + All Staff Members + + + + + Tab X + + + + + + + + + + Filters + + + + + + + + + + + + + + + + + + + + + + + + + + + + Name + + + + + + + + + + + Email Address + + + + + + + + + + + + + No of Rows: + + + + 20 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Chris Cooper + + + Active + + + + admin@example.com + + + + + + + + + Jacob Smith + + + Inactive + + + + admin@example.com + + + + + + + + + Jacob Smith + + + Active + + + + admin@example.com + + + + + + + + + Jacob Smith + + + Active + + + + admin@example.com + + + + + + + + + Jacob Smith + + + Active + + + + admin@example.com + + + + + + + + + Jacob Smith + + + Active + + + + admin@example.com + + + + + + + + + Jacob Smith + + + Inactive + + + + admin@example.com + + + + + + + + + Jacob Smith + + + Active + + + + admin@example.com + + + + + + + + + Jacob Smith + + + Active + + + + admin@example.com + + + + + + + + + Jacob Smith + + + Inactive + + + + admin@example.com + + + + + + + + + Jacob Smith + + + Inactive + + + + admin@example.com + + + + + + + + + Jacob Smith + + + Active + + + + admin@example.com + + + + + + + + +`; + +exports[`Storyshots Views / Staff / Staff members no limits 1`] = ` @@ -218206,6 +231697,11 @@ exports[`Storyshots Views / Staff / Staff members when loading 1`] = ` + + 7/10 members + + + 1/3 warehouses used + `; +exports[`Storyshots Views / Warehouses / Warehouse list limits reached 1`] = ` + + + + + Warehouses + + + + + 3/3 warehouses used + + + + Create Warehouse + + + + + + + + + + + + + Warehouse limit reached + + + + + + + + + + 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. + + + + + + + + + + + All Warehouses + + + + + Tab X + + + + + + + + + + + + + + + + + + + + + + + + + + + Name + + + + + + + + + Shipping Zones + + + Actions + + + + + + + + + + + No of Rows: + + + + 20 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + C our wares + + + Europe, Oceania, Asia, Americas, Africa + + + + + + + + + + + + + + + + + + + + + + + Be stocked + + + Europe, Oceania, Asia, Americas, Africa + + + + + + + + + + + + + + + + + + + + + + + A Warehouse + + + Europe, Oceania, Asia, Americas, Africa + + + + + + + + + + + + + + + + + + + + + + + Darkwares + + + Europe, Oceania, Asia, Americas, Africa + + + + + + + + + + + + + + + + + + + + + + + + + + +`; + exports[`Storyshots Views / Warehouses / Warehouse list loading 1`] = ` + + 1/3 warehouses used + + + 1/3 warehouses used + `; + +exports[`Storyshots Views / Warehouses / Warehouse list no limits 1`] = ` + + + + + Warehouses + + + + + + Create Warehouse + + + + + + + + + + + + All Warehouses + + + + + Tab X + + + + + + + + + + + + + + + + + + + + + + + + + + + Name + + + + + + + + + Shipping Zones + + + Actions + + + + + + + + + + + No of Rows: + + + + 20 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + C our wares + + + Europe, Oceania, Asia, Americas, Africa + + + + + + + + + + + + + + + + + + + + + + + Be stocked + + + Europe, Oceania, Asia, Americas, Africa + + + + + + + + + + + + + + + + + + + + + + + A Warehouse + + + Europe, Oceania, Asia, Americas, Africa + + + + + + + + + + + + + + + + + + + + + + + Darkwares + + + Europe, Oceania, Asia, Americas, Africa + + + + + + + + + + + + + + + + + + + + + + + + + + +`; diff --git a/src/storybook/stories/orders/OrderListPage.tsx b/src/storybook/stories/orders/OrderListPage.tsx index b96142e84..6234ba90a 100644 --- a/src/storybook/stories/orders/OrderListPage.tsx +++ b/src/storybook/stories/orders/OrderListPage.tsx @@ -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", () => ); + .add("when no data", () => ) + .add("no limits", () => ) + .add("limits reached", () => ( + + )); diff --git a/src/storybook/stories/products/ProductListPage.tsx b/src/storybook/stories/products/ProductListPage.tsx index dc7112aee..ea30c87a9 100644 --- a/src/storybook/stories/products/ProductListPage.tsx +++ b/src/storybook/stories/products/ProductListPage.tsx @@ -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", () => ) + .add("limits reached", () => ( + )); diff --git a/src/storybook/stories/products/ProductUpdatePage.tsx b/src/storybook/stories/products/ProductUpdatePage.tsx index 24f12e1dc..868f7f69f 100644 --- a/src/storybook/stories/products/ProductUpdatePage.tsx +++ b/src/storybook/stories/products/ProductUpdatePage.tsx @@ -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", () => ( + )) + .add("no limits", () => ) + .add("limits reached", () => ( + )); diff --git a/src/theme/buttons.ts b/src/theme/buttons.ts index afc8d0bdb..70efd07e2 100644 --- a/src/theme/buttons.ts +++ b/src/theme/buttons.ts @@ -45,7 +45,7 @@ const buttonOverrides = (colors: IThemeColors): Overrides => ({ } }, textPrimary: { - "& span": { + "&:not($disabled) span": { color: colors.primary } } diff --git a/src/theme/createSaleorTheme.ts b/src/theme/createSaleorTheme.ts index 16775a413..7aefc33cc 100644 --- a/src/theme/createSaleorTheme.ts +++ b/src/theme/createSaleorTheme.ts @@ -217,7 +217,8 @@ const createTheme = (colors: IThemeColors): SaleorTheme => color: colors.font.default }, h5: { - fontSize: "1.3125rem" + fontSize: "1.3125rem", + fontWeight: 500 } } }); diff --git a/src/theme/palette.ts b/src/theme/palette.ts index 6e3be3e03..73b7e6592 100644 --- a/src/theme/palette.ts +++ b/src/theme/palette.ts @@ -6,6 +6,7 @@ const createPalette = (colors: IThemeColors): SaleorPaletteOptions => ({ action: { active: colors.checkbox.default }, + alert: colors.alert, background: colors.background, divider: colors.divider, error: { diff --git a/src/theme/types.ts b/src/theme/types.ts index 02d098ce8..c218a4b64 100644 --- a/src/theme/types.ts +++ b/src/theme/types.ts @@ -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; diff --git a/src/utils/api.ts b/src/utils/api.ts new file mode 100644 index 000000000..75c61364e --- /dev/null +++ b/src/utils/api.ts @@ -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) + ); +} diff --git a/src/utils/limits.ts b/src/utils/limits.ts new file mode 100644 index 000000000..1a1631e26 --- /dev/null +++ b/src/utils/limits.ts @@ -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]; +} diff --git a/src/warehouses/components/WarehouseListPage/WarehouseListPage.stories.tsx b/src/warehouses/components/WarehouseListPage/WarehouseListPage.stories.tsx index 42f304c76..949de4499 100644 --- a/src/warehouses/components/WarehouseListPage/WarehouseListPage.stories.tsx +++ b/src/warehouses/components/WarehouseListPage/WarehouseListPage.stories.tsx @@ -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", () => ( )) - .add("no data", () => ); + .add("no data", () => ) + .add("no limits", () => ) + .add("limits reached", () => ( + + )); diff --git a/src/warehouses/components/WarehouseListPage/WarehouseListPage.tsx b/src/warehouses/components/WarehouseListPage/WarehouseListPage.tsx index 6439c353f..d4225598f 100644 --- a/src/warehouses/components/WarehouseListPage/WarehouseListPage.tsx +++ b/src/warehouses/components/WarehouseListPage/WarehouseListPage.tsx @@ -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, TabPageProps { + limits: RefreshLimits_shop_limits; warehouses: WarehouseWithShippingFragment[]; onBack: () => void; onRemove: (id: string) => void; @@ -32,6 +36,7 @@ export const WarehouseListPage: React.FC = ({ warehouses, currentTab, disabled, + limits, initialSearch, pageInfo, settings, @@ -52,19 +57,44 @@ export const WarehouseListPage: React.FC = ({ }) => { const intl = useIntl(); + const limitReached = isLimitReached(limits, "warehouses"); + return ( - - + + + + + = ({ 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 = ({ params }) => { text: intl.formatMessage(commonMessages.savedChanges) }); refetch(); + limitOpts.refetch(); closeModal(); } } @@ -141,6 +148,7 @@ const WarehouseList: React.FC = ({ 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}
+ *Optional. Adding product to collection helps users find it. +