From a3abb9c476dbe1616ee628c04b97785a0e81e1aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20=C5=BBegle=C5=84?= Date: Tue, 13 Apr 2021 11:59:16 +0200 Subject: [PATCH] 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 --- CHANGELOG.md | 2 + assets/images/alert-dark.svg | 5 + assets/images/alert.svg | 5 + locale/defaultMessages.json | 52 + .../ChannelsListPage.stories.tsx | 8 +- .../ChannelsListPage/ChannelsListPage.tsx | 28 +- .../views/ChannelsList/ChannelsList.tsx | 10 +- src/components/Alert/Alert.stories.tsx | 13 + src/components/Alert/Alert.tsx | 84 + src/components/Alert/index.ts | 0 src/components/PageHeader/PageHeader.tsx | 29 +- src/components/Shop/query.ts | 22 + src/components/Shop/types/RefreshLimits.ts | 49 + src/components/Theme/themes.ts | 32 + src/components/messages/styles.ts | 2 +- src/fixtures.ts | 40 + src/fragments/shop.ts | 21 + src/fragments/types/LimitInfoFragment.ts | 17 + src/fragments/types/ShopLimitFragment.ts | 37 + src/hooks/makeMutation.ts | 13 +- src/hooks/useForm.ts | 2 +- src/intl.ts | 3 + src/mutations.tsx | 8 +- .../OrderListPage/OrderListPage.tsx | 14 + src/orders/views/OrderList/OrderList.tsx | 7 + .../ProductListPage/ProductListPage.tsx | 28 +- .../ProductUpdatePage.test.tsx | 3 +- .../ProductUpdatePage/ProductUpdatePage.tsx | 4 + .../ProductVariantCreator.stories.tsx | 14 +- .../ProductVariantCreatorContent.tsx | 3 + .../ProductVariantCreatorPage.tsx | 24 +- .../ProductVariantCreatorValues.tsx | 31 +- .../ProductVariants/ProductVariants.tsx | 22 + .../views/ProductList/ProductList.tsx | 8 + .../views/ProductUpdate/ProductUpdate.tsx | 8 + .../ProductVariantCreator.tsx | 7 + .../StaffListPage/StaffListPage.stories.tsx | 7 + .../StaffListPage/StaffListPage.tsx | 33 +- src/staff/mutations.ts | 2 +- src/staff/views/StaffList/StaffList.tsx | 189 +- .../__snapshots__/Stories.test.ts.snap | 14668 +++++++++++++++- .../stories/orders/OrderListPage.tsx | 9 +- .../stories/products/ProductListPage.tsx | 7 + .../stories/products/ProductUpdatePage.tsx | 12 +- src/theme/buttons.ts | 2 +- src/theme/createSaleorTheme.ts | 3 +- src/theme/palette.ts | 1 + src/theme/types.ts | 6 + src/utils/api.ts | 12 + src/utils/limits.ts | 24 + .../WarehouseListPage.stories.tsx | 9 +- .../WarehouseListPage/WarehouseListPage.tsx | 34 +- .../views/WarehouseList/WarehouseList.tsx | 8 + 53 files changed, 15543 insertions(+), 138 deletions(-) create mode 100644 assets/images/alert-dark.svg create mode 100644 assets/images/alert.svg create mode 100644 src/components/Alert/Alert.stories.tsx create mode 100644 src/components/Alert/Alert.tsx create mode 100644 src/components/Alert/index.ts create mode 100644 src/components/Shop/types/RefreshLimits.ts create mode 100644 src/fragments/types/LimitInfoFragment.ts create mode 100644 src/fragments/types/ShopLimitFragment.ts create mode 100644 src/utils/api.ts create mode 100644 src/utils/limits.ts 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} /> + + + 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 ) : ( + + + (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 +
+
+
+ +
+
+
+
+
+
+
+ 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 +
+
+
+ +
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ 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 +
+
+
+
+ +
+ +
+
+
+
+
+
+
+
+
+
+ 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. +
+
+
+
+
+
+
+
+ + +
+
+
+
+
+ +
+
+
+ + +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ No. of Order +
+ +
+
+
+
+ Date +
+
+
+
+
+ Customer +
+
+
+
+
+ Payment +
+
+
+
+
+ Fulfillment status +
+
+
+
+
+ Total +
+
+
+
+
+
+
+`; + exports[`Storyshots Views / Orders / Order list loading 1`] = `
`; +exports[`Storyshots Views / Orders / Order list no limits 1`] = ` +
+
+
+
+ Orders +
+
+
+
+ +
+ +
+
+
+
+
+
+
+ + +
+
+
+
+
+ +
+
+
+ + +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ No. of Order +
+ +
+
+
+
+ Date +
+
+
+
+
+ Customer +
+
+
+
+
+ Payment +
+
+
+
+
+ Fulfillment status +
+
+
+
+
+ Total +
+
+
+
+
+
+
+`; + exports[`Storyshots Views / Orders / Order list when no data 1`] = `
+ +
+
+
+
+
+
+
+
+
+
+
+
+ Id sit dolores adipisci +
+
+
+
+
+ Id sit dolores adipisci +
+
+
+
+
+ Id sit dolores adipisci +
+
+
+
+
+ Id sit dolores adipisci +
+
+
+
+
+ Id sit dolores adipisci +
+
+
+
+
+
+
+
+ + Attributes + +
+
+
+
+
+
+
+
+ 2 Attributes +
+
+ +
+
+
+
+
+ Borders +
+
+
+
+
+ + +
+
+
+
+
+
+
+
+ Legacy +
+
+
+
+
+ + +
+
+
+
+
+
+ Auto Loan Account +
+ +
+
+
+
+
+
+
+
+
+
+ + Variants + +
+ +
+
+
+
+
+
+
+
+
+
+
+ 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 + +
+
+
+
+
+
+ + Search Engine Preview + +
+ +
+
+
+
+
+
+ Add search engine title and description to make this product easier to find +
+
+
+
+
+
+ + Metadata + +
+
+
+
+ + + + + + + + + + + + +